-
Notifications
You must be signed in to change notification settings - Fork 1
/
ifarchiveindexmod.py
249 lines (203 loc) · 8.13 KB
/
ifarchiveindexmod.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
import os
import os.path
import re
from collections import OrderedDict
"""ifarchiveindexmod:
This module lets you programmatically modify the metadata in selected Index
files.
(It does not currently allow you to create a completely new Index file.
Do that by hand.)
To use:
from ifarchiveindexmod import IndexMod
# pathname is the directory that contains if-archive and indexes
indexmod = IndexMod(pathname)
ent = indexmod.getfile('if-archive/games/whatever.dat')
ent.add_metadata('key', 'value')
# olddir is a directory where the original Index files will be moved
# as a backup.
indexmod.rewrite(olddir)
You may add metadata to any number of files before the rewrite() call.
If you call getfile() for a filename which is not present in its Index
file, it will be added as a new entry.
"""
class IndexMod:
def __init__(self, rootdir):
self.rootdir = rootdir
self.archivedir = os.path.join(self.rootdir, 'if-archive')
if not os.path.exists(self.archivedir):
raise Exception('%s does not contain an if-archive directory' % (self.archivedir,))
self.dirs = {}
def hasfile(self, pathname):
"""Check whether an Index entry exists for the given pathname.
"""
(dirname, filename) = self.split(pathname)
if dirname in self.dirs:
dir = self.dirs[dirname]
else:
try:
dir = IndexDir(dirname, rootdir=self.rootdir)
except FileNotFoundError:
return False
self.dirs[dirname] = dir
return dir.hasfile(filename)
def getfile(self, pathname):
"""Return an IndexFile entry, creating it if necessary.
"""
(dirname, filename) = self.split(pathname)
if dirname in self.dirs:
dir = self.dirs[dirname]
else:
dir = IndexDir(dirname, rootdir=self.rootdir)
self.dirs[dirname] = dir
return dir.getfile(filename)
def split(self, pathname):
"""Split a pathname into (dirname, filename).
The "if-archive/" (or "/if-archive/") on the front of the pathname
is optional.
"""
if pathname.startswith('/'):
pathname = pathname[1:]
if not pathname.startswith('if-archive/'):
pathname = 'if-archive/' + pathname
(dirname, sep, filename) = pathname.rpartition('/')
if not (sep and filename):
raise Exception('%s does not have a file path' % (pathname,))
return (dirname, filename)
def rewrite(self, olddir=None, dryrun=False):
"""Write back out all the Index files which have changed.
If the olddir argument is provided, the original Index files are
moved to that directory (with unique names).
If dryrun is True, Index-new files are written (in place) but
the original Index files are left untouched.
"""
for (dirname, dir) in self.dirs.items():
if dir.isdirty():
print('Rewriting %s' % (dir.dirname,))
dir.rewrite(olddir, dryrun=dryrun)
# A filename header starts with exactly one "#" (an h1 header in Markdown)
filename_pattern = re.compile('^#[^#]')
# These patterns match the rules of metadata lines as defined in the
# Markdown extension:
# https://python-markdown.github.io/extensions/meta_data/
meta_start_pattern = re.compile('^[ ]?[ ]?[ ]?([a-zA-Z0-9_-]+):')
meta_cont_pattern = re.compile('^( |\\t)')
class IndexDir:
def __init__(self, dirname, rootdir=None):
self.dirname = dirname
self.indexpath = os.path.join(rootdir, dirname, 'Index')
self.description = []
self.files = OrderedDict()
# Parse the existing Index file.
infl = open(self.indexpath, encoding='utf-8')
curfile = None
curmetaline = None
for ln in infl.readlines():
if filename_pattern.match(ln):
# File entry header.
filename = ln[1:].strip()
curfile = IndexFile(filename, self)
curmetaline = True
self.files[filename] = curfile
continue
if not curfile:
# Directory description line.
self.description.append(ln)
continue
# Part of the file entry.
if curmetaline is not None:
match = meta_start_pattern.match(ln)
match2 = meta_cont_pattern.match(ln)
if ln.strip() == '':
curmetaline = None
elif match:
# New metadata line
curmetaline = match.group(1)
val = ln[match.end() : ].strip()
curfile.add_metadata(curmetaline, val, dirty=False)
continue
elif type(curmetaline) is str and match2:
val = ln[match2.end() : ].strip()
curfile.add_metadata(curmetaline, val, dirty=False)
continue
else:
curmetaline = None
# We're done with the metadata, so this is a description line.
# For consistency, the description will always start with a blank line.
if len(curfile.description) == 0 and ln.strip() != '':
curfile.description.append('\n')
curfile.description.append(ln)
infl.close()
def __repr__(self):
return '<IndexDir %s>' % (self.dirname,)
def isdirty(self):
for file in self.files.values():
if file.dirty:
return True
return False
def hasfile(self, filename):
if filename in self.files:
return True
return False
def getfile(self, filename):
"""Return an IndexFile entry, creating it if necessary.
"""
if filename in self.files:
return self.files[filename]
# Create a new (dirty) IndexFile entry.
curfile = IndexFile(filename, self)
self.files[filename] = curfile
curfile.description.append('\n')
curfile.dirty = True
return curfile
def rewrite(self, olddir=None, dryrun=False):
"""Write the Index file back out.
"""
newpath = self.indexpath+'-new'
outfl = open(newpath, 'w', encoding='utf-8')
for ln in self.description:
outfl.write(ln)
if not self.description or self.description[-1].strip():
# Description should end with a blank line.
outfl.write('\n')
for (filename, file) in self.files.items():
outfl.write('# %s\n' % (file.filename,))
for (key, ls) in file.metadata.items():
first = True
for val in ls:
if first:
outfl.write('%s: %s\n' % (key, val,))
first = False
else:
outfl.write(' %s\n' % (val,))
for ln in file.description:
outfl.write(ln)
if not file.description or file.description[-1].strip():
# Description should end with a blank line.
outfl.write('\n')
if not dryrun:
file.dirty = False
outfl.close()
if dryrun:
return
if olddir:
val = self.dirname.replace('/', 'X') + 'XIndex'
oldpath = os.path.join(olddir, val)
os.replace(self.indexpath, oldpath)
os.replace(newpath, self.indexpath)
class IndexFile:
def __init__(self, filename, dir):
self.filename = filename
self.dir = dir
self.description = []
self.metadata = OrderedDict()
self.dirty = False
def __repr__(self):
return '<IndexFile %s>' % (self.filename,)
def add_metadata(self, key, val, dirty=True):
ls = self.metadata.get(key)
if ls is None:
ls = []
self.metadata[key] = ls
ls.append(val)
if dirty:
self.dirty = True