Merge lp:~jderose/filestore/explicit-create into lp:filestore
- explicit-create
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 320 |
Proposed branch: | lp:~jderose/filestore/explicit-create |
Merge into: | lp:filestore |
Diff against target: |
3188 lines (+1996/-623) 9 files modified
benchmark-protocol.py (+23/-42) debian/control (+2/-2) filestore/__init__.py (+230/-174) filestore/migration.py (+281/-0) filestore/misc.py (+16/-8) filestore/tests/__init__.py (+816/-388) filestore/tests/run.py (+5/-1) filestore/tests/test_migration.py (+548/-0) filestore/tests/test_misc.py (+75/-8) |
To merge this branch: | bzr merge lp:~jderose/filestore/explicit-create |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Raymond | Approve | ||
Review via email: mp+163821@code.launchpad.net |
Commit message
Description of the change
For background, see this bug:
https:/
The are two interwoven goals here. The first is to force the programmer always to choose between assuming no file-store yet exists in a given parentdir, or assuming a file-store already exists in a given parentdir, with an unforgiving mountain of exceptions and double checks to punish anyone who crosses the streams. Architecturally the previous fuzzy gray area here was a really bad call on my part, and so I'm trying to atone now =)
The second is to cleanly separate out the V0 to V1 upgrade functionality from day to day FileStore functionality. Because the act of creating a FileStore instance could implicitly initialize a file-store layout where there wasn't one before, the upgrade functionality really had to be hooked into FileStore.
It's better to do the "soft-upgrade" (basically, moving files/ to files0/) before you create a FileStore instance, which you can now do:
>>> parentdir = '/media/
>>> m = Migration(
>>> m.needs_upgrade() # Soft upgrade if needed, but wont "create" anything
>>> fs = FileStore(
>>> fs = FileStore.
If `Migration.
Anyway, changes include:
* A file-store layout now must always be explicitly initialized, which is done using the new FileStore.create() classmethod:
FileStore.
If path.join(
This is a slightly different API than originally discussed in the bug, but there are a few circumstances where manually assigned the ID is handy. For one, unit testing, like to see what happens if you try to connect two physically distinct file-stores that happen to have the same ID. Also, for performance reasons one might want to periodically format a drive, but then initialize the file-store using the previous ID so the ID is stable over time.
Any additional **kw get included in the filestore.json document. The two that seem handy to me at the moment are `uuid` and `label` (the containing partition's UUID and Label).
* On the other hand, FileStore.
FileStore.
Note that when "loading" an existing file-store, you can't actually change the ID. The optional `expected_id` kwarg tells FileStore.
Also note that previously you could sort of "override" the copies value, but now this can only be provided in FileStore.create(). FileStore.
* Added the low-level `_create_
_create_
It isn't indented as part of the public API just yet, but I think it's a big step forward in terms of the file-store layout initialization being closer to atomic. It's a lot more robust than it was. It creates the layout in temporary directory .dmedia-<STORE_ID>, does everything relative to an open directory descriptor (hooray for Python 3.3), and then at the very last minute renames:
.dmedia-
* FileStore instances (including TempFileStore) now all have a FileStore.doc attribute, which is a Python dict and is the exact Dmedia V1 scheme compliant doc ready to save into CouchDB. This doc is dumped to filestore.json by FileStore.create(), and then is loaded whenever you load an existing file-store with FileStore.
* Moved all upgrade/migration functionality out of filestore and into the new filestore.migration module; this is much more robust than before and has extensive unit tests
* A bit of sundry cleanup and modernization, of the sort I just can't turn down during a refactor-fest such as this
James Raymond (jamesmr) : | # |
Preview Diff
1 | === modified file 'benchmark-protocol.py' |
2 | --- benchmark-protocol.py 2013-01-13 01:58:52 +0000 |
3 | +++ benchmark-protocol.py 2013-05-15 00:48:33 +0000 |
4 | @@ -3,54 +3,35 @@ |
5 | import timeit |
6 | |
7 | setup = """ |
8 | -from base64 import b32encode, b64encode |
9 | -from _filestore import fast_b32encode |
10 | - |
11 | -from filestore import check_id, B32ALPHABET |
12 | -from filestore.protocols import Protocol, OldProtocol, MiB |
13 | - |
14 | -digest = b'N' * 30 |
15 | -_id = 'GTUK6VPCITPAUSG3FLVNYK7IXEPXWDKGQ4T2X4IUOYXDE232' |
16 | - |
17 | -new = Protocol(8 * MiB, 240) |
18 | -old = OldProtocol(8 * MiB, 240) |
19 | - |
20 | -leaf_index = 17 |
21 | -leaf_data = b'S' * 1776 |
22 | - |
23 | -file_size = 31415 |
24 | -leaf_hashes = b'D' * 30 |
25 | +from base64 import b64encode |
26 | +from dbase32 import db32enc |
27 | +from filestore.protocols import VERSION0, VERSION1, make_key |
28 | + |
29 | +leaf_count = 32 # 256 MiB file, probably a good middle ground |
30 | +leaf_hashes = b'B' * (VERSION1.digest_bytes * leaf_count) |
31 | +file_size = VERSION1.leaf_size * leaf_count |
32 | +key_file_size = make_key(file_size) |
33 | """ |
34 | |
35 | -N = 50 * 1000 |
36 | |
37 | -def run(statement): |
38 | +def run(statement, K=150): |
39 | t = timeit.Timer(statement, setup) |
40 | - elapsed = t.timeit(N) |
41 | - rate = int(N / elapsed) |
42 | + n = K * 1000 |
43 | + elapsed = t.timeit(n) |
44 | + rate = int(n / elapsed) |
45 | print('{:>10,}: {}'.format(rate, statement)) |
46 | |
47 | |
48 | print('Executions per second:') |
49 | |
50 | -run('b32encode(digest)') |
51 | -run('fast_b32encode(digest)') |
52 | -run("fast_b32encode(digest).decode('utf-8')") |
53 | - |
54 | -run('b64encode(digest)') |
55 | -run("b64encode(digest).decode('utf-8')") |
56 | -run("b64encode(digest).decode('ascii')") |
57 | - |
58 | -run('check_id(_id)') |
59 | -run('set(_id).issubset(B32ALPHABET)') |
60 | -run('B32ALPHABET.issuperset(_id)') |
61 | - |
62 | -run("old._hash_leaf(leaf_index, leaf_data, b'')") |
63 | -run("new._hash_leaf(leaf_index, leaf_data, b'')") |
64 | - |
65 | -run('old._hash_root(file_size, leaf_hashes)') |
66 | -run('new._hash_root(file_size, leaf_hashes)') |
67 | -run('new.hash_root(file_size, leaf_hashes)') |
68 | - |
69 | -run('str(4294967295)') |
70 | -run("str(4294967295).encode('utf-8')") |
71 | +run('VERSION0._hash_root(key_file_size, leaf_hashes)') |
72 | +run('VERSION1._hash_root(key_file_size, leaf_hashes)') |
73 | +print('') |
74 | +run('VERSION0.hash_root(file_size, leaf_hashes)') |
75 | +run('VERSION1.hash_root(file_size, leaf_hashes)') |
76 | +print('') |
77 | +run('b64encode(VERSION0.hash_root(file_size, leaf_hashes))') |
78 | +run(' db32enc(VERSION1.hash_root(file_size, leaf_hashes))') |
79 | + |
80 | + |
81 | + |
82 | |
83 | === modified file 'debian/control' |
84 | --- debian/control 2013-02-21 05:20:15 +0000 |
85 | +++ debian/control 2013-05-15 00:48:33 +0000 |
86 | @@ -6,7 +6,7 @@ |
87 | python3-all-dev (>= 3.3), |
88 | python3-sphinx, |
89 | python3-skein (>= 0.7.1), |
90 | - python3-dbase32 (>= 0.4), |
91 | + python3-dbase32 (>= 0.6), |
92 | Standards-Version: 3.9.3 |
93 | X-Python3-Version: >= 3.3 |
94 | Homepage: https://launchpad.net/filestore |
95 | @@ -15,7 +15,7 @@ |
96 | Architecture: any |
97 | Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends}, |
98 | python3-skein (>= 0.7.1), |
99 | - python3-dbase32 (>= 0.4), |
100 | + python3-dbase32 (>= 0.6), |
101 | Description: Dmedia hashing protocol and file layout |
102 | This is the heart of Dmedia. It has been split out of Dmedia to make it easier |
103 | to review, and in hopes that other apps might use the hashing protocol and |
104 | |
105 | === modified file 'filestore/__init__.py' |
106 | --- filestore/__init__.py 2013-04-28 20:21:18 +0000 |
107 | +++ filestore/__init__.py 2013-05-15 00:48:33 +0000 |
108 | @@ -76,17 +76,17 @@ |
109 | from os import path |
110 | import io |
111 | import stat |
112 | -from base64 import b64encode |
113 | +import fcntl |
114 | import json |
115 | from threading import Thread |
116 | from queue import Queue |
117 | from collections import namedtuple |
118 | import logging |
119 | - |
120 | -from dbase32 import db32enc, isdb32, random_id, DB32ALPHABET |
121 | -from dbase32.rfc3548 import isb32, b32enc |
122 | - |
123 | -from .protocols import TYPE_ERROR, VERSION0 |
124 | +import time |
125 | + |
126 | +from dbase32 import db32enc, isdb32, random_id, DB32ALPHABET, RANDOM_B32LEN |
127 | + |
128 | +from .protocols import TYPE_ERROR |
129 | from .protocols import VERSION1 as PROTOCOL |
130 | |
131 | try: |
132 | @@ -111,16 +111,14 @@ |
133 | # Handy constants for file layout: |
134 | DOTNAME = '.dmedia' |
135 | |
136 | -# V0 Base32 subdir names: |
137 | -B32ALPHABET = '234567ABCDEFGHIJKLMNOPQRSTUVWXYZ' |
138 | -B32NAMES = tuple(a + b for a in B32ALPHABET for b in B32ALPHABET) |
139 | +# Max durability we allow a local, physical location to count for. This is |
140 | +# conservatively at 2 so that a single physical location is never enough (as |
141 | +# Dmedia will try to maintain at least 3 copies). |
142 | +MAX_COPIES = 2 |
143 | |
144 | # V1 Dbase32 subdir names: |
145 | DB32NAMES = tuple(a + b for a in DB32ALPHABET for b in DB32ALPHABET) |
146 | |
147 | -# The presense of any of these means a V0, Base32 file layout: |
148 | -NAMES_DIFF = tuple(sorted(set(B32NAMES) - set(DB32NAMES))) |
149 | - |
150 | # Named tuples: |
151 | ContentHash = namedtuple('ContentHash', 'id file_size leaf_hashes') |
152 | File = namedtuple('File', 'name size mtime') |
153 | @@ -790,22 +788,154 @@ |
154 | ############################################### |
155 | # Misc functions used by the `FileStore` class: |
156 | |
157 | -def ensuredir(d): |
158 | - """ |
159 | - Ensure that *d* exists, is a directory, and is not a symlink. |
160 | - """ |
161 | + |
162 | +class Opener: |
163 | + __slots__ = ('dir_fd',) |
164 | + |
165 | + def __init__(self, dir_fd): |
166 | + self.dir_fd = dir_fd |
167 | + |
168 | + def __call__(self, name, flags): |
169 | + return os.open(name, flags, dir_fd=self.dir_fd) |
170 | + |
171 | + |
172 | +def _mkdir(name, dir_fd): |
173 | + os.mkdir(name, dir_fd=dir_fd) |
174 | + os.chmod(name, 0o777, dir_fd=dir_fd) |
175 | + |
176 | + |
177 | +def _rename(src, dst, dir_fd): |
178 | + os.rename(src, dst, src_dir_fd=dir_fd, dst_dir_fd=dir_fd) |
179 | + |
180 | + |
181 | +def _dumps(obj): |
182 | + return json.dumps(obj, |
183 | + ensure_ascii=False, |
184 | + sort_keys=True, |
185 | + separators=(',',': '), |
186 | + indent=4, |
187 | + ) |
188 | + |
189 | + |
190 | +def _write(name, dir_fd, text): |
191 | + fp = open(name, 'x', opener=Opener(dir_fd)) |
192 | + fp.write(text) |
193 | + fp.flush() |
194 | + os.fsync(fp.fileno()) |
195 | + os.chmod(fp.fileno(), 0o444) |
196 | + fp.close() |
197 | + |
198 | + |
199 | +def _create_doc(store_id, copies, **kw): |
200 | + if not isinstance(store_id, str): |
201 | + raise TypeError( |
202 | + TYPE_ERROR.format('store_id', str, type(store_id), store_id) |
203 | + ) |
204 | + if len(store_id) != RANDOM_B32LEN: |
205 | + raise ValueError('len(store_id) is {}, need {}: {!r}'.format( |
206 | + len(store_id), RANDOM_B32LEN, store_id) |
207 | + ) |
208 | + if not isdb32(store_id): |
209 | + raise ValueError('store_id: invalid Dbase32 ID: {!r}'.format(store_id)) |
210 | + if not isinstance(copies, int): |
211 | + raise TypeError( |
212 | + TYPE_ERROR.format('copies', int, type(copies), copies) |
213 | + ) |
214 | + if not (0 <= copies <= MAX_COPIES): |
215 | + raise ValueError( |
216 | + 'copies is {}, need 0 <= copies <= {}'.format(copies, MAX_COPIES) |
217 | + ) |
218 | + doc = { |
219 | + '_id': store_id, |
220 | + 'time': time.time(), |
221 | + 'type': 'dmedia/store', |
222 | + 'plugin': 'filestore', |
223 | + 'copies': copies, |
224 | + } |
225 | + for (key, value) in kw.items(): |
226 | + doc.setdefault(key, value) |
227 | + return doc |
228 | + |
229 | + |
230 | +def check_doc(doc, expected_id=None): |
231 | + if not isinstance(doc, dict): |
232 | + raise TypeError( |
233 | + TYPE_ERROR.format('doc', dict, type(doc), doc) |
234 | + ) |
235 | + _id = doc['_id'] |
236 | + if not isinstance(_id, str): |
237 | + raise TypeError( |
238 | + TYPE_ERROR.format("doc['_id']", str, type(_id), _id) |
239 | + ) |
240 | + if len(_id) != RANDOM_B32LEN: |
241 | + raise ValueError("len(doc['_id']) is {}, need {}: {!r}".format( |
242 | + len(_id), RANDOM_B32LEN, _id) |
243 | + ) |
244 | + if not isdb32(_id): |
245 | + raise ValueError( |
246 | + "doc['_id'] is not a valid Dbase32 ID: {!r}".format(_id) |
247 | + ) |
248 | + if not (expected_id is None or expected_id == _id): |
249 | + raise ValueError( |
250 | + "doc['_id']: expected {!r}; got {!r}".format(expected_id, _id) |
251 | + ) |
252 | + copies = doc['copies'] |
253 | + if not isinstance(copies, int): |
254 | + raise TypeError( |
255 | + TYPE_ERROR.format("doc['copies']", int, type(copies), copies) |
256 | + ) |
257 | + if not (0 <= copies <= MAX_COPIES): |
258 | + raise ValueError( |
259 | + "doc['copies'] is {}, need 0 <= doc['copies'] <= {}".format( |
260 | + copies, MAX_COPIES) |
261 | + ) |
262 | + _type = doc['type'] |
263 | + if _type != 'dmedia/store': |
264 | + raise ValueError( |
265 | + "doc['type'] is {!r}, must be 'dmedia/store'".format(_type) |
266 | + ) |
267 | + return doc |
268 | + |
269 | + |
270 | +def _check_parentdir(parentdir): |
271 | + parentdir = check_path(parentdir) |
272 | + if DOTNAME in parentdir.split(os.sep): |
273 | + raise NestedParent(parentdir, DOTNAME) |
274 | + if not path.isdir(parentdir): |
275 | + raise ValueError('parentdir not a directory: {!r}'.format(parentdir)) |
276 | + return parentdir |
277 | + |
278 | + |
279 | +def _create_filestore(parentdir, store_id, copies, **kw): |
280 | + """ |
281 | + Low level function for creating a Dbase32, V1+ FileStore layout. |
282 | + |
283 | + Please use `FileStore.create()` instead of this function |
284 | + |
285 | + This is not regarded as part of the public FileStore API and may break |
286 | + without warning. |
287 | + """ |
288 | + dir_fd = os.open(parentdir, os.O_DIRECTORY) |
289 | try: |
290 | - os.mkdir(d) |
291 | - return True |
292 | - except OSError: |
293 | - mode = os.lstat(d).st_mode |
294 | - if stat.S_ISLNK(mode): |
295 | - raise ValueError( |
296 | - '{!r} is symlink to {!r}'.format(d, os.readlink(d)) |
297 | - ) |
298 | - if not stat.S_ISDIR(mode): |
299 | - raise ValueError('not a directory: {!r}'.format(d)) |
300 | - return False |
301 | + doc = _create_doc(store_id, copies, **kw) |
302 | + tmpname = '-'.join([DOTNAME, store_id]) |
303 | + |
304 | + # Create all the directories: |
305 | + _mkdir(tmpname, dir_fd) |
306 | + for name in ('files', 'partial', 'corrupt', 'tmp'): |
307 | + _mkdir(path.join(tmpname, name), dir_fd) |
308 | + for name in DB32NAMES: |
309 | + _mkdir(path.join(tmpname, 'files', name), dir_fd) |
310 | + |
311 | + # Write the two files: |
312 | + _write(path.join(tmpname, 'README.txt'), dir_fd, README) |
313 | + _write(path.join(tmpname, 'filestore.json'), dir_fd, _dumps(doc)) |
314 | + |
315 | + # Rename to the final directory name, return doc: |
316 | + _rename(tmpname, DOTNAME, dir_fd) |
317 | + return doc |
318 | + finally: |
319 | + os.close(dir_fd) |
320 | |
321 | |
322 | def statvfs(pathname): |
323 | @@ -830,98 +960,6 @@ |
324 | return StatVFS(size, used, avail, readonly, st.f_frsize) |
325 | |
326 | |
327 | -def is_v0_files(files): |
328 | - for name in NAMES_DIFF: |
329 | - if path.isdir(path.join(files, name)): |
330 | - return True |
331 | - return False |
332 | - |
333 | - |
334 | -def dumps(obj): |
335 | - return json.dumps(obj, |
336 | - ensure_ascii=False, |
337 | - sort_keys=True, |
338 | - separators=(',',': '), |
339 | - indent=4, |
340 | - ) |
341 | - |
342 | - |
343 | -def migrate_store_doc(basedir): |
344 | - store = path.join(basedir, 'store.json') |
345 | - store0 = path.join(basedir, 'store0.json') |
346 | - try: |
347 | - doc = json.load(open(store, 'r')) |
348 | - except FileNotFoundError: |
349 | - log.error("'store.json' does not exist in %r", basedir) |
350 | - return False |
351 | - |
352 | - if path.exists(store0): |
353 | - raise Exception("'store0.json' already exists in {!r}".format(basedir)) |
354 | - |
355 | - log.warning("Moving V0 'store.json' to 'store0.json' in %r", basedir) |
356 | - os.rename(store, store0) |
357 | - |
358 | - from dbase32.rfc3548 import b32dec |
359 | - assert doc.get('migrated') is None |
360 | - old_id = doc['_id'] |
361 | - new_id = db32enc(b32dec(old_id)) |
362 | - log.warning('Migrating FileStore ID from %r to %r in %r', |
363 | - old_id, new_id, basedir) |
364 | - doc['_id'] = new_id |
365 | - doc['migrated'] = True |
366 | - text = dumps(doc) |
367 | - tmp = path.join(basedir, 'store.json.' + random_id()) |
368 | - fp = open(tmp, 'x') |
369 | - fp.write(text) |
370 | - fp.flush() |
371 | - os.fsync(fp.fileno()) |
372 | - os.chmod(fp.fileno(), 0o444) |
373 | - fp.close() |
374 | - os.rename(tmp, store) |
375 | - return True |
376 | - |
377 | - |
378 | -################################################### |
379 | -# The `Migration` class use for V0 => V1 migration: |
380 | - |
381 | -class Migration: |
382 | - def __init__(self, fs): |
383 | - assert isinstance(fs, FileStore) |
384 | - self.fs = fs |
385 | - self.files0 = fs.join('files0') |
386 | - assert path.isdir(self.files0) |
387 | - |
388 | - def __iter__(self): |
389 | - for prefix in B32NAMES: |
390 | - subdir = path.join(self.files0, prefix) |
391 | - for name in sorted(os.listdir(subdir)): |
392 | - src = path.join(subdir, name) |
393 | - v0_id = prefix + name |
394 | - assert path.isfile(src) or path.islink(src) |
395 | - assert isb32(v0_id) and len(v0_id) == 48 |
396 | - |
397 | - if path.islink(src): |
398 | - log.info('Reading symlink %r', src) |
399 | - yield (v0_id, os.readlink(src), None) |
400 | - else: |
401 | - src_fp = open(src, 'rb') |
402 | - h0 = Hasher(protocol=VERSION0, enc=b32enc) |
403 | - h1 = Hasher() |
404 | - for leaf in reader_iter(src_fp): |
405 | - h0.hash_leaf(leaf) |
406 | - h1.hash_leaf(leaf) |
407 | - ch0 = h0.content_hash() |
408 | - ch1 = h1.content_hash() |
409 | - assert isdb32(ch1.id) |
410 | - if ch0.id != v0_id: |
411 | - yield (v0_id, None, None) |
412 | - else: |
413 | - dst = self.fs.path(ch1.id) |
414 | - log.info('Moving %r to %r', src, dst) |
415 | - os.rename(src, dst) |
416 | - os.symlink(ch1.id, src) |
417 | - yield (v0_id, ch1.id, ch1) |
418 | - |
419 | |
420 | ######################## |
421 | # The `FileStore` class: |
422 | @@ -947,50 +985,54 @@ |
423 | us to provide even stronger data durability guarantees |
424 | """ |
425 | |
426 | - def __init__(self, parentdir, _id=None, copies=0): |
427 | + dir_fd = None |
428 | + |
429 | + @classmethod |
430 | + def create(cls, parentdir, store_id=None, copies=1, **kw): |
431 | + parentdir = _check_parentdir(parentdir) |
432 | + dotdir = path.join(parentdir, DOTNAME) |
433 | + if path.exists(dotdir): |
434 | + raise ValueError( |
435 | + 'Cannot create FileStore, dotdir exists: {!r}'.format(dotdir) |
436 | + ) |
437 | + store_id = (random_id() if store_id is None else store_id) |
438 | + _create_filestore(parentdir, store_id, copies, **kw) |
439 | + return cls(parentdir, store_id) |
440 | + |
441 | + def __init__(self, parentdir, expected_id=None): |
442 | """ |
443 | Initialize. |
444 | |
445 | - :param parentdir: directory for file store, eg ``'/home/jderose'`` |
446 | - :param dotname: name of control directory, default is ``'.dmedia'`` |
447 | + :param parentdir: directory for file store, eg ``'/media/MyDrive'`` |
448 | + :param excepted_id: if provided, an exception is raised if the ID loaded |
449 | + from the ``'filestore.json'`` file doesn't match |
450 | """ |
451 | - self.parentdir = check_path(parentdir) |
452 | + self.parentdir = _check_parentdir(parentdir) |
453 | self.basedir = path.join(self.parentdir, DOTNAME) |
454 | + self.dir_fd = os.open(self.basedir, os.O_DIRECTORY | os.O_NOFOLLOW) |
455 | + fcntl.flock(self.dir_fd, fcntl.LOCK_SH | fcntl.LOCK_NB) |
456 | + fp = open('filestore.json', 'r', opener=self) |
457 | + self.doc = check_doc(json.load(fp), expected_id) |
458 | + self.id = self.doc['_id'] |
459 | + self.copies = self.doc['copies'] |
460 | + |
461 | + # FIXME: Probably don't need this once we raname relative to dir_fd: |
462 | self.tmp = path.join(self.basedir, 'tmp') |
463 | - self.id = _id |
464 | - self.copies = copies |
465 | - |
466 | - # Make sure parentdir isn't nested in another FileStore: |
467 | - if DOTNAME in self.parentdir.split(os.sep): |
468 | - raise NestedParent(self.parentdir, DOTNAME) |
469 | - |
470 | - # Make sure parentdir is a directory: |
471 | - if not path.isdir(self.parentdir): |
472 | - raise ValueError('{}.parentdir not a directory: {!r}'.format( |
473 | - self.__class__.__name__, self.parentdir) |
474 | - ) |
475 | - |
476 | - files = self.join('files') |
477 | - files0 = self.join('files0') |
478 | - |
479 | - # If basedir doesn't exist, create it and initialize all dirs in layout: |
480 | - if ensuredir(self.basedir): |
481 | - log.info('Initalizing FileStore in %r', self.basedir) |
482 | - self.init_dirs() |
483 | - elif is_v0_files(files): |
484 | - if path.exists(files0): |
485 | - raise Exception( |
486 | - "'files' is V0 layout but 'files0' exists in {!r}".format(self.basedir) |
487 | - ) |
488 | - log.warning("Moving V0 'files' to 'files0' in %r", self.basedir) |
489 | - os.rename(files, files0) |
490 | - self.init_dirs() |
491 | - migrate_store_doc(self.basedir) |
492 | - |
493 | - self.needs_migration = path.isdir(files0) |
494 | + |
495 | + def __del__(self): |
496 | + if self.dir_fd is not None: |
497 | + os.close(self.dir_fd) |
498 | + |
499 | + def __call__(self, name, flags): |
500 | + """ |
501 | + As a callable, use a `FileStore` as an opener for ``os.open()``. |
502 | + """ |
503 | + return os.open(name, flags, dir_fd=self.dir_fd) |
504 | |
505 | def __repr__(self): |
506 | - return '{}({!r})'.format(self.__class__.__name__, self.parentdir) |
507 | + return '{}({!r}, {!r})'.format( |
508 | + self.__class__.__name__, self.parentdir, self.id |
509 | + ) |
510 | |
511 | def __iter__(self): |
512 | """ |
513 | @@ -1021,26 +1063,39 @@ |
514 | if stat.S_ISREG(st.st_mode) and st.st_size > 0: |
515 | yield Stat(_id, fullname, st.st_size, st.st_mtime) |
516 | |
517 | - def init_dirs(self): |
518 | - """ |
519 | - Creates the needed subdirectories inside the control directory. |
520 | - |
521 | - `FileStore.__init__()` automatically calls this if the control directory |
522 | - does not yet exists. |
523 | - """ |
524 | - os.chmod(self.basedir, 0o777) |
525 | - for name in ('files', 'partial', 'corrupt', 'tmp'): |
526 | - d = path.join(self.basedir, name) |
527 | - ensuredir(d) |
528 | - os.chmod(d, 0o777) |
529 | + def _check_dir(self, name): |
530 | + assert path.normpath(name) == name |
531 | + assert not path.isabs(name) |
532 | + st = os.stat(name, dir_fd=self.dir_fd, follow_symlinks=False) |
533 | + if not stat.S_ISDIR(st.st_mode): |
534 | + raise ValueError( |
535 | + '{!r} not a directory in {!r}'.format(name, self.basedir) |
536 | + ) |
537 | + if stat.S_IMODE(st.st_mode) != 0o777: |
538 | + raise ValueError( |
539 | + 'Directory {!r} mode not 0777 in {!r}'.format(name, self.basedir) |
540 | + ) |
541 | + |
542 | + def _check_file(self, name): |
543 | + assert path.normpath(name) == name |
544 | + assert not path.isabs(name) |
545 | + st = os.stat(name, dir_fd=self.dir_fd, follow_symlinks=False) |
546 | + if not stat.S_ISREG(st.st_mode): |
547 | + raise ValueError( |
548 | + '{!r} not a file in {!r}'.format(name, self.basedir) |
549 | + ) |
550 | + if stat.S_IMODE(st.st_mode) != 0o444: |
551 | + raise ValueError( |
552 | + 'File {!r} mode not 0444 in {!r}'.format(name, self.basedir) |
553 | + ) |
554 | + |
555 | + def check_layout(self): |
556 | + for name in ('files', 'tmp', 'partial', 'corrupt'): |
557 | + self._check_dir(name) |
558 | for name in DB32NAMES: |
559 | - d = path.join(self.basedir, 'files', name) |
560 | - ensuredir(d) |
561 | - os.chmod(d, 0o777) |
562 | - f = path.join(self.basedir, 'README.txt') |
563 | - if not path.exists(f): |
564 | - open(f, 'wt').write(README) |
565 | - os.chmod(f, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) |
566 | + self._check_dir(path.join('files', name)) |
567 | + for name in ('README.txt', 'filestore.json'): |
568 | + self._check_file(name) |
569 | |
570 | def statvfs(self): |
571 | """ |
572 | @@ -1300,6 +1355,7 @@ |
573 | this method rather than letting boto compute the md5 hash itself. |
574 | """ |
575 | import hashlib |
576 | + from base64 import b64encode |
577 | md5 = hashlib.md5() |
578 | for leaf in self.verify_iter2(_id): |
579 | md5.update(leaf.data) |
580 | |
581 | === added file 'filestore/migration.py' |
582 | --- filestore/migration.py 1970-01-01 00:00:00 +0000 |
583 | +++ filestore/migration.py 2013-05-15 00:48:33 +0000 |
584 | @@ -0,0 +1,281 @@ |
585 | +# filestore: Dmedia hashing protocol and file layout |
586 | +# Copyright (C) 2013 Novacut Inc |
587 | +# |
588 | +# This file is part of `filestore`. |
589 | +# |
590 | +# `filestore` is free software: you can redistribute it and/or modify it under |
591 | +# the terms of the GNU Affero General Public License as published by the Free |
592 | +# Software Foundation, either version 3 of the License, or (at your option) any |
593 | +# later version. |
594 | +# |
595 | +# `filestore` is distributed in the hope that it will be useful, but WITHOUT ANY |
596 | +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
597 | +# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
598 | +# details. |
599 | +# |
600 | +# You should have received a copy of the GNU Affero General Public License along |
601 | +# with `filestore`. If not, see <http://www.gnu.org/licenses/>. |
602 | +# |
603 | +# Authors: |
604 | +# Jason Gerard DeRose <jderose@novacut.com> |
605 | + |
606 | +""" |
607 | +Migrate a V0 layout to V1, rehash to map V0 to V1 IDs. |
608 | +""" |
609 | + |
610 | +import os |
611 | +from os import path |
612 | +from copy import deepcopy |
613 | +import json |
614 | +import time |
615 | +import logging |
616 | + |
617 | +import dbase32 |
618 | +from dbase32 import rfc3548 |
619 | + |
620 | +from . import check_doc, reader_iter, Hasher, DB32NAMES, README |
621 | +from . import _check_parentdir, _create_doc, _dumps |
622 | +from .misc import load_data, write_files |
623 | +from .protocols import VERSION0 |
624 | + |
625 | + |
626 | +log = logging.getLogger() |
627 | + |
628 | +# V0 Base32 subdir names: |
629 | +B32ALPHABET = '234567ABCDEFGHIJKLMNOPQRSTUVWXYZ' |
630 | +B32NAMES = tuple(a + b for a in B32ALPHABET for b in B32ALPHABET) |
631 | + |
632 | +# The presense of any of these means a V0, Base32 file layout: |
633 | +NAMES_DIFF = tuple(sorted(set(B32NAMES) - set(DB32NAMES))) |
634 | + |
635 | + |
636 | +def is_v0_files(files): |
637 | + for name in NAMES_DIFF: |
638 | + if path.isdir(path.join(files, name)): |
639 | + return True |
640 | + return False |
641 | + |
642 | + |
643 | +def b32_to_db32(_id): |
644 | + """ |
645 | + Re-encode an ID from Base32 to Dbase32. |
646 | + |
647 | + >>> b32_to_db32('5Q2VGOCRKWGDCSJOHDMXYFCE') |
648 | + 'WJTO9H5KDP965LCHA6FQR857' |
649 | + |
650 | + """ |
651 | + return dbase32.db32enc(rfc3548.b32dec(_id)) |
652 | + |
653 | + |
654 | +def v0_doc_to_v1(old): |
655 | + assert isinstance(old, dict) |
656 | + new = deepcopy(old) |
657 | + new['_id'] = b32_to_db32(old['_id']) |
658 | + new['type'] = 'dmedia/store' |
659 | + new['plugin'] = 'filestore' |
660 | + return check_doc(new) |
661 | + |
662 | + |
663 | +def open_file(fullname): |
664 | + try: |
665 | + return open(fullname, 'r') |
666 | + except FileNotFoundError: |
667 | + pass |
668 | + |
669 | + |
670 | +def load_doc(fullname): |
671 | + try: |
672 | + return json.load(open(fullname, 'r')) |
673 | + except (FileNotFoundError, ValueError): |
674 | + pass |
675 | + |
676 | + |
677 | +class Migration: |
678 | + def __init__(self, parentdir): |
679 | + self.parentdir = _check_parentdir(parentdir) |
680 | + self.basedir = path.join(self.parentdir, '.dmedia') |
681 | + self.files = self.join('files') |
682 | + self.files0 = self.join('files0') |
683 | + self.store = self.join('store.json') |
684 | + self.store0 = self.join('store0.json') |
685 | + self.filestore = self.join('filestore.json') |
686 | + |
687 | + def __iter__(self): |
688 | + if not self.needs_migration(): |
689 | + return |
690 | + for prefix in B32NAMES: |
691 | + subdir = path.join(self.files0, prefix) |
692 | + for name in sorted(os.listdir(subdir)): |
693 | + src = path.join(subdir, name) |
694 | + v0_id = prefix + name |
695 | + assert path.isfile(src) or path.islink(src) |
696 | + assert rfc3548.isb32(v0_id) and len(v0_id) == 48 |
697 | + |
698 | + if path.islink(src): |
699 | + log.info('Reading symlink %r', src) |
700 | + yield (v0_id, os.readlink(src), None) |
701 | + else: |
702 | + src_fp = open(src, 'rb') |
703 | + h0 = Hasher(protocol=VERSION0, enc=rfc3548.b32enc) |
704 | + h1 = Hasher() |
705 | + for leaf in reader_iter(src_fp): |
706 | + h0.hash_leaf(leaf) |
707 | + h1.hash_leaf(leaf) |
708 | + ch0 = h0.content_hash() |
709 | + ch1 = h1.content_hash() |
710 | + assert dbase32.isdb32(ch1.id) |
711 | + if ch0.id != v0_id: |
712 | + yield (v0_id, None, None) |
713 | + else: |
714 | + dst = self.v1_path(ch1.id) |
715 | + log.info('Moving %r to %r', src, dst) |
716 | + os.rename(src, dst) |
717 | + os.symlink(ch1.id, src) |
718 | + yield (v0_id, ch1.id, ch1) |
719 | + |
720 | + def needs_migration(self): |
721 | + if not path.isdir(self.basedir): |
722 | + return False |
723 | + self.write_v1_doc_if_needed() |
724 | + if self.move_files_if_needed(): |
725 | + return True |
726 | + return path.isdir(self.files0) |
727 | + |
728 | + def build_v0_simulation(self): |
729 | + os.mkdir(self.basedir) |
730 | + os.chmod(self.basedir, 0o777) |
731 | + for name in ('partial', 'corrupt', 'tmp'): |
732 | + self.mkdir(name) |
733 | + self.create_v0_files() |
734 | + self.write('README.txt', README) |
735 | + doc = { |
736 | + '_id': rfc3548.random_id(), |
737 | + 'copies': 1, |
738 | + 'plugin': 'filestore', |
739 | + 'time': time.time(), |
740 | + 'type': 'dmedia/store' |
741 | + } |
742 | + self.write('store.json', _dumps(doc)) |
743 | + V0 = load_data('V0') |
744 | + write_files(self.basedir, VERSION0) |
745 | + for (key, _id) in V0['root_hashes'].items(): |
746 | + src = self.join(key) |
747 | + dst = self.path(_id) |
748 | + os.rename(src, dst) |
749 | + os.chmod(dst, 0o444) |
750 | + return doc |
751 | + |
752 | + def join(self, *parts): |
753 | + if not parts: |
754 | + raise ValueError('Cannot have empty parts') |
755 | + return path.join(self.basedir, *parts) |
756 | + |
757 | + def mkdir(self, *parts): |
758 | + fullname = self.join(*parts) |
759 | + os.mkdir(fullname) |
760 | + os.chmod(fullname, 0o777) |
761 | + return fullname |
762 | + |
763 | + def write(self, name, text): |
764 | + fullname = self.join(name) |
765 | + fp = open(fullname, 'x') |
766 | + fp.write(text) |
767 | + fp.flush() |
768 | + os.fsync(fp.fileno()) |
769 | + os.chmod(fp.fileno(), 0o444) |
770 | + fp.close() |
771 | + return fullname |
772 | + |
773 | + def load_v0_doc(self): |
774 | + # Hack: as we switched from 'store.json' to 'filestore.json' after the |
775 | + # initial migration functionality landed in 13.05, the old V0 doc might |
776 | + # be in the 'store0.json' file: |
777 | + fp = open_file(self.store0) or open_file(self.store) |
778 | + if fp is None: |
779 | + return |
780 | + try: |
781 | + return json.load(fp) |
782 | + except ValueError: |
783 | + pass |
784 | + |
785 | + def get_v1_doc(self): |
786 | + # If store0.json is presest, it should always be a V0, Base32 doc: |
787 | + old = load_doc(self.store0) |
788 | + if old is not None: |
789 | + return v0_doc_to_v1(old) |
790 | + |
791 | + # If only store.json is presest, there are a few scenarios: |
792 | + old = load_doc(self.store) |
793 | + if old is not None: |
794 | + old_id = old['_id'] |
795 | + # This scenario is always clear-cut: |
796 | + if not dbase32.isdb32(old_id): |
797 | + return v0_doc_to_v1(old) |
798 | + # Also clear-cut, but 'migration' flag only in 13.05 daily builds: |
799 | + if old.get('migrated') is True or not rfc3548.isb32(old_id): |
800 | + old['type'] = 'dmedia/store' |
801 | + old['plugin'] = 'filestore' |
802 | + old.pop('migrated', None) |
803 | + return check_doc(old) |
804 | + # High chance of being correct, but possible for it to be wrong: |
805 | + return v0_doc_to_v1(old) |
806 | + |
807 | + # Otherwise create a new V1 doc, without any migration: |
808 | + return _create_doc(dbase32.random_id(), 1) |
809 | + |
810 | + def write_v1_doc_if_needed(self): |
811 | + if path.isfile(self.filestore): |
812 | + return False |
813 | + doc = self.get_v1_doc() |
814 | + self.write('filestore.json', _dumps(doc)) |
815 | + assert path.isfile(self.filestore) |
816 | + return True |
817 | + |
818 | + def path(self, _id): |
819 | + assert isinstance(_id, str) |
820 | + assert rfc3548.isb32(_id) or dbase32.isdb32(_id) |
821 | + assert len(_id) == 48 |
822 | + return self.join('files', _id[:2], _id[2:]) |
823 | + |
824 | + def v0_path(self, v0_id): |
825 | + assert isinstance(v0_id, str) |
826 | + assert rfc3548.isb32(v0_id) |
827 | + assert len(v0_id) == 48 |
828 | + return self.join('files0', v0_id[:2], v0_id[2:]) |
829 | + |
830 | + def v1_path(self, v1_id): |
831 | + assert isinstance(v1_id, str) |
832 | + assert dbase32.isdb32(v1_id) |
833 | + assert len(v1_id) == 48 |
834 | + return self.join('files', v1_id[:2], v1_id[2:]) |
835 | + |
836 | + def create_v0_files(self): |
837 | + """ |
838 | + For testing or reversing the migration, create the V0 layout: |
839 | + """ |
840 | + self.mkdir('files') |
841 | + for name in B32NAMES: |
842 | + self.mkdir('files', name) |
843 | + |
844 | + def create_v1_files(self): |
845 | + """ |
846 | + After moving a V0 files to files0, we need to create a correct V1 files. |
847 | + """ |
848 | + self.mkdir('files') |
849 | + for name in DB32NAMES: |
850 | + self.mkdir('files', name) |
851 | + |
852 | + def move_files_if_needed(self): |
853 | + if is_v0_files(self.files): |
854 | + self.move_v0_files_to_files0() |
855 | + return True |
856 | + if not path.isdir(self.files): |
857 | + self.create_v1_files() |
858 | + return False |
859 | + |
860 | + def move_v0_files_to_files0(self): |
861 | + assert is_v0_files(self.files) |
862 | + assert not path.exists(self.files0) |
863 | + log.warning('Moving V0 files/ to files0/ in %r', self.basedir) |
864 | + os.rename(self.files, self.files0) |
865 | + self.create_v1_files() |
866 | |
867 | === modified file 'filestore/misc.py' |
868 | --- filestore/misc.py 2013-04-28 17:02:28 +0000 |
869 | +++ filestore/misc.py 2013-05-15 00:48:33 +0000 |
870 | @@ -30,10 +30,10 @@ |
871 | from hashlib import md5 |
872 | import binascii |
873 | |
874 | -from dbase32 import db32enc |
875 | +from dbase32 import db32enc, random_id |
876 | |
877 | from .protocols import PERS_LEAF, PERS_ROOT, VERSION1 |
878 | -from . import FileStore |
879 | +from . import FileStore, _create_filestore |
880 | |
881 | |
882 | DATADIR = path.join(path.dirname(path.abspath(__file__)), 'data') |
883 | @@ -159,19 +159,27 @@ |
884 | |
885 | For example: |
886 | |
887 | - >>> fs = TempFileStore('hello', 3) |
888 | + >>> fs = TempFileStore('39AY39AY39AY39AY39AY39AY', 2) |
889 | >>> fs.id |
890 | - 'hello' |
891 | + '39AY39AY39AY39AY39AY39AY' |
892 | >>> fs.copies |
893 | - 3 |
894 | + 2 |
895 | |
896 | """ |
897 | - def __init__(self, _id=None, copies=0): |
898 | + |
899 | + @classmethod |
900 | + def create(cls, store_id=None, copies=1, **kw): |
901 | + return cls(store_id, copies, **kw) |
902 | + |
903 | + def __init__(self, store_id=None, copies=1, **kw): |
904 | parentdir = tempfile.mkdtemp(prefix='TempFileStore.') |
905 | - super().__init__(parentdir, _id, copies) |
906 | - assert self.parentdir is parentdir |
907 | + store_id = (random_id() if store_id is None else store_id) |
908 | + _create_filestore(parentdir, store_id, copies, **kw) |
909 | + super().__init__(parentdir, store_id) |
910 | + assert self.parentdir == parentdir |
911 | |
912 | def __del__(self): |
913 | + super().__del__() |
914 | if path.isdir(self.parentdir): |
915 | shutil.rmtree(self.parentdir) |
916 | |
917 | |
918 | === modified file 'filestore/tests/__init__.py' |
919 | --- filestore/tests/__init__.py 2013-04-28 18:11:23 +0000 |
920 | +++ filestore/tests/__init__.py 2013-05-15 00:48:33 +0000 |
921 | @@ -33,6 +33,8 @@ |
922 | import shutil |
923 | import json |
924 | from random import SystemRandom |
925 | +import time |
926 | +from uuid import uuid4 |
927 | |
928 | from _skein import skein512 |
929 | from dbase32 import isdb32, db32enc, random_id |
930 | @@ -45,13 +47,6 @@ |
931 | random = SystemRandom() |
932 | TYPE_ERROR = '{}: need a {!r}; got a {!r}: {!r}' |
933 | |
934 | -B32NAMES = [] |
935 | -for a in filestore.B32ALPHABET: |
936 | - for b in filestore.B32ALPHABET: |
937 | - B32NAMES.append(a + b) |
938 | -B32NAMES.sort() |
939 | -B32NAMES = tuple(B32NAMES) |
940 | - |
941 | |
942 | # Sample file: |
943 | LEAVES = ( |
944 | @@ -99,11 +94,21 @@ |
945 | shutil.rmtree(self.dir) |
946 | self.dir = None |
947 | |
948 | + def chmod(self, mode, *parts): |
949 | + p = self.join(*parts) |
950 | + os.chmod(p, mode) |
951 | + return p |
952 | + |
953 | def mkdir(self, *parts): |
954 | d = self.join(*parts) |
955 | os.mkdir(d) |
956 | return d |
957 | |
958 | + def rmdir(self, *parts): |
959 | + d = self.join(*parts) |
960 | + os.rmdir(d) |
961 | + return d |
962 | + |
963 | def makedirs(self, *parts): |
964 | d = self.join(*parts) |
965 | if not path.exists(d): |
966 | @@ -116,6 +121,17 @@ |
967 | open(f, 'xb').close() |
968 | return f |
969 | |
970 | + def remove(self, *parts): |
971 | + f = self.join(*parts) |
972 | + os.remove(f) |
973 | + return f |
974 | + |
975 | + def symlink(self, target, *parts): |
976 | + link = self.join(*parts) |
977 | + os.symlink(target, link) |
978 | + assert path.islink(link) |
979 | + return link |
980 | + |
981 | def write(self, content, *parts): |
982 | d = self.makedirs(*parts[:-1]) |
983 | f = self.join(*parts) |
984 | @@ -299,27 +315,6 @@ |
985 | self.assertIsInstance(filestore.DOTNAME, str) |
986 | self.assertTrue(filestore.DOTNAME.startswith('.')) |
987 | |
988 | - def test_B32ALPHABET(self): |
989 | - self.assertIsInstance(filestore.B32ALPHABET, str) |
990 | - self.assertEqual(len(filestore.B32ALPHABET), 32) |
991 | - self.assertEqual(len(set(filestore.B32ALPHABET)), 32) |
992 | - self.assertEqual(filestore.B32ALPHABET, |
993 | - ''.join(sorted(filestore.B32ALPHABET)) |
994 | - ) |
995 | - |
996 | - def test_B32NAMES(self): |
997 | - self.assertIsInstance(filestore.B32NAMES, tuple) |
998 | - self.assertEqual(len(filestore.B32NAMES), 1024) |
999 | - self.assertEqual(len(set(filestore.B32NAMES)), 1024) |
1000 | - self.assertEqual(filestore.B32NAMES, |
1001 | - tuple(sorted(filestore.B32NAMES)) |
1002 | - ) |
1003 | - for name in filestore.B32NAMES: |
1004 | - self.assertIsInstance(name, str) |
1005 | - self.assertEqual(len(name), 2) |
1006 | - self.assertTrue(name.isupper() or name.isnumeric()) |
1007 | - self.assertTrue(set(name).issubset(filestore.B32ALPHABET)) |
1008 | - |
1009 | def test_DB32ALPHABET(self): |
1010 | self.assertIsInstance(filestore.DB32ALPHABET, str) |
1011 | self.assertEqual(len(filestore.DB32ALPHABET), 32) |
1012 | @@ -341,20 +336,6 @@ |
1013 | self.assertTrue(name.isupper() or name.isnumeric()) |
1014 | self.assertTrue(set(name).issubset(filestore.DB32ALPHABET)) |
1015 | |
1016 | - def test_NAMES_DIFF(self): |
1017 | - self.assertIsInstance(filestore.NAMES_DIFF, tuple) |
1018 | - self.assertEqual(len(filestore.NAMES_DIFF), 124) |
1019 | - self.assertEqual(len(set(filestore.NAMES_DIFF)), 124) |
1020 | - self.assertEqual(filestore.NAMES_DIFF, |
1021 | - tuple(sorted(filestore.NAMES_DIFF)) |
1022 | - ) |
1023 | - for name in filestore.NAMES_DIFF: |
1024 | - self.assertIsInstance(name, str) |
1025 | - self.assertEqual(len(name), 2) |
1026 | - self.assertTrue(name.isupper() or name.isnumeric()) |
1027 | - self.assertIn(name, filestore.B32NAMES) |
1028 | - self.assertNotIn(name, filestore.DB32NAMES) |
1029 | - |
1030 | |
1031 | class TestFunctions(TestCase): |
1032 | def test_hash_leaf(self): |
1033 | @@ -1053,9 +1034,9 @@ |
1034 | def test_batch_import_iter(self): |
1035 | batch = filestore.Batch(tuple(), 0, 0) |
1036 | tmp1 = TempDir() |
1037 | - fs1 = filestore.FileStore(tmp1.dir) |
1038 | + fs1 = filestore.FileStore.create(tmp1.dir) |
1039 | tmp2 = TempDir() |
1040 | - fs2 = filestore.FileStore(tmp2.dir) |
1041 | + fs2 = filestore.FileStore.create(tmp2.dir) |
1042 | |
1043 | # Test with wrong batch type |
1044 | b = (tuple(), 0, 0) |
1045 | @@ -1133,9 +1114,9 @@ |
1046 | cb = Callback() |
1047 | batch = filestore.Batch(tuple(), 0, 0) |
1048 | tmp1 = TempDir() |
1049 | - fs1 = filestore.FileStore(tmp1.dir) |
1050 | + fs1 = filestore.FileStore.create(tmp1.dir) |
1051 | tmp2 = TempDir() |
1052 | - fs2 = filestore.FileStore(tmp2.dir) |
1053 | + fs2 = filestore.FileStore.create(tmp2.dir) |
1054 | self.assertEqual( |
1055 | list(filestore.batch_import_iter(batch, fs1, fs2, callback=cb)), |
1056 | [] |
1057 | @@ -1228,49 +1209,577 @@ |
1058 | self.assertIsInstance(ch, filestore.ContentHash) |
1059 | self.assertEqual(ch.id, vectors['root_hashes'][name]) |
1060 | |
1061 | - def test_ensuredir(self): |
1062 | - f = filestore.ensuredir |
1063 | - tmp = TempDir() |
1064 | - |
1065 | - # Test that os.makedirs() is not used: |
1066 | - d = tmp.join('foo', 'bar') |
1067 | + def test_create_doc(self): |
1068 | + # Bad `store_id` type: |
1069 | + bad_id = random_id().encode('utf-8') |
1070 | + with self.assertRaises(TypeError) as cm: |
1071 | + filestore._create_doc(bad_id, 1) |
1072 | + self.assertEqual( |
1073 | + str(cm.exception), |
1074 | + TYPE_ERROR.format('store_id', str, bytes, bad_id) |
1075 | + ) |
1076 | + |
1077 | + # Bad `store_id` length: |
1078 | + bad_id = random_id(10) |
1079 | + with self.assertRaises(ValueError) as cm: |
1080 | + filestore._create_doc(bad_id, 1) |
1081 | + self.assertEqual( |
1082 | + str(cm.exception), |
1083 | + 'len(store_id) is 16, need 24: {!r}'.format(bad_id) |
1084 | + ) |
1085 | + |
1086 | + # `store_id` is not valid Dbase32 ID: |
1087 | + bad_id = 'Z' * 24 |
1088 | + with self.assertRaises(ValueError) as cm: |
1089 | + filestore._create_doc(bad_id, 1) |
1090 | + self.assertEqual( |
1091 | + str(cm.exception), |
1092 | + "store_id: invalid Dbase32 ID: 'ZZZZZZZZZZZZZZZZZZZZZZZZ'" |
1093 | + ) |
1094 | + |
1095 | + # Bad `copies` type: |
1096 | + good_id = random_id() |
1097 | + with self.assertRaises(TypeError) as cm: |
1098 | + filestore._create_doc(good_id, 1.0) |
1099 | + self.assertEqual( |
1100 | + str(cm.exception), |
1101 | + TYPE_ERROR.format('copies', int, float, 1.0) |
1102 | + ) |
1103 | + with self.assertRaises(TypeError) as cm: |
1104 | + filestore._create_doc(good_id, '1') |
1105 | + self.assertEqual( |
1106 | + str(cm.exception), |
1107 | + TYPE_ERROR.format('copies', int, str, '1') |
1108 | + ) |
1109 | + |
1110 | + # copies < 0: |
1111 | + with self.assertRaises(ValueError) as cm: |
1112 | + filestore._create_doc(good_id, -1) |
1113 | + self.assertEqual( |
1114 | + str(cm.exception), |
1115 | + 'copies is -1, need 0 <= copies <= 2' |
1116 | + ) |
1117 | + |
1118 | + # copies > 2: |
1119 | + with self.assertRaises(ValueError) as cm: |
1120 | + filestore._create_doc(good_id, 3) |
1121 | + self.assertEqual( |
1122 | + str(cm.exception), |
1123 | + 'copies is 3, need 0 <= copies <= 2' |
1124 | + ) |
1125 | + |
1126 | + # Test with all possible good values for `copies`: |
1127 | + for copies in range(3): |
1128 | + store_id = random_id() |
1129 | + ts_before = time.time() |
1130 | + doc = filestore._create_doc(store_id, copies) |
1131 | + ts_after = time.time() |
1132 | + self.assertIsInstance(doc, dict) |
1133 | + ts = doc['time'] |
1134 | + self.assertIsInstance(ts, float) |
1135 | + self.assertTrue(ts_before <= ts <= ts_after) |
1136 | + self.assertEqual(doc, { |
1137 | + '_id': store_id, |
1138 | + 'time': ts, |
1139 | + 'type': 'dmedia/store', |
1140 | + 'plugin': 'filestore', |
1141 | + 'copies': copies, |
1142 | + }) |
1143 | + |
1144 | + # Test with extra kwargs: |
1145 | + for copies in range(3): |
1146 | + store_id = random_id() |
1147 | + uuid = str(uuid4()) |
1148 | + label = 'Dmedia' + random_id(5).lower() |
1149 | + kw = {'uuid': uuid, 'label': label} |
1150 | + ts_before = time.time() |
1151 | + doc = filestore._create_doc(store_id, copies, **kw) |
1152 | + ts_after = time.time() |
1153 | + self.assertIsInstance(doc, dict) |
1154 | + ts = doc['time'] |
1155 | + self.assertIsInstance(ts, float) |
1156 | + self.assertTrue(ts_before <= ts <= ts_after) |
1157 | + self.assertEqual(doc, { |
1158 | + '_id': store_id, |
1159 | + 'time': ts, |
1160 | + 'type': 'dmedia/store', |
1161 | + 'plugin': 'filestore', |
1162 | + 'copies': copies, |
1163 | + 'uuid': uuid, |
1164 | + 'label': label, |
1165 | + }) |
1166 | + |
1167 | + def test_check_doc(self): |
1168 | + _id = random_id() |
1169 | + doc = {'_id': _id, 'copies': 1, 'type': 'dmedia/store'} |
1170 | + self.assertIs(filestore.check_doc(doc), doc) |
1171 | + self.assertEqual( |
1172 | + filestore.check_doc(doc), |
1173 | + {'_id': _id, 'copies': 1, 'type': 'dmedia/store'} |
1174 | + ) |
1175 | + |
1176 | + # expected_id is provided and doesn't match doc['_id']: |
1177 | + expected_id = random_id() |
1178 | + with self.assertRaises(ValueError) as cm: |
1179 | + filestore.check_doc(doc, expected_id=expected_id) |
1180 | + self.assertEqual( |
1181 | + str(cm.exception), |
1182 | + "doc['_id']: expected {!r}; got {!r}".format(expected_id, _id) |
1183 | + ) |
1184 | + |
1185 | + # Bad doc type: |
1186 | + doc_s = json.dumps(doc) |
1187 | + with self.assertRaises(TypeError) as cm: |
1188 | + filestore.check_doc(doc_s) |
1189 | + self.assertEqual( |
1190 | + str(cm.exception), |
1191 | + TYPE_ERROR.format('doc', dict, str, doc_s) |
1192 | + ) |
1193 | + |
1194 | + # Bad doc['_id'] type: |
1195 | + bad_id = _id.encode('utf-8') |
1196 | + bad = {'_id': bad_id, 'copies': 1, 'type': 'dmedia/store'} |
1197 | + with self.assertRaises(TypeError) as cm: |
1198 | + filestore.check_doc(bad) |
1199 | + self.assertEqual( |
1200 | + str(cm.exception), |
1201 | + TYPE_ERROR.format("doc['_id']", str, bytes, bad_id) |
1202 | + ) |
1203 | + |
1204 | + # Bad doc['_id'] value: |
1205 | + bad_id = random_id(10) |
1206 | + bad = {'_id': bad_id, 'copies': 1, 'type': 'dmedia/store'} |
1207 | + with self.assertRaises(ValueError) as cm: |
1208 | + filestore.check_doc(bad) |
1209 | + self.assertEqual( |
1210 | + str(cm.exception), |
1211 | + "len(doc['_id']) is 16, need 24: {!r}".format(bad_id) |
1212 | + ) |
1213 | + bad_id = 'Z' * 24 |
1214 | + bad = {'_id': bad_id, 'copies': 1, 'type': 'dmedia/store'} |
1215 | + with self.assertRaises(ValueError) as cm: |
1216 | + filestore.check_doc(bad) |
1217 | + self.assertEqual( |
1218 | + str(cm.exception), |
1219 | + "doc['_id'] is not a valid Dbase32 ID: {!r}".format(bad_id) |
1220 | + ) |
1221 | + |
1222 | + # Bad doc['copies'] type: |
1223 | + bad = {'_id': _id, 'copies': 1.5, 'type': 'dmedia/store'} |
1224 | + with self.assertRaises(TypeError) as cm: |
1225 | + filestore.check_doc(bad) |
1226 | + self.assertEqual( |
1227 | + str(cm.exception), |
1228 | + TYPE_ERROR.format("doc['copies']", int, float, 1.5) |
1229 | + ) |
1230 | + |
1231 | + # Bad doc['copies'] value: |
1232 | + bad = {'_id': _id, 'copies': -1, 'type': 'dmedia/store'} |
1233 | + with self.assertRaises(ValueError) as cm: |
1234 | + filestore.check_doc(bad) |
1235 | + self.assertEqual( |
1236 | + str(cm.exception), |
1237 | + "doc['copies'] is -1, need 0 <= doc['copies'] <= 2" |
1238 | + ) |
1239 | + bad = {'_id': _id, 'copies': 3, 'type': 'dmedia/store'} |
1240 | + with self.assertRaises(ValueError) as cm: |
1241 | + filestore.check_doc(bad) |
1242 | + self.assertEqual( |
1243 | + str(cm.exception), |
1244 | + "doc['copies'] is 3, need 0 <= doc['copies'] <= 2" |
1245 | + ) |
1246 | + |
1247 | + # Bad doc['type'] value: |
1248 | + bad = {'_id': _id, 'copies': 1, 'type': 'dmedia/file'} |
1249 | + with self.assertRaises(ValueError) as cm: |
1250 | + filestore.check_doc(bad) |
1251 | + self.assertEqual( |
1252 | + str(cm.exception), |
1253 | + "doc['type'] is 'dmedia/file', must be 'dmedia/store'" |
1254 | + ) |
1255 | + |
1256 | + def test_check_parentdir(self): |
1257 | + tmp = TempDir() |
1258 | + |
1259 | + # Test with relative path: |
1260 | + with self.assertRaises(filestore.PathError) as cm: |
1261 | + filestore._check_parentdir('foo/bar') |
1262 | + self.assertEqual(cm.exception.untrusted, 'foo/bar') |
1263 | + self.assertEqual(cm.exception.abspath, path.abspath('foo/bar')) |
1264 | + self.assertEqual( |
1265 | + str(cm.exception), |
1266 | + '{!r} resolves to {!r}'.format('foo/bar', path.abspath('foo/bar')) |
1267 | + ) |
1268 | + |
1269 | + # Test with path traversal: |
1270 | + with self.assertRaises(filestore.PathError) as cm: |
1271 | + filestore._check_parentdir('/foo/bar/../../root') |
1272 | + self.assertEqual(cm.exception.untrusted, '/foo/bar/../../root') |
1273 | + self.assertEqual(cm.exception.abspath, '/root') |
1274 | + self.assertEqual( |
1275 | + str(cm.exception), |
1276 | + '{!r} resolves to {!r}'.format('/foo/bar/../../root', '/root') |
1277 | + ) |
1278 | + |
1279 | + # Nested parentdir: |
1280 | + tmp.mkdir('.dmedia') |
1281 | + parentdir = tmp.mkdir('.dmedia', random_id().lower()) |
1282 | + with self.assertRaises(filestore.NestedParent) as cm: |
1283 | + filestore._check_parentdir(parentdir) |
1284 | + self.assertEqual(cm.exception.parentdir, parentdir) |
1285 | + self.assertEqual(cm.exception.dotname, '.dmedia') |
1286 | + |
1287 | + # parentdir does not exist: |
1288 | + parentdir = tmp.join(random_id().lower()) |
1289 | + with self.assertRaises(ValueError) as cm: |
1290 | + filestore._check_parentdir(parentdir) |
1291 | + self.assertEqual(str(cm.exception), |
1292 | + 'parentdir not a directory: {!r}'.format(parentdir) |
1293 | + ) |
1294 | + |
1295 | + # parentdir is a file: |
1296 | + parentdir = tmp.touch(random_id().lower()) |
1297 | + with self.assertRaises(ValueError) as cm: |
1298 | + filestore._check_parentdir(parentdir) |
1299 | + self.assertEqual(str(cm.exception), |
1300 | + 'parentdir not a directory: {!r}'.format(parentdir) |
1301 | + ) |
1302 | + |
1303 | + # When parentdir is good |
1304 | + parentdir = tmp.mkdir(random_id().lower()) |
1305 | + self.assertEqual(filestore._check_parentdir(parentdir), parentdir) |
1306 | + |
1307 | + def check_directory(self, fullname): |
1308 | + self.assertEqual(path.abspath(fullname), fullname) |
1309 | + st = os.stat(fullname, follow_symlinks=False) |
1310 | + self.assertTrue(stat.S_ISDIR(st.st_mode), fullname) |
1311 | + self.assertEqual(stat.S_IMODE(st.st_mode), 0o777, fullname) |
1312 | + |
1313 | + def check_file(self, fullname, text): |
1314 | + self.assertEqual(path.abspath(fullname), fullname) |
1315 | + st = os.stat(fullname, follow_symlinks=False) |
1316 | + self.assertTrue(stat.S_ISREG(st.st_mode), fullname) |
1317 | + self.assertEqual(stat.S_IMODE(st.st_mode), 0o444, fullname) |
1318 | + self.assertEqual(open(fullname, 'r').read(), text) |
1319 | + |
1320 | + def test_create_filestore(self): |
1321 | + tmp = TempDir() |
1322 | + store_id = random_id() |
1323 | + |
1324 | + doc = filestore._create_filestore(tmp.dir, store_id, 1) |
1325 | + self.assertIsInstance(doc, dict) |
1326 | + ts = doc['time'] |
1327 | + self.assertIsInstance(ts, float) |
1328 | + self.assertEqual(doc, { |
1329 | + '_id': store_id, |
1330 | + 'time': ts, |
1331 | + 'type': 'dmedia/store', |
1332 | + 'plugin': 'filestore', |
1333 | + 'copies': 1, |
1334 | + }) |
1335 | + |
1336 | + # Check the directories: |
1337 | + self.check_directory(tmp.join('.dmedia')) |
1338 | + for name in ('files', 'tmp', 'partial', 'corrupt'): |
1339 | + self.check_directory(tmp.join('.dmedia', name)) |
1340 | + for name in filestore.DB32NAMES: |
1341 | + self.check_directory(tmp.join('.dmedia', 'files', name)) |
1342 | + |
1343 | + # Check the files: |
1344 | + self.check_file(tmp.join('.dmedia', 'README.txt'), filestore.README) |
1345 | + self.check_file(tmp.join('.dmedia', 'filestore.json'), filestore._dumps(doc)) |
1346 | + |
1347 | + # Check the directory contents: |
1348 | + self.assertEqual(os.listdir(tmp.dir), ['.dmedia']) |
1349 | + self.assertEqual( |
1350 | + sorted(os.listdir(tmp.join('.dmedia'))), |
1351 | + ['README.txt', 'corrupt', 'files', 'filestore.json', 'partial', 'tmp'] |
1352 | + ) |
1353 | + for name in ('tmp', 'partial', 'corrupt'): |
1354 | + self.assertEqual(os.listdir(tmp.join('.dmedia', name)), []) |
1355 | + self.assertEqual( |
1356 | + sorted(os.listdir(tmp.join('.dmedia', 'files'))), |
1357 | + list(filestore.DB32NAMES) |
1358 | + ) |
1359 | + for name in filestore.DB32NAMES: |
1360 | + self.assertEqual(os.listdir(tmp.join('.dmedia', 'files', name)), []) |
1361 | + |
1362 | + # Test when .dmedia already exists in parentdir: |
1363 | with self.assertRaises(OSError) as cm: |
1364 | - f(d) |
1365 | - self.assertEqual( |
1366 | - str(cm.exception), |
1367 | - '[Errno 2] No such file or directory: {!r}'.format(d) |
1368 | - ) |
1369 | - self.assertFalse(path.exists(d)) |
1370 | - self.assertFalse(path.exists(tmp.join('foo'))) |
1371 | - |
1372 | - # Test when dir does not exist |
1373 | - d = tmp.join('foo') |
1374 | - self.assertFalse(path.exists(d)) |
1375 | - self.assertIs(f(d), True) |
1376 | - self.assertTrue(path.isdir(d)) |
1377 | - |
1378 | - # Test when dir exists: |
1379 | - self.assertIs(f(d), False) |
1380 | - self.assertTrue(path.isdir(d)) |
1381 | - |
1382 | - # Test when dir exists and is a file: |
1383 | - d = tmp.touch('bar') |
1384 | - with self.assertRaises(ValueError) as cm: |
1385 | - f(d) |
1386 | - self.assertEqual(str(cm.exception), 'not a directory: {!r}'.format(d)) |
1387 | - |
1388 | - # Test when dir exists and is a symlink to a dir: |
1389 | - target = tmp.makedirs('target') |
1390 | - link = tmp.join('link') |
1391 | - os.symlink(target, link) |
1392 | - self.assertTrue(path.isdir(link)) |
1393 | - self.assertTrue(path.islink(link)) |
1394 | - with self.assertRaises(ValueError) as cm: |
1395 | - f(link) |
1396 | - self.assertEqual( |
1397 | - str(cm.exception), |
1398 | - '{!r} is symlink to {!r}'.format(link, target) |
1399 | - ) |
1400 | + filestore._create_filestore(tmp.dir, store_id, 1) |
1401 | + self.assertEqual(str(cm.exception), |
1402 | + "[Errno 39] Directory not empty: '.dmedia'" |
1403 | + ) |
1404 | + tmpname = '.dmedia-' + store_id |
1405 | + self.assertEqual(set(os.listdir(tmp.dir)), set(['.dmedia', tmpname])) |
1406 | + self.assertTrue(path.isfile(tmp.join(tmpname, 'filestore.json'))) |
1407 | + |
1408 | + def test_check_dir(self): |
1409 | + tmp = TempDir() |
1410 | + fs = filestore.FileStore.create(tmp.dir) |
1411 | + tmp.mkdir('.dmedia', 'foo') |
1412 | + |
1413 | + # Test with a few good directories: |
1414 | + tmp.mkdir('.dmedia', 'good') |
1415 | + tmp.chmod(0o777, '.dmedia', 'good') |
1416 | + self.assertIsNone(fs._check_dir('good')) |
1417 | + tmp.mkdir('.dmedia', 'foo', 'good') |
1418 | + tmp.chmod(0o777, '.dmedia', 'foo', 'good') |
1419 | + self.assertIsNone(fs._check_dir('foo/good')) |
1420 | + |
1421 | + # Test with symlinks to the same good directories: |
1422 | + self.assertTrue(path.isdir(tmp.symlink('good', '.dmedia', 'link'))) |
1423 | + with self.assertRaises(ValueError) as cm: |
1424 | + fs._check_dir('link') |
1425 | + self.assertEqual(str(cm.exception), |
1426 | + "'link' not a directory in {!r}".format(fs.basedir) |
1427 | + ) |
1428 | + self.assertTrue(path.isdir(tmp.symlink('good', '.dmedia', 'foo', 'link'))) |
1429 | + with self.assertRaises(ValueError) as cm: |
1430 | + fs._check_dir('foo/link') |
1431 | + self.assertEqual(str(cm.exception), |
1432 | + "'foo/link' not a directory in {!r}".format(fs.basedir) |
1433 | + ) |
1434 | + |
1435 | + # Does not exist: |
1436 | + with self.assertRaises(FileNotFoundError) as cm: |
1437 | + fs._check_dir('nope') |
1438 | + self.assertEqual(cm.exception.filename, 'nope') |
1439 | + with self.assertRaises(FileNotFoundError) as cm: |
1440 | + fs._check_dir('foo/nope') |
1441 | + self.assertEqual(cm.exception.filename, 'foo/nope') |
1442 | + |
1443 | + # A file instead of a directory: |
1444 | + tmp.touch('.dmedia', 'afile') |
1445 | + with self.assertRaises(ValueError) as cm: |
1446 | + fs._check_dir('afile') |
1447 | + self.assertEqual(str(cm.exception), |
1448 | + "'afile' not a directory in {!r}".format(fs.basedir) |
1449 | + ) |
1450 | + tmp.touch('.dmedia', 'foo', 'afile') |
1451 | + with self.assertRaises(ValueError) as cm: |
1452 | + fs._check_dir('foo/afile') |
1453 | + self.assertEqual(str(cm.exception), |
1454 | + "'foo/afile' not a directory in {!r}".format(fs.basedir) |
1455 | + ) |
1456 | + |
1457 | + # Directory, but mode is not 0777: |
1458 | + tmp.mkdir('.dmedia', 'adir') |
1459 | + with self.assertRaises(ValueError) as cm: |
1460 | + fs._check_dir('adir') |
1461 | + self.assertEqual(str(cm.exception), |
1462 | + "Directory 'adir' mode not 0777 in {!r}".format(fs.basedir) |
1463 | + ) |
1464 | + tmp.mkdir('.dmedia', 'foo', 'adir') |
1465 | + with self.assertRaises(ValueError) as cm: |
1466 | + fs._check_dir('foo/adir') |
1467 | + self.assertEqual(str(cm.exception), |
1468 | + "Directory 'foo/adir' mode not 0777 in {!r}".format(fs.basedir) |
1469 | + ) |
1470 | + |
1471 | + # All good: |
1472 | + tmp.chmod(0o777, '.dmedia', 'adir') |
1473 | + self.assertIsNone(fs._check_dir('adir')) |
1474 | + tmp.chmod(0o777, '.dmedia', 'foo', 'adir') |
1475 | + self.assertIsNone(fs._check_dir('foo/adir')) |
1476 | + |
1477 | + def test_check_file(self): |
1478 | + tmp = TempDir() |
1479 | + fs = filestore.FileStore.create(tmp.dir) |
1480 | + tmp.mkdir('.dmedia', 'foo') |
1481 | + |
1482 | + # Test with a few good files: |
1483 | + tmp.touch('.dmedia', 'good') |
1484 | + tmp.chmod(0o444, '.dmedia', 'good') |
1485 | + self.assertIsNone(fs._check_file('good')) |
1486 | + tmp.touch('.dmedia', 'foo', 'good') |
1487 | + tmp.chmod(0o444, '.dmedia', 'foo', 'good') |
1488 | + self.assertIsNone(fs._check_file('foo/good')) |
1489 | + |
1490 | + # Test with symlinks to the same good files: |
1491 | + self.assertTrue(path.isfile(tmp.symlink('good', '.dmedia', 'link'))) |
1492 | + with self.assertRaises(ValueError) as cm: |
1493 | + fs._check_file('link') |
1494 | + self.assertEqual(str(cm.exception), |
1495 | + "'link' not a file in {!r}".format(fs.basedir) |
1496 | + ) |
1497 | + self.assertTrue(path.isfile(tmp.symlink('good', '.dmedia', 'foo', 'link'))) |
1498 | + with self.assertRaises(ValueError) as cm: |
1499 | + fs._check_file('foo/link') |
1500 | + self.assertEqual(str(cm.exception), |
1501 | + "'foo/link' not a file in {!r}".format(fs.basedir) |
1502 | + ) |
1503 | + |
1504 | + # File does not exist: |
1505 | + with self.assertRaises(FileNotFoundError) as cm: |
1506 | + fs._check_file('nope') |
1507 | + self.assertEqual(cm.exception.filename, 'nope') |
1508 | + with self.assertRaises(FileNotFoundError) as cm: |
1509 | + fs._check_file('foo/nope') |
1510 | + self.assertEqual(cm.exception.filename, 'foo/nope') |
1511 | + |
1512 | + # Directory instead of a file: |
1513 | + tmp.mkdir('.dmedia', 'adir') |
1514 | + with self.assertRaises(ValueError) as cm: |
1515 | + fs._check_file('adir') |
1516 | + self.assertEqual(str(cm.exception), |
1517 | + "'adir' not a file in {!r}".format(fs.basedir) |
1518 | + ) |
1519 | + tmp.mkdir('.dmedia', 'foo', 'adir') |
1520 | + with self.assertRaises(ValueError) as cm: |
1521 | + fs._check_file('foo/adir') |
1522 | + self.assertEqual(str(cm.exception), |
1523 | + "'foo/adir' not a file in {!r}".format(fs.basedir) |
1524 | + ) |
1525 | + |
1526 | + # File, but mode is not 0444: |
1527 | + tmp.touch('.dmedia', 'afile') |
1528 | + with self.assertRaises(ValueError) as cm: |
1529 | + fs._check_file('afile') |
1530 | + self.assertEqual(str(cm.exception), |
1531 | + "File 'afile' mode not 0444 in {!r}".format(fs.basedir) |
1532 | + ) |
1533 | + tmp.touch('.dmedia', 'foo', 'afile') |
1534 | + with self.assertRaises(ValueError) as cm: |
1535 | + fs._check_file('foo/afile') |
1536 | + self.assertEqual(str(cm.exception), |
1537 | + "File 'foo/afile' mode not 0444 in {!r}".format(fs.basedir) |
1538 | + ) |
1539 | + |
1540 | + # All good: |
1541 | + tmp.chmod(0o444, '.dmedia', 'afile') |
1542 | + self.assertIsNone(fs._check_file('afile')) |
1543 | + tmp.chmod(0o444, '.dmedia', 'foo', 'afile') |
1544 | + self.assertIsNone(fs._check_file('foo/afile')) |
1545 | + |
1546 | + def test_check_layout(self): |
1547 | + # FIXME: also needs to test that symlinks aren't followed! |
1548 | + tmp = TempDir() |
1549 | + fs = filestore.FileStore.create(tmp.dir) |
1550 | + |
1551 | + # Cause some damage: |
1552 | + tmp.chmod(0o750, '.dmedia', 'files') |
1553 | + tmp.rmdir('.dmedia', 'tmp') |
1554 | + tmp.touch('.dmedia', 'tmp') |
1555 | + tmp.rmdir('.dmedia', 'corrupt') |
1556 | + tmp.chmod(0o750, '.dmedia', 'files', '39') |
1557 | + tmp.rmdir('.dmedia', 'files', '69') |
1558 | + tmp.touch('.dmedia', 'files', '69') |
1559 | + tmp.rmdir('.dmedia', 'files', 'AY') |
1560 | + tmp.chmod(0o600, '.dmedia', 'README.txt') |
1561 | + tmp.touch('.dmedia', 'good-file') |
1562 | + tmp.chmod(0o444, '.dmedia', 'good-file') |
1563 | + tmp.remove('.dmedia', 'filestore.json') |
1564 | + self.assertTrue(path.isfile(tmp.symlink('good-file', '.dmedia', 'filestore.json'))) |
1565 | + self.assertIsNone(fs._check_file('good-file')) |
1566 | + |
1567 | + # Test for expected execption, fix damage one by one: |
1568 | + # files/ |
1569 | + with self.assertRaises(ValueError) as cm: |
1570 | + fs.check_layout() |
1571 | + self.assertEqual(str(cm.exception), |
1572 | + "Directory 'files' mode not 0777 in {!r}".format(fs.basedir) |
1573 | + ) |
1574 | + tmp.chmod(0o777, '.dmedia', 'files') |
1575 | + |
1576 | + # tmp/ |
1577 | + with self.assertRaises(ValueError) as cm: |
1578 | + fs.check_layout() |
1579 | + self.assertEqual(str(cm.exception), |
1580 | + "'tmp' not a directory in {!r}".format(fs.basedir) |
1581 | + ) |
1582 | + tmp.remove('.dmedia', 'tmp') |
1583 | + tmp.mkdir('.dmedia', 'tmp') |
1584 | + with self.assertRaises(ValueError) as cm: |
1585 | + fs.check_layout() |
1586 | + self.assertEqual(str(cm.exception), |
1587 | + "Directory 'tmp' mode not 0777 in {!r}".format(fs.basedir) |
1588 | + ) |
1589 | + tmp.chmod(0o777, '.dmedia', 'tmp') |
1590 | + |
1591 | + # corrupt/ |
1592 | + with self.assertRaises(FileNotFoundError) as cm: |
1593 | + fs.check_layout() |
1594 | + self.assertEqual(cm.exception.filename, 'corrupt') |
1595 | + tmp.mkdir('.dmedia', 'corrupt') |
1596 | + with self.assertRaises(ValueError) as cm: |
1597 | + fs.check_layout() |
1598 | + self.assertEqual(str(cm.exception), |
1599 | + "Directory 'corrupt' mode not 0777 in {!r}".format(fs.basedir) |
1600 | + ) |
1601 | + tmp.chmod(0o777, '.dmedia', 'corrupt') |
1602 | + |
1603 | + # files/39/ |
1604 | + with self.assertRaises(ValueError) as cm: |
1605 | + fs.check_layout() |
1606 | + self.assertEqual(str(cm.exception), |
1607 | + "Directory 'files/39' mode not 0777 in {!r}".format(fs.basedir) |
1608 | + ) |
1609 | + tmp.chmod(0o777, '.dmedia', 'files', '39') |
1610 | + |
1611 | + # files/69/ |
1612 | + with self.assertRaises(ValueError) as cm: |
1613 | + fs.check_layout() |
1614 | + self.assertEqual(str(cm.exception), |
1615 | + "'files/69' not a directory in {!r}".format(fs.basedir) |
1616 | + ) |
1617 | + tmp.remove('.dmedia', 'files', '69') |
1618 | + tmp.mkdir('.dmedia', 'files', '69') |
1619 | + with self.assertRaises(ValueError) as cm: |
1620 | + fs.check_layout() |
1621 | + self.assertEqual(str(cm.exception), |
1622 | + "Directory 'files/69' mode not 0777 in {!r}".format(fs.basedir) |
1623 | + ) |
1624 | + tmp.chmod(0o777, '.dmedia', 'files', '69') |
1625 | + |
1626 | + # files/AY/ |
1627 | + with self.assertRaises(FileNotFoundError) as cm: |
1628 | + fs.check_layout() |
1629 | + self.assertEqual(cm.exception.filename, 'files/AY') |
1630 | + tmp.mkdir('.dmedia', 'files/AY') |
1631 | + with self.assertRaises(ValueError) as cm: |
1632 | + fs.check_layout() |
1633 | + self.assertEqual(str(cm.exception), |
1634 | + "Directory 'files/AY' mode not 0777 in {!r}".format(fs.basedir) |
1635 | + ) |
1636 | + tmp.chmod(0o777, '.dmedia', 'files', 'AY') |
1637 | + |
1638 | + # README.txt |
1639 | + with self.assertRaises(ValueError) as cm: |
1640 | + fs.check_layout() |
1641 | + self.assertEqual(str(cm.exception), |
1642 | + "File 'README.txt' mode not 0444 in {!r}".format(fs.basedir) |
1643 | + ) |
1644 | + tmp.chmod(0o444, '.dmedia', 'README.txt') |
1645 | + |
1646 | + # filestore.json |
1647 | + with self.assertRaises(ValueError) as cm: |
1648 | + fs.check_layout() |
1649 | + self.assertEqual(str(cm.exception), |
1650 | + "'filestore.json' not a file in {!r}".format(fs.basedir) |
1651 | + ) |
1652 | + tmp.remove('.dmedia', 'filestore.json') |
1653 | + with self.assertRaises(FileNotFoundError) as cm: |
1654 | + fs.check_layout() |
1655 | + self.assertEqual(cm.exception.filename, 'filestore.json') |
1656 | + tmp.touch('.dmedia', 'filestore.json') |
1657 | + with self.assertRaises(ValueError) as cm: |
1658 | + fs.check_layout() |
1659 | + self.assertEqual(str(cm.exception), |
1660 | + "File 'filestore.json' mode not 0444 in {!r}".format(fs.basedir) |
1661 | + ) |
1662 | + tmp.chmod(0o444, '.dmedia', 'filestore.json') |
1663 | + |
1664 | + # All damage fixed: |
1665 | + self.assertIsNone(fs.check_layout()) |
1666 | + |
1667 | + # Test once more with a prestine layout: |
1668 | + tmp = TempDir() |
1669 | + fs = filestore.FileStore.create(tmp.dir) |
1670 | + self.assertIsNone(fs.check_layout()) |
1671 | + del fs |
1672 | + fs = filestore.FileStore(tmp.dir) |
1673 | + self.assertIsNone(fs.check_layout()) |
1674 | |
1675 | def test_statvfs(self): |
1676 | tmp = TempDir() |
1677 | @@ -1287,90 +1796,6 @@ |
1678 | filestore.StatVFS(size, size - free, avail, False, s1.f_frsize) |
1679 | ) |
1680 | |
1681 | - def test_is_v0_files(self): |
1682 | - v0 = TempDir() |
1683 | - for name in filestore.B32NAMES: |
1684 | - v0.mkdir(name) |
1685 | - self.assertIs(filestore.is_v0_files(v0.dir), True) |
1686 | - v1 = TempDir() |
1687 | - for name in filestore.DB32NAMES: |
1688 | - v1.mkdir(name) |
1689 | - self.assertIs(filestore.is_v0_files(v1.dir), False) |
1690 | - tmp = TempDir() |
1691 | - fs = filestore.FileStore(tmp.dir) |
1692 | - files = tmp.join('.dmedia', 'files') |
1693 | - self.assertTrue(path.isdir(files)) |
1694 | - self.assertIs(filestore.is_v0_files(files), False) |
1695 | - |
1696 | - def test_migrate_store_doc(self): |
1697 | - tmp = TempDir() |
1698 | - store = tmp.join('store.json') |
1699 | - store0 = tmp.join('store0.json') |
1700 | - self.assertIs(filestore.migrate_store_doc(tmp.dir), False) |
1701 | - |
1702 | - # A V0 doc: |
1703 | - doc = { |
1704 | - '_id': 'DLA4NDZRW2LXEPF3RV7YHMON', |
1705 | - 'copies': 1, |
1706 | - 'plugin': 'filestore', |
1707 | - 'time': 1320063400.353743, |
1708 | - 'type': 'dmedia/store', |
1709 | - } |
1710 | - json.dump(doc, open(store, 'x')) |
1711 | - self.assertIs(filestore.migrate_store_doc(tmp.dir), True) |
1712 | - self.assertEqual(json.load(open(store, 'r')), |
1713 | - { |
1714 | - '_id': '6E3VG6SKPTEQ7I8UKOYRAFHG', |
1715 | - 'copies': 1, |
1716 | - 'plugin': 'filestore', |
1717 | - 'time': 1320063400.353743, |
1718 | - 'type': 'dmedia/store', |
1719 | - 'migrated': True, |
1720 | - } |
1721 | - ) |
1722 | - self.assertEqual(stat.S_IMODE(os.stat(store).st_mode), 0o444) |
1723 | - self.assertEqual(json.load(open(store0, 'r')), doc) |
1724 | - self.assertEqual( |
1725 | - set(os.listdir(tmp.dir)), |
1726 | - {'store.json', 'store0.json'} |
1727 | - ) |
1728 | - with self.assertRaises(Exception) as cm: |
1729 | - filestore.migrate_store_doc(tmp.dir) |
1730 | - self.assertEqual( |
1731 | - str(cm.exception), |
1732 | - "'store0.json' already exists in {!r}".format(tmp.dir) |
1733 | - ) |
1734 | - |
1735 | - # Try with some random ID values: |
1736 | - for i in range(25): |
1737 | - tmp = TempDir() |
1738 | - store = tmp.join('store.json') |
1739 | - store0 = tmp.join('store0.json') |
1740 | - data = os.urandom(15) |
1741 | - b32_id = b32enc(data) |
1742 | - db32_id = db32enc(data) |
1743 | - self.assertNotEqual(b32_id, db32_id) |
1744 | - json.dump({'_id': b32_id}, open(store, 'x')) |
1745 | - self.assertIs(filestore.migrate_store_doc(tmp.dir), True) |
1746 | - self.assertEqual(json.load(open(store, 'r')), |
1747 | - { |
1748 | - '_id': db32_id, |
1749 | - 'migrated': True, |
1750 | - } |
1751 | - ) |
1752 | - self.assertEqual(stat.S_IMODE(os.stat(store).st_mode), 0o444) |
1753 | - self.assertEqual(json.load(open(store0, 'r')), {'_id': b32_id}) |
1754 | - self.assertEqual( |
1755 | - set(os.listdir(tmp.dir)), |
1756 | - {'store.json', 'store0.json'} |
1757 | - ) |
1758 | - with self.assertRaises(Exception) as cm: |
1759 | - filestore.migrate_store_doc(tmp.dir) |
1760 | - self.assertEqual( |
1761 | - str(cm.exception), |
1762 | - "'store0.json' already exists in {!r}".format(tmp.dir) |
1763 | - ) |
1764 | - |
1765 | |
1766 | class TestHasher(TestCase): |
1767 | def test_init(self): |
1768 | @@ -1486,11 +1911,41 @@ |
1769 | |
1770 | |
1771 | class TestFileStore(TestCase): |
1772 | - def test_init(self): |
1773 | - |
1774 | + def check_subdirs(self, basedir): |
1775 | + self.assertEqual( |
1776 | + sorted(os.listdir(basedir)), |
1777 | + ['README.txt', 'corrupt', 'files', 'filestore.json', 'partial', 'tmp'] |
1778 | + ) |
1779 | + |
1780 | + f = path.join(basedir, 'README.txt') |
1781 | + self.assertFalse(path.islink(f)) |
1782 | + self.assertEqual(open(f, 'r').read(), filestore.README) |
1783 | + |
1784 | + self.assertEqual( |
1785 | + sorted(os.listdir(path.join(basedir, 'files'))), |
1786 | + list(filestore.DB32NAMES) |
1787 | + ) |
1788 | + for name in ['corrupt', 'partial', 'tmp']: |
1789 | + d = path.join(basedir, name) |
1790 | + self.assertEqual(os.listdir(d), []) |
1791 | + |
1792 | + for name in ['corrupt', 'files', 'partial', 'tmp']: |
1793 | + d = path.join(basedir, name) |
1794 | + self.assertTrue(path.isdir(d)) |
1795 | + self.assertFalse(path.islink(d)) |
1796 | + for name in filestore.DB32NAMES: |
1797 | + d = path.join(basedir, 'files', name) |
1798 | + self.assertTrue(path.isdir(d)) |
1799 | + self.assertFalse(path.islink(d)) |
1800 | + self.assertEqual(os.listdir(d), []) |
1801 | + |
1802 | + def test_create(self): |
1803 | + """ |
1804 | + Test the FileStore.create() classmethod |
1805 | + """ |
1806 | # Test with relative path: |
1807 | with self.assertRaises(filestore.PathError) as cm: |
1808 | - fs = filestore.FileStore('foo/bar') |
1809 | + fs = filestore.FileStore.create('foo/bar') |
1810 | self.assertEqual(cm.exception.untrusted, 'foo/bar') |
1811 | self.assertEqual(cm.exception.abspath, path.abspath('foo/bar')) |
1812 | self.assertEqual( |
1813 | @@ -1500,7 +1955,7 @@ |
1814 | |
1815 | # Test with path traversal: |
1816 | with self.assertRaises(filestore.PathError) as cm: |
1817 | - fs = filestore.FileStore('/foo/bar/../../root') |
1818 | + fs = filestore.FileStore.create('/foo/bar/../../root') |
1819 | self.assertEqual(cm.exception.untrusted, '/foo/bar/../../root') |
1820 | self.assertEqual(cm.exception.abspath, '/root') |
1821 | self.assertEqual( |
1822 | @@ -1512,7 +1967,7 @@ |
1823 | tmp = TempDir() |
1824 | parentdir = tmp.makedirs('.dmedia', 'foo') |
1825 | with self.assertRaises(filestore.NestedParent) as cm: |
1826 | - fs = filestore.FileStore(parentdir) |
1827 | + fs = filestore.FileStore.create(parentdir) |
1828 | self.assertEqual(cm.exception.parentdir, parentdir) |
1829 | self.assertEqual(cm.exception.dotname, '.dmedia') |
1830 | self.assertEqual( |
1831 | @@ -1524,167 +1979,192 @@ |
1832 | tmp = TempDir() |
1833 | parentdir = tmp.join('nope') |
1834 | with self.assertRaises(ValueError) as cm: |
1835 | - fs = filestore.FileStore(parentdir) |
1836 | + fs = filestore.FileStore.create(parentdir) |
1837 | self.assertEqual( |
1838 | str(cm.exception), |
1839 | - 'FileStore.parentdir not a directory: {!r}'.format(parentdir) |
1840 | + 'parentdir not a directory: {!r}'.format(parentdir) |
1841 | ) |
1842 | |
1843 | # Test when parentdir is a file: |
1844 | tmp = TempDir() |
1845 | parentdir = tmp.touch('somefile') |
1846 | with self.assertRaises(ValueError) as cm: |
1847 | - fs = filestore.FileStore(parentdir) |
1848 | + fs = filestore.FileStore.create(parentdir) |
1849 | self.assertEqual( |
1850 | str(cm.exception), |
1851 | - 'FileStore.parentdir not a directory: {!r}'.format(parentdir) |
1852 | + 'parentdir not a directory: {!r}'.format(parentdir) |
1853 | ) |
1854 | |
1855 | - # Test when basedir does not exist |
1856 | + # Test when basedir does not exist: |
1857 | tmp = TempDir() |
1858 | basedir = tmp.join('.dmedia') |
1859 | self.assertFalse(path.exists(basedir)) |
1860 | - fs = filestore.FileStore(tmp.dir) |
1861 | + fs = filestore.FileStore.create(tmp.dir) |
1862 | self.assertEqual(fs.parentdir, tmp.dir) |
1863 | self.assertEqual(fs.basedir, basedir) |
1864 | self.assertTrue(path.isdir(basedir)) |
1865 | self.assertEqual(fs.tmp, path.join(basedir, 'tmp')) |
1866 | self.assertTrue(path.isdir(fs.tmp)) |
1867 | - self.assertIsNone(fs.id) |
1868 | - self.assertEqual(fs.copies, 0) |
1869 | - self.assertFalse(fs.needs_migration) |
1870 | + self.assertIsInstance(fs.id, str) |
1871 | + self.assertEqual(len(fs.id), 24) |
1872 | + self.assertTrue(isdb32(fs.id)) |
1873 | + self.assertEqual(fs.copies, 1) |
1874 | + self.check_subdirs(fs.basedir) |
1875 | |
1876 | # Test when _id and copies are supplied |
1877 | tmp = TempDir() |
1878 | - fs = filestore.FileStore(tmp.dir, 'foo', 1) |
1879 | - self.assertEqual(fs.id, 'foo') |
1880 | - self.assertEqual(fs.copies, 1) |
1881 | - self.assertFalse(fs.needs_migration) |
1882 | + store_id = random_id() |
1883 | + fs = filestore.FileStore.create(tmp.dir, store_id, 2) |
1884 | + self.assertEqual(fs.id, store_id) |
1885 | + self.assertEqual(fs.copies, 2) |
1886 | + self.check_subdirs(fs.basedir) |
1887 | |
1888 | # Test when basedir exists and is a directory |
1889 | tmp = TempDir() |
1890 | - basedir = tmp.makedirs('.dmedia') |
1891 | - fs = filestore.FileStore(tmp.dir) |
1892 | - self.assertEqual(fs.parentdir, tmp.dir) |
1893 | - self.assertEqual(fs.basedir, basedir) |
1894 | - self.assertTrue(path.isdir(basedir)) |
1895 | - self.assertEqual(fs.tmp, path.join(basedir, 'tmp')) |
1896 | - self.assertFalse(path.isdir(fs.tmp)) |
1897 | - self.assertFalse(fs.needs_migration) |
1898 | + basedir = tmp.mkdir('.dmedia') |
1899 | + with self.assertRaises(ValueError) as cm: |
1900 | + filestore.FileStore.create(tmp.dir) |
1901 | + self.assertEqual(str(cm.exception), |
1902 | + 'Cannot create FileStore, dotdir exists: {!r}'.format(basedir) |
1903 | + ) |
1904 | |
1905 | # Test when basedir exists and is a file |
1906 | tmp = TempDir() |
1907 | basedir = tmp.touch('.dmedia') |
1908 | with self.assertRaises(ValueError) as cm: |
1909 | - fs = filestore.FileStore(tmp.dir) |
1910 | - self.assertEqual( |
1911 | - str(cm.exception), |
1912 | - 'not a directory: {!r}'.format(basedir) |
1913 | - ) |
1914 | - |
1915 | - # Test when basedir exists and is a symlink to a dir |
1916 | - tmp = TempDir() |
1917 | - d = tmp.makedirs('foo') |
1918 | - basedir = tmp.join('.dmedia') |
1919 | - os.symlink(d, basedir) |
1920 | - with self.assertRaises(ValueError) as cm: |
1921 | - fs = filestore.FileStore(tmp.dir) |
1922 | - self.assertEqual( |
1923 | - str(cm.exception), |
1924 | - '{!r} is symlink to {!r}'.format(basedir, d) |
1925 | - ) |
1926 | - |
1927 | - # Test when .dmedia/files/ contains a V0, Base32 layout: |
1928 | - tmp = TempDir() |
1929 | - files = tmp.join('.dmedia', 'files') |
1930 | - files0 = tmp.join('.dmedia', 'files0') |
1931 | - fs = filestore.FileStore(tmp.dir) |
1932 | - shutil.rmtree(files) |
1933 | - os.mkdir(files) |
1934 | - for name in filestore.B32NAMES: |
1935 | - os.mkdir(path.join(files, name)) |
1936 | - fs = filestore.FileStore(tmp.dir) |
1937 | - self.assertTrue(fs.needs_migration) |
1938 | - self.assertTrue(path.isdir(files)) |
1939 | - for name in filestore.DB32NAMES: |
1940 | - self.assertTrue(path.isdir(path.join(files, name))) |
1941 | - self.assertEqual(sorted(os.listdir(files)), list(filestore.DB32NAMES)) |
1942 | - self.assertTrue(path.isdir(files0)) |
1943 | - for name in filestore.B32NAMES: |
1944 | - self.assertTrue(path.isdir(path.join(files0, name))) |
1945 | - self.assertEqual(sorted(os.listdir(files0)), list(filestore.B32NAMES)) |
1946 | - |
1947 | - # Test that no futher change is done: |
1948 | - fs = filestore.FileStore(tmp.dir) |
1949 | - self.assertTrue(fs.needs_migration) |
1950 | - self.assertTrue(path.isdir(files)) |
1951 | - for name in filestore.DB32NAMES: |
1952 | - self.assertTrue(path.isdir(path.join(files, name))) |
1953 | - self.assertEqual(sorted(os.listdir(files)), list(filestore.DB32NAMES)) |
1954 | - self.assertTrue(path.isdir(files0)) |
1955 | - for name in filestore.B32NAMES: |
1956 | - self.assertTrue(path.isdir(path.join(files0, name))) |
1957 | - self.assertEqual(sorted(os.listdir(files0)), list(filestore.B32NAMES)) |
1958 | - |
1959 | - # Test when files contains V0/Base32 layout but files0 already exists: |
1960 | - shutil.rmtree(files) |
1961 | - os.mkdir(files) |
1962 | - for name in filestore.B32NAMES: |
1963 | - os.mkdir(path.join(files, name)) |
1964 | - with self.assertRaises(Exception) as cm: |
1965 | - fs = filestore.FileStore(tmp.dir) |
1966 | - self.assertEqual( |
1967 | - str(cm.exception), |
1968 | - "'files' is V0 layout but 'files0' exists in {!r}".format(tmp.join('.dmedia')) |
1969 | - ) |
1970 | - |
1971 | - # Test that store.json gets properly migrated: |
1972 | - tmp = TempDir() |
1973 | - files = tmp.join('.dmedia', 'files') |
1974 | - files0 = tmp.join('.dmedia', 'files0') |
1975 | - store = tmp.join('.dmedia', 'store.json') |
1976 | - store0 = tmp.join('.dmedia', 'store0.json') |
1977 | - |
1978 | - # Setup: |
1979 | - fs = filestore.FileStore(tmp.dir) |
1980 | - shutil.rmtree(files) |
1981 | - os.mkdir(files) |
1982 | - for name in filestore.B32NAMES: |
1983 | - os.mkdir(path.join(files, name)) |
1984 | - data = os.urandom(15) |
1985 | - b32_id = b32enc(data) |
1986 | - db32_id = db32enc(data) |
1987 | - self.assertNotEqual(b32_id, db32_id) |
1988 | - json.dump({'_id': b32_id}, open(store, 'x')) |
1989 | - |
1990 | - # And test: |
1991 | - fs = filestore.FileStore(tmp.dir) |
1992 | - self.assertTrue(fs.needs_migration) |
1993 | - self.assertTrue(path.isdir(files)) |
1994 | - for name in filestore.DB32NAMES: |
1995 | - self.assertTrue(path.isdir(path.join(files, name))) |
1996 | - self.assertEqual(sorted(os.listdir(files)), list(filestore.DB32NAMES)) |
1997 | - self.assertTrue(path.isdir(files0)) |
1998 | - for name in filestore.B32NAMES: |
1999 | - self.assertTrue(path.isdir(path.join(files0, name))) |
2000 | - self.assertEqual(sorted(os.listdir(files0)), list(filestore.B32NAMES)) |
2001 | - self.assertEqual(json.load(open(store, 'r')), |
2002 | - { |
2003 | - '_id': db32_id, |
2004 | - 'migrated': True, |
2005 | - } |
2006 | - ) |
2007 | - self.assertEqual(stat.S_IMODE(os.stat(store).st_mode), 0o444) |
2008 | - self.assertEqual(json.load(open(store0, 'r')), {'_id': b32_id}) |
2009 | + filestore.FileStore.create(tmp.dir) |
2010 | + self.assertEqual(str(cm.exception), |
2011 | + 'Cannot create FileStore, dotdir exists: {!r}'.format(basedir) |
2012 | + ) |
2013 | + |
2014 | + def test_init(self): |
2015 | + # Test with relative path: |
2016 | + with self.assertRaises(filestore.PathError) as cm: |
2017 | + fs = filestore.FileStore('foo/bar') |
2018 | + self.assertEqual(cm.exception.untrusted, 'foo/bar') |
2019 | + self.assertEqual(cm.exception.abspath, path.abspath('foo/bar')) |
2020 | + self.assertEqual( |
2021 | + str(cm.exception), |
2022 | + '{!r} resolves to {!r}'.format('foo/bar', path.abspath('foo/bar')) |
2023 | + ) |
2024 | + |
2025 | + # Test with path traversal: |
2026 | + with self.assertRaises(filestore.PathError) as cm: |
2027 | + fs = filestore.FileStore('/foo/bar/../../root') |
2028 | + self.assertEqual(cm.exception.untrusted, '/foo/bar/../../root') |
2029 | + self.assertEqual(cm.exception.abspath, '/root') |
2030 | + self.assertEqual( |
2031 | + str(cm.exception), |
2032 | + '{!r} resolves to {!r}'.format('/foo/bar/../../root', '/root') |
2033 | + ) |
2034 | + |
2035 | + # Test with a nested parentdir: |
2036 | + tmp = TempDir() |
2037 | + parentdir = tmp.makedirs('.dmedia', 'foo') |
2038 | + with self.assertRaises(filestore.NestedParent) as cm: |
2039 | + fs = filestore.FileStore(parentdir) |
2040 | + self.assertEqual(cm.exception.parentdir, parentdir) |
2041 | + self.assertEqual(cm.exception.dotname, '.dmedia') |
2042 | + self.assertEqual( |
2043 | + str(cm.exception), |
2044 | + '{!r} contains {!r}'.format(parentdir, '.dmedia') |
2045 | + ) |
2046 | + |
2047 | + # Test when parentdir doesn't exists: |
2048 | + tmp = TempDir() |
2049 | + parentdir = tmp.join('nope') |
2050 | + with self.assertRaises(ValueError) as cm: |
2051 | + fs = filestore.FileStore(parentdir) |
2052 | + self.assertEqual( |
2053 | + str(cm.exception), |
2054 | + 'parentdir not a directory: {!r}'.format(parentdir) |
2055 | + ) |
2056 | + |
2057 | + # Test when parentdir is a file: |
2058 | + tmp = TempDir() |
2059 | + parentdir = tmp.touch('somefile') |
2060 | + with self.assertRaises(ValueError) as cm: |
2061 | + fs = filestore.FileStore(parentdir) |
2062 | + self.assertEqual( |
2063 | + str(cm.exception), |
2064 | + 'parentdir not a directory: {!r}'.format(parentdir) |
2065 | + ) |
2066 | + |
2067 | + # Test when FileStore has not been initialized with FileStore.create(): |
2068 | + tmp = TempDir() |
2069 | + with self.assertRaises(FileNotFoundError) as cm: |
2070 | + filestore.FileStore(tmp.dir) |
2071 | + |
2072 | + # Test when basedir exists and is a file |
2073 | + tmp = TempDir() |
2074 | + tmp.touch('.dmedia') |
2075 | + with self.assertRaises(NotADirectoryError) as cm: |
2076 | + filestore.FileStore(tmp.dir) |
2077 | + |
2078 | + # Test when FileStore was properly initalized: |
2079 | + tmp = TempDir() |
2080 | + store_id = random_id() |
2081 | + doc = filestore._create_filestore(tmp.dir, store_id, 1) |
2082 | + fs = filestore.FileStore(tmp.dir) |
2083 | + self.assertEqual(fs.parentdir, tmp.dir) |
2084 | + self.assertEqual(fs.basedir, tmp.join('.dmedia')) |
2085 | + self.assertIsInstance(fs.dir_fd, int) |
2086 | + self.assertEqual(fs.doc, doc) |
2087 | + self.assertEqual(fs.id, store_id) |
2088 | + self.assertEqual(fs.copies, 1) |
2089 | + self.assertEqual(fs.tmp, tmp.join('.dmedia', 'tmp')) |
2090 | + self.check_subdirs(fs.basedir) |
2091 | + |
2092 | + # Test when expected_id is provided and matches: |
2093 | + fs = filestore.FileStore(tmp.dir, expected_id=store_id) |
2094 | + self.assertEqual(fs.parentdir, tmp.dir) |
2095 | + self.assertEqual(fs.basedir, tmp.join('.dmedia')) |
2096 | + self.assertIsInstance(fs.dir_fd, int) |
2097 | + self.assertEqual(fs.doc, doc) |
2098 | + self.assertEqual(fs.id, store_id) |
2099 | + self.assertEqual(fs.copies, 1) |
2100 | + self.assertEqual(fs.tmp, tmp.join('.dmedia', 'tmp')) |
2101 | + self.check_subdirs(fs.basedir) |
2102 | + |
2103 | + # Test when expected_id is provided and does *not* match: |
2104 | + wrong_id = random_id() |
2105 | + with self.assertRaises(ValueError) as cm: |
2106 | + filestore.FileStore(tmp.dir, expected_id=wrong_id) |
2107 | + self.assertEqual(str(cm.exception), |
2108 | + "doc['_id']: expected {!r}; got {!r}".format(wrong_id, store_id) |
2109 | + ) |
2110 | |
2111 | def test_repr(self): |
2112 | - tmp = TempDir() |
2113 | - fs = filestore.FileStore(tmp.dir) |
2114 | - self.assertEqual(repr(fs), 'FileStore({!r})'.format(tmp.dir)) |
2115 | + # With auto-generated ID: |
2116 | + tmp = TempDir() |
2117 | + fs = filestore.FileStore.create(tmp.dir) |
2118 | + store_id = fs.id |
2119 | + self.assertTrue(isdb32(store_id) and len(store_id) == 24) |
2120 | + self.assertEqual(repr(fs), |
2121 | + 'FileStore({!r}, {!r})'.format(tmp.dir, store_id) |
2122 | + ) |
2123 | + del fs |
2124 | + fs = filestore.FileStore(tmp.dir) |
2125 | + self.assertEqual(repr(fs), |
2126 | + 'FileStore({!r}, {!r})'.format(tmp.dir, store_id) |
2127 | + ) |
2128 | + |
2129 | + # With explicit ID: |
2130 | + tmp = TempDir() |
2131 | + store_id = random_id() |
2132 | + fs = filestore.FileStore.create(tmp.dir, store_id) |
2133 | + self.assertEqual(repr(fs), |
2134 | + 'FileStore({!r}, {!r})'.format(tmp.dir, store_id) |
2135 | + ) |
2136 | + del fs |
2137 | + fs = filestore.FileStore(tmp.dir) |
2138 | + self.assertEqual(repr(fs), |
2139 | + 'FileStore({!r}, {!r})'.format(tmp.dir, store_id) |
2140 | + ) |
2141 | |
2142 | def test_iter(self): |
2143 | tmp = TempDir() |
2144 | - fs = filestore.FileStore(tmp.dir) |
2145 | + fs = filestore.FileStore.create(tmp.dir) |
2146 | |
2147 | # Should ignore files with wrong ID length: |
2148 | short = tuple(random_file_id(25) for i in range(50)) |
2149 | @@ -1766,61 +2246,9 @@ |
2150 | assert path.islink(fs.path(_id)) |
2151 | self.assertEqual(list(fs), stats) |
2152 | |
2153 | - def check_subdirs(self, basedir): |
2154 | - self.assertEqual( |
2155 | - sorted(os.listdir(basedir)), |
2156 | - ['README.txt', 'corrupt', 'files', 'partial', 'tmp'] |
2157 | - ) |
2158 | - |
2159 | - f = path.join(basedir, 'README.txt') |
2160 | - self.assertFalse(path.islink(f)) |
2161 | - self.assertEqual(open(f, 'rt').read(), filestore.README) |
2162 | - |
2163 | - self.assertEqual( |
2164 | - sorted(os.listdir(path.join(basedir, 'files'))), |
2165 | - list(filestore.DB32NAMES) |
2166 | - ) |
2167 | - for name in ['corrupt', 'partial', 'tmp']: |
2168 | - d = path.join(basedir, name) |
2169 | - self.assertEqual(os.listdir(d), []) |
2170 | - |
2171 | - for name in ['corrupt', 'files', 'partial', 'tmp']: |
2172 | - d = path.join(basedir, name) |
2173 | - self.assertTrue(path.isdir(d)) |
2174 | - self.assertFalse(path.islink(d)) |
2175 | - for name in filestore.DB32NAMES: |
2176 | - d = path.join(basedir, 'files', name) |
2177 | - self.assertTrue(path.isdir(d)) |
2178 | - self.assertFalse(path.islink(d)) |
2179 | - self.assertEqual(os.listdir(d), []) |
2180 | - |
2181 | - def test_init_dirs(self): |
2182 | - tmp = TempDir() |
2183 | - |
2184 | - # Create basedir dir so init_dirs() doesn't get called |
2185 | - basedir = tmp.makedirs('.dmedia') |
2186 | - fs = filestore.FileStore(tmp.dir) |
2187 | - self.assertEqual(os.listdir(basedir), []) |
2188 | - |
2189 | - # Test when no subdirs exist: |
2190 | - self.assertIsNone(fs.init_dirs()) |
2191 | - self.check_subdirs(basedir) |
2192 | - |
2193 | - # Test when all subdirs exist: |
2194 | - self.assertIsNone(fs.init_dirs()) |
2195 | - self.check_subdirs(basedir) |
2196 | - |
2197 | - # Test when some subdirs exist: |
2198 | - os.rmdir(path.join(basedir, 'tmp')) |
2199 | - for (i, name) in enumerate(filestore.DB32NAMES): |
2200 | - if i % 3 == 0: |
2201 | - d = path.join(basedir, 'files', name) |
2202 | - self.assertIsNone(fs.init_dirs()) |
2203 | - self.check_subdirs(basedir) |
2204 | - |
2205 | def test_statvfs(self): |
2206 | tmp = TempDir() |
2207 | - fs = filestore.FileStore(tmp.dir) |
2208 | + fs = filestore.FileStore.create(tmp.dir) |
2209 | s1 = os.statvfs(tmp.dir) |
2210 | s2 = fs.statvfs() |
2211 | self.assertIsInstance(s2, filestore.StatVFS) |
2212 | @@ -1835,7 +2263,7 @@ |
2213 | tmp = TempDir() |
2214 | parentdir = tmp.makedirs('foo') |
2215 | basedir = tmp.join('foo', '.dmedia') |
2216 | - fs = filestore.FileStore(parentdir) |
2217 | + fs = filestore.FileStore.create(parentdir) |
2218 | |
2219 | bad = tmp.join('foo', '.dmedia2', 'stuff') |
2220 | with self.assertRaises(filestore.PathTraversal) as cm: |
2221 | @@ -1858,7 +2286,7 @@ |
2222 | def test_join(self): |
2223 | tmp = TempDir() |
2224 | parentdir = tmp.makedirs('foo') |
2225 | - fs = filestore.FileStore(parentdir) |
2226 | + fs = filestore.FileStore.create(parentdir) |
2227 | |
2228 | # Test with an absolute path in parts: |
2229 | with self.assertRaises(filestore.PathTraversal) as cm: |
2230 | @@ -1900,7 +2328,7 @@ |
2231 | def test_path(self): |
2232 | tmp = TempDir() |
2233 | parentdir = tmp.makedirs('foo') |
2234 | - fs = filestore.FileStore(parentdir) |
2235 | + fs = filestore.FileStore.create(parentdir) |
2236 | |
2237 | _id = random_file_id() |
2238 | self.assertEqual( |
2239 | @@ -1917,7 +2345,7 @@ |
2240 | def test_partial_path(self): |
2241 | tmp = TempDir() |
2242 | parentdir = tmp.makedirs('foo') |
2243 | - fs = filestore.FileStore(parentdir) |
2244 | + fs = filestore.FileStore.create(parentdir) |
2245 | |
2246 | _id = random_file_id() |
2247 | self.assertEqual( |
2248 | @@ -1934,7 +2362,7 @@ |
2249 | def test_corrupt_path(self): |
2250 | tmp = TempDir() |
2251 | parentdir = tmp.makedirs('foo') |
2252 | - fs = filestore.FileStore(parentdir) |
2253 | + fs = filestore.FileStore.create(parentdir) |
2254 | |
2255 | _id = random_file_id() |
2256 | self.assertEqual( |
2257 | @@ -1950,7 +2378,7 @@ |
2258 | |
2259 | def test_random_tmp_path(self): |
2260 | tmp = TempDir() |
2261 | - fs = filestore.FileStore(tmp.dir) |
2262 | + fs = filestore.FileStore.create(tmp.dir) |
2263 | filename = fs.random_tmp_path() |
2264 | self.assertEqual(path.dirname(filename), fs.tmp) |
2265 | name = path.basename(filename) |
2266 | @@ -1960,7 +2388,7 @@ |
2267 | |
2268 | def test_exists(self): |
2269 | tmp = TempDir() |
2270 | - fs = filestore.FileStore(tmp.dir) |
2271 | + fs = filestore.FileStore.create(tmp.dir) |
2272 | |
2273 | id1 = random_file_id() |
2274 | |
2275 | @@ -1988,7 +2416,7 @@ |
2276 | def test_stat(self): |
2277 | # File doesn't exist |
2278 | tmp = TempDir() |
2279 | - fs = filestore.FileStore(tmp.dir) |
2280 | + fs = filestore.FileStore.create(tmp.dir) |
2281 | _id = random_file_id() |
2282 | with self.assertRaises(filestore.FileNotFound) as cm: |
2283 | st = fs.stat(_id) |
2284 | @@ -2001,7 +2429,7 @@ |
2285 | |
2286 | # File is a symlink: |
2287 | tmp = TempDir() |
2288 | - fs = filestore.FileStore(tmp.dir) |
2289 | + fs = filestore.FileStore.create(tmp.dir) |
2290 | file = random_file_id() |
2291 | link = random_file_id() |
2292 | open(fs.path(file), 'xb').write(b'Novacut') |
2293 | @@ -2019,7 +2447,7 @@ |
2294 | |
2295 | # File is a directory |
2296 | tmp = TempDir() |
2297 | - fs = filestore.FileStore(tmp.dir) |
2298 | + fs = filestore.FileStore.create(tmp.dir) |
2299 | _id = random_file_id() |
2300 | os.mkdir(fs.path(_id)) |
2301 | assert path.isdir(fs.path(_id)) |
2302 | @@ -2034,7 +2462,7 @@ |
2303 | |
2304 | # Empty file |
2305 | tmp = TempDir() |
2306 | - fs = filestore.FileStore(tmp.dir) |
2307 | + fs = filestore.FileStore.create(tmp.dir) |
2308 | _id = random_file_id() |
2309 | open(fs.path(_id), 'wb').close() |
2310 | assert path.isfile(fs.path(_id)) |
2311 | @@ -2051,7 +2479,7 @@ |
2312 | |
2313 | # Valid file |
2314 | tmp = TempDir() |
2315 | - fs = filestore.FileStore(tmp.dir) |
2316 | + fs = filestore.FileStore.create(tmp.dir) |
2317 | _id = random_file_id() |
2318 | open(fs.path(_id), 'wb').write(b'Novacut') |
2319 | st = fs.stat(_id) |
2320 | @@ -2063,7 +2491,7 @@ |
2321 | |
2322 | def test_open(self): |
2323 | tmp = TempDir() |
2324 | - fs = filestore.FileStore(tmp.dir) |
2325 | + fs = filestore.FileStore.create(tmp.dir) |
2326 | |
2327 | _id = random_file_id() |
2328 | |
2329 | @@ -2086,7 +2514,7 @@ |
2330 | |
2331 | def test_verify(self): |
2332 | tmp = TempDir() |
2333 | - fs = filestore.FileStore(tmp.dir) |
2334 | + fs = filestore.FileStore.create(tmp.dir) |
2335 | |
2336 | canonical = fs.path(ID) |
2337 | corrupt = fs.corrupt_path(ID) |
2338 | @@ -2134,7 +2562,7 @@ |
2339 | |
2340 | def test_verify_iter(self): |
2341 | tmp = TempDir() |
2342 | - fs = filestore.FileStore(tmp.dir) |
2343 | + fs = filestore.FileStore.create(tmp.dir) |
2344 | canonical = fs.path(ID) |
2345 | corrupt = fs.corrupt_path(ID) |
2346 | |
2347 | @@ -2230,7 +2658,7 @@ |
2348 | |
2349 | def test_verify_iter2(self): |
2350 | tmp = TempDir() |
2351 | - fs = filestore.FileStore(tmp.dir) |
2352 | + fs = filestore.FileStore.create(tmp.dir) |
2353 | |
2354 | canonical = fs.path(ID) |
2355 | corrupt = fs.corrupt_path(ID) |
2356 | @@ -2277,7 +2705,7 @@ |
2357 | |
2358 | def test_content_md5(self): |
2359 | tmp = TempDir() |
2360 | - fs = filestore.FileStore(tmp.dir) |
2361 | + fs = filestore.FileStore.create(tmp.dir) |
2362 | |
2363 | canonical = fs.path(ID) |
2364 | corrupt = fs.corrupt_path(ID) |
2365 | @@ -2322,7 +2750,7 @@ |
2366 | |
2367 | def test_remove(self): |
2368 | tmp = TempDir() |
2369 | - fs = filestore.FileStore(tmp.dir) |
2370 | + fs = filestore.FileStore.create(tmp.dir) |
2371 | _id = random_file_id() |
2372 | canonical = fs.path(_id) |
2373 | |
2374 | @@ -2340,7 +2768,7 @@ |
2375 | def test_purge_tmp(self): |
2376 | tmp = TempDir() |
2377 | t = tmp.join('.dmedia', 'tmp') |
2378 | - fs = filestore.FileStore(tmp.dir) |
2379 | + fs = filestore.FileStore.create(tmp.dir) |
2380 | self.assertTrue(path.isdir(t)) |
2381 | |
2382 | # Test when nothing is in tmp: |
2383 | @@ -2373,7 +2801,7 @@ |
2384 | |
2385 | # Test that symlinks are ignored: |
2386 | tmp2 = TempDir() |
2387 | - fs2 = filestore.FileStore(tmp2.dir) |
2388 | + fs2 = filestore.FileStore.create(tmp2.dir) |
2389 | names = ['aye', 'bee', 'see'] |
2390 | for i in range(10): |
2391 | size = random.randint(1, 1024 * 1024) |
2392 | @@ -2410,7 +2838,7 @@ |
2393 | def test_allocate_tmp(self): |
2394 | tmp = TempDir() |
2395 | t = tmp.join('.dmedia', 'tmp') |
2396 | - fs = filestore.FileStore(tmp.dir) |
2397 | + fs = filestore.FileStore.create(tmp.dir) |
2398 | self.assertTrue(path.isdir(t)) |
2399 | |
2400 | # Test with size=None |
2401 | @@ -2458,7 +2886,7 @@ |
2402 | |
2403 | def test_allocate_partial(self): |
2404 | tmp = TempDir() |
2405 | - fs = filestore.FileStore(tmp.dir) |
2406 | + fs = filestore.FileStore.create(tmp.dir) |
2407 | _id = random_file_id() |
2408 | filename = tmp.join('.dmedia', 'partial', _id) |
2409 | |
2410 | @@ -2530,7 +2958,7 @@ |
2411 | |
2412 | def test_move_to_canonical(self): |
2413 | tmp = TempDir() |
2414 | - fs = filestore.FileStore(tmp.dir) |
2415 | + fs = filestore.FileStore.create(tmp.dir) |
2416 | _id = random_file_id() |
2417 | dst = fs.path(_id) |
2418 | |
2419 | @@ -2601,7 +3029,7 @@ |
2420 | |
2421 | def test_move_to_corrupt(self): |
2422 | tmp = TempDir() |
2423 | - fs = filestore.FileStore(tmp.dir) |
2424 | + fs = filestore.FileStore.create(tmp.dir) |
2425 | _id = random_file_id() |
2426 | corrupt = fs.corrupt_path(_id) |
2427 | canonical = fs.path(_id) |
2428 | @@ -2685,7 +3113,7 @@ |
2429 | def test_verify_and_move(self): |
2430 | # Test when it's all good |
2431 | tmp = TempDir() |
2432 | - fs = filestore.FileStore(tmp.dir) |
2433 | + fs = filestore.FileStore.create(tmp.dir) |
2434 | src = fs.partial_path(ID) |
2435 | write_sample_file(src) |
2436 | src_fp = open(src, 'rb') |
2437 | @@ -2696,7 +3124,7 @@ |
2438 | |
2439 | # Test when tmp_fp.name != partial_path(_id) |
2440 | tmp = TempDir() |
2441 | - fs = filestore.FileStore(tmp.dir) |
2442 | + fs = filestore.FileStore.create(tmp.dir) |
2443 | src = path.join(fs.tmp, 'foo.mov') |
2444 | write_sample_file(src) |
2445 | src_fp = open(src, 'rb') |
2446 | @@ -2710,7 +3138,7 @@ |
2447 | |
2448 | # Test when partial has wrong content |
2449 | tmp = TempDir() |
2450 | - fs = filestore.FileStore(tmp.dir) |
2451 | + fs = filestore.FileStore.create(tmp.dir) |
2452 | src = fs.partial_path(ID) |
2453 | src_fp = open(src, 'wb') |
2454 | for leaf in LEAVES: |
2455 | @@ -2730,7 +3158,7 @@ |
2456 | |
2457 | def test_hash_and_move(self): |
2458 | tmp = TempDir() |
2459 | - fs = filestore.FileStore(tmp.dir) |
2460 | + fs = filestore.FileStore.create(tmp.dir) |
2461 | src = path.join(fs.tmp, 'foo.mov') |
2462 | write_sample_file(src) |
2463 | src_fp = open(src, 'rb') |
2464 | @@ -2741,7 +3169,7 @@ |
2465 | |
2466 | # Test David's use case: |
2467 | tmp = TempDir() |
2468 | - fs = filestore.FileStore(tmp.dir) |
2469 | + fs = filestore.FileStore.create(tmp.dir) |
2470 | tmp_fp = fs.allocate_tmp() |
2471 | write_sample_file(tmp_fp) |
2472 | self.assertFalse(fs.exists(ID)) |
2473 | @@ -2754,7 +3182,7 @@ |
2474 | src = tmp.join('movie.mov') |
2475 | write_sample_file(src) |
2476 | |
2477 | - fs = filestore.FileStore(tmp.dir) |
2478 | + fs = filestore.FileStore.create(tmp.dir) |
2479 | dst = fs.path(ID) |
2480 | self.assertTrue(path.isfile(src)) |
2481 | self.assertFalse(path.exists(dst)) |
2482 | @@ -2768,12 +3196,12 @@ |
2483 | |
2484 | def test_copy(self): |
2485 | tmp = TempDir() |
2486 | - fs = filestore.FileStore(tmp.dir) |
2487 | + fs = filestore.FileStore.create(tmp.dir) |
2488 | |
2489 | tmp1 = TempDir() |
2490 | tmp2 = TempDir() |
2491 | - fs1 = filestore.FileStore(tmp1.dir) |
2492 | - fs2 = filestore.FileStore(tmp2.dir) |
2493 | + fs1 = filestore.FileStore.create(tmp1.dir) |
2494 | + fs2 = filestore.FileStore.create(tmp2.dir) |
2495 | |
2496 | with self.assertRaises(TypeError) as cm: |
2497 | fs.copy(ID) |
2498 | |
2499 | === modified file 'filestore/tests/run.py' |
2500 | --- filestore/tests/run.py 2013-02-22 02:14:23 +0000 |
2501 | +++ filestore/tests/run.py 2013-05-15 00:48:33 +0000 |
2502 | @@ -22,6 +22,7 @@ |
2503 | """ |
2504 | Run `filestore` unit tests. |
2505 | """ |
2506 | + |
2507 | import sys |
2508 | from os import path |
2509 | from unittest import TestLoader, TextTestRunner |
2510 | @@ -34,9 +35,12 @@ |
2511 | 'filestore', |
2512 | 'filestore.protocols', |
2513 | 'filestore.misc', |
2514 | + 'filestore.migration', |
2515 | 'filestore.tests', |
2516 | + 'filestore.tests.run', |
2517 | 'filestore.tests.test_protocols', |
2518 | - 'filestore.tests.test_misc' |
2519 | + 'filestore.tests.test_misc', |
2520 | + 'filestore.tests.test_migration', |
2521 | ) |
2522 | |
2523 | |
2524 | |
2525 | === added file 'filestore/tests/test_migration.py' |
2526 | --- filestore/tests/test_migration.py 1970-01-01 00:00:00 +0000 |
2527 | +++ filestore/tests/test_migration.py 2013-05-15 00:48:33 +0000 |
2528 | @@ -0,0 +1,548 @@ |
2529 | +# filestore: dmedia hashing protocol and file layout |
2530 | +# Copyright (C) 2013 Novacut Inc |
2531 | +# |
2532 | +# This file is part of `filestore`. |
2533 | +# |
2534 | +# `filestore` is free software: you can redistribute it and/or modify it under |
2535 | +# the terms of the GNU Affero General Public License as published by the Free |
2536 | +# Software Foundation, either version 3 of the License, or (at your option) any |
2537 | +# later version. |
2538 | +# |
2539 | +# `filestore` is distributed in the hope that it will be useful, but WITHOUT ANY |
2540 | +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
2541 | +# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
2542 | +# details. |
2543 | +# |
2544 | +# You should have received a copy of the GNU Affero General Public License along |
2545 | +# with `filestore`. If not, see <http://www.gnu.org/licenses/>. |
2546 | +# |
2547 | +# Authors: |
2548 | +# Jason Gerard DeRose <jderose@novacut.com> |
2549 | + |
2550 | +""" |
2551 | +Unit tests for the `filestore.migration` module. |
2552 | +""" |
2553 | + |
2554 | +from unittest import TestCase |
2555 | +import os |
2556 | +from os import path |
2557 | +import stat |
2558 | +import time |
2559 | +import json |
2560 | +import shutil |
2561 | + |
2562 | +import dbase32 |
2563 | +from dbase32 import rfc3548 |
2564 | + |
2565 | +import filestore |
2566 | +from filestore import migration, misc |
2567 | + |
2568 | +from . import TempDir |
2569 | + |
2570 | + |
2571 | +class TestConstants(TestCase): |
2572 | + def test_B32ALPHABET(self): |
2573 | + self.assertIsInstance(migration.B32ALPHABET, str) |
2574 | + self.assertEqual(len(migration.B32ALPHABET), 32) |
2575 | + self.assertEqual(len(set(migration.B32ALPHABET)), 32) |
2576 | + self.assertEqual(migration.B32ALPHABET, |
2577 | + ''.join(sorted(migration.B32ALPHABET)) |
2578 | + ) |
2579 | + self.assertEqual(migration.B32ALPHABET, |
2580 | + ''.join(sorted(rfc3548.B32_FORWARD)) |
2581 | + ) |
2582 | + |
2583 | + def test_B32NAMES(self): |
2584 | + self.assertIsInstance(migration.B32NAMES, tuple) |
2585 | + self.assertEqual(len(migration.B32NAMES), 1024) |
2586 | + self.assertEqual(len(set(migration.B32NAMES)), 1024) |
2587 | + self.assertEqual(migration.B32NAMES, |
2588 | + tuple(sorted(migration.B32NAMES)) |
2589 | + ) |
2590 | + for name in migration.B32NAMES: |
2591 | + self.assertIsInstance(name, str) |
2592 | + self.assertEqual(len(name), 2) |
2593 | + self.assertTrue(name.isupper() or name.isnumeric()) |
2594 | + self.assertTrue(set(name).issubset(migration.B32ALPHABET)) |
2595 | + |
2596 | + def test_NAMES_DIFF(self): |
2597 | + self.assertIsInstance(migration.NAMES_DIFF, tuple) |
2598 | + self.assertEqual(len(migration.NAMES_DIFF), 124) |
2599 | + self.assertEqual(len(set(migration.NAMES_DIFF)), 124) |
2600 | + self.assertEqual(migration.NAMES_DIFF, |
2601 | + tuple(sorted(migration.NAMES_DIFF)) |
2602 | + ) |
2603 | + for name in migration.NAMES_DIFF: |
2604 | + self.assertIsInstance(name, str) |
2605 | + self.assertEqual(len(name), 2) |
2606 | + self.assertTrue(name.isupper() or name.isnumeric()) |
2607 | + self.assertIn(name, migration.B32NAMES) |
2608 | + self.assertNotIn(name, filestore.DB32NAMES) |
2609 | + |
2610 | + |
2611 | +class TestFunctions(TestCase): |
2612 | + def test_is_v0_files(self): |
2613 | + v0 = TempDir() |
2614 | + for name in migration.B32NAMES: |
2615 | + v0.mkdir(name) |
2616 | + self.assertIs(migration.is_v0_files(v0.dir), True) |
2617 | + v1 = TempDir() |
2618 | + for name in filestore.DB32NAMES: |
2619 | + v1.mkdir(name) |
2620 | + self.assertIs(migration.is_v0_files(v1.dir), False) |
2621 | + tmp = TempDir() |
2622 | + fs = filestore.FileStore.create(tmp.dir) |
2623 | + files = tmp.join('.dmedia', 'files') |
2624 | + self.assertTrue(path.isdir(files)) |
2625 | + self.assertIs(migration.is_v0_files(files), False) |
2626 | + |
2627 | + def test_b32_to_db32(self): |
2628 | + for i in range(100): |
2629 | + data = os.urandom(15) |
2630 | + b32 = rfc3548.b32enc(data) |
2631 | + db32 = dbase32.db32enc(data) |
2632 | + self.assertEqual(migration.b32_to_db32(b32), db32) |
2633 | + |
2634 | + def test_v0_doc_to_v1(self): |
2635 | + doc = { |
2636 | + "_id": "4RNPIS23VFWZIOZDTPNHHENA", |
2637 | + "copies": 1, |
2638 | + "plugin": "filestore", |
2639 | + "time": 1359461597.704514, |
2640 | + "type": "dmedia/store" |
2641 | + } |
2642 | + self.assertEqual(migration.v0_doc_to_v1(doc), { |
2643 | + "_id": "VKGIBLTUO8PSBHS6MIGAA7G3", |
2644 | + "copies": 1, |
2645 | + "plugin": "filestore", |
2646 | + "time": 1359461597.704514, |
2647 | + "type": "dmedia/store" |
2648 | + }) |
2649 | + |
2650 | + |
2651 | +class TestMigration(TestCase): |
2652 | + def check_dir(self, fullname): |
2653 | + self.assertEqual(path.abspath(fullname), fullname) |
2654 | + st = os.stat(fullname, follow_symlinks=False) |
2655 | + self.assertTrue(stat.S_ISDIR(st.st_mode), fullname) |
2656 | + self.assertEqual(stat.S_IMODE(st.st_mode), 0o777, fullname) |
2657 | + |
2658 | + def check_file(self, fullname): |
2659 | + self.assertEqual(path.abspath(fullname), fullname) |
2660 | + st = os.stat(fullname, follow_symlinks=False) |
2661 | + self.assertTrue(stat.S_ISREG(st.st_mode), fullname) |
2662 | + self.assertEqual(stat.S_IMODE(st.st_mode), 0o444, fullname) |
2663 | + |
2664 | + def test_init(self): |
2665 | + tmp = TempDir() |
2666 | + m = migration.Migration(tmp.dir) |
2667 | + self.assertEqual(m.parentdir, tmp.dir) |
2668 | + self.assertEqual(m.basedir, tmp.join(filestore.DOTNAME)) |
2669 | + self.assertEqual(m.files, tmp.join(filestore.DOTNAME, 'files')) |
2670 | + self.assertEqual(m.files0, tmp.join(filestore.DOTNAME, 'files0')) |
2671 | + self.assertEqual(m.store, tmp.join(filestore.DOTNAME, 'store.json')) |
2672 | + self.assertEqual(m.store0, tmp.join(filestore.DOTNAME, 'store0.json')) |
2673 | + self.assertEqual(m.filestore, tmp.join(filestore.DOTNAME, 'filestore.json')) |
2674 | + |
2675 | + def test_needs_migration(self): |
2676 | + tmp = TempDir() |
2677 | + m = migration.Migration(tmp.dir) |
2678 | + self.assertIs(m.needs_migration(), False) |
2679 | + tmp.mkdir(filestore.DOTNAME) |
2680 | + self.assertFalse(path.exists(m.filestore)) |
2681 | + self.assertFalse(path.exists(m.files)) |
2682 | + self.assertIs(m.needs_migration(), False) |
2683 | + self.check_file(m.filestore) |
2684 | + self.check_dir(m.files) |
2685 | + |
2686 | + # When there was a V0 files/ layout: |
2687 | + tmp = TempDir() |
2688 | + tmp.mkdir(filestore.DOTNAME) |
2689 | + m = migration.Migration(tmp.dir) |
2690 | + m.create_v0_files() |
2691 | + self.assertFalse(path.exists(m.filestore)) |
2692 | + self.assertFalse(path.exists(m.files0)) |
2693 | + self.assertIs(m.needs_migration(), True) |
2694 | + self.check_file(m.filestore) |
2695 | + self.check_dir(m.files0) |
2696 | + self.check_dir(m.files) |
2697 | + |
2698 | + # Again, after "soft" migration was already done: |
2699 | + self.assertIs(m.needs_migration(), True) |
2700 | + self.check_file(m.filestore) |
2701 | + self.check_dir(m.files0) |
2702 | + self.check_dir(m.files) |
2703 | + |
2704 | + # Remove files0, one more test: |
2705 | + shutil.rmtree(m.files0) |
2706 | + self.assertIs(m.needs_migration(), False) |
2707 | + self.check_file(m.filestore) |
2708 | + self.assertFalse(path.exists(m.files0)) |
2709 | + self.check_dir(m.files) |
2710 | + |
2711 | + def test_build_v0_simulation(self): |
2712 | + V0 = misc.load_data('V0') |
2713 | + V1 = misc.load_data('V1') |
2714 | + tmp = TempDir() |
2715 | + m = migration.Migration(tmp.dir) |
2716 | + doc = m.build_v0_simulation() |
2717 | + self.assertIsInstance(doc, dict) |
2718 | + old_id = doc['_id'] |
2719 | + |
2720 | + # First run, before rehashing has been done: |
2721 | + id_map1 = {} |
2722 | + ch_map = {} |
2723 | + for (v0_id, v1_id, v1_ch) in m: |
2724 | + id_map1[v0_id] = v1_id |
2725 | + self.assertIsInstance(v1_ch, filestore.ContentHash) |
2726 | + self.assertEqual(v1_ch.id, v1_id) |
2727 | + ch_map[v1_id] = v1_ch |
2728 | + self.assertEqual(len(id_map1), 6) |
2729 | + self.assertEqual(len(ch_map), 6) |
2730 | + self.assertEqual(len(set(id_map1.values())), 6) |
2731 | + for (key, v0_id) in V0['root_hashes'].items(): |
2732 | + self.assertEqual(id_map1[v0_id], V1['root_hashes'][key]) |
2733 | + |
2734 | + # Second run, after ID mapping via our "broken" symlinks is in place: |
2735 | + id_map2 = {} |
2736 | + for (v0_id, v1_id, v1_ch) in m: |
2737 | + id_map2[v0_id] = v1_id |
2738 | + self.assertIsNone(v1_ch) |
2739 | + self.assertEqual(id_map2, id_map1) |
2740 | + |
2741 | + # See if we get a valid V1 FileStore layout: |
2742 | + fs = filestore.FileStore(tmp.dir) |
2743 | + self.assertEqual(migration.b32_to_db32(old_id), fs.id) |
2744 | + fs.check_layout() |
2745 | + ids = [st.id for st in fs] |
2746 | + self.assertEqual(sorted(V1['root_hashes'].values()), ids) |
2747 | + self.assertEqual(sorted(ch_map.keys()), ids) |
2748 | + for (_id, ch) in ch_map.items(): |
2749 | + self.assertEqual(fs.verify(_id), ch) |
2750 | + |
2751 | + def test_join(self): |
2752 | + tmp = TempDir() |
2753 | + m = migration.Migration(tmp.dir) |
2754 | + |
2755 | + # Cannot have empty parts: |
2756 | + with self.assertRaises(ValueError) as cm: |
2757 | + m.join() |
2758 | + self.assertEqual(str(cm.exception), 'Cannot have empty parts') |
2759 | + |
2760 | + part1 = dbase32.random_id() |
2761 | + self.assertEqual(m.join(part1), |
2762 | + tmp.join(filestore.DOTNAME, part1) |
2763 | + ) |
2764 | + part2 = dbase32.random_id() |
2765 | + self.assertEqual(m.join(part1, part2), |
2766 | + tmp.join(filestore.DOTNAME, part1, part2) |
2767 | + ) |
2768 | + part3 = dbase32.random_id() |
2769 | + self.assertEqual(m.join(part1, part2, part3), |
2770 | + tmp.join(filestore.DOTNAME, part1, part2, part3) |
2771 | + ) |
2772 | + |
2773 | + def test_mkdir(self): |
2774 | + tmp = TempDir() |
2775 | + m = migration.Migration(tmp.dir) |
2776 | + part1 = dbase32.random_id() |
2777 | + part2 = dbase32.random_id() |
2778 | + |
2779 | + # It shouldn't create basedir |
2780 | + with self.assertRaises(FileNotFoundError) as cm: |
2781 | + m.mkdir(part1) |
2782 | + self.assertEqual(cm.exception.filename, |
2783 | + tmp.join(filestore.DOTNAME, part1) |
2784 | + ) |
2785 | + tmp.mkdir(filestore.DOTNAME) |
2786 | + |
2787 | + # Cannot have empty parts: |
2788 | + with self.assertRaises(ValueError) as cm: |
2789 | + m.mkdir() |
2790 | + self.assertEqual(str(cm.exception), 'Cannot have empty parts') |
2791 | + |
2792 | + # Test with 1st dir: |
2793 | + d1 = m.mkdir(part1) |
2794 | + self.assertEqual(d1, tmp.join(filestore.DOTNAME, part1)) |
2795 | + self.check_dir(d1) |
2796 | + with self.assertRaises(FileExistsError) as cm: |
2797 | + m.mkdir(part1) |
2798 | + self.assertEqual(cm.exception.filename, d1) |
2799 | + |
2800 | + # Test with 2nd dir: |
2801 | + d2 = m.mkdir(part1, part2) |
2802 | + self.assertEqual(d2, tmp.join(filestore.DOTNAME, part1, part2)) |
2803 | + self.check_dir(d2) |
2804 | + with self.assertRaises(FileExistsError) as cm: |
2805 | + m.mkdir(part1, part2) |
2806 | + self.assertEqual(cm.exception.filename, d2) |
2807 | + |
2808 | + def test_write(self): |
2809 | + doc = { |
2810 | + '_id': dbase32.random_id(), |
2811 | + 'time': time.time(), |
2812 | + 'type': 'dmedia/store', |
2813 | + 'plugin': 'filestore', |
2814 | + 'copies': 1, |
2815 | + } |
2816 | + text = json.dumps(doc) |
2817 | + name = dbase32.random_id(10).lower() + '.json' |
2818 | + tmp = TempDir() |
2819 | + m = migration.Migration(tmp.dir) |
2820 | + |
2821 | + # It shouldn't create basedir |
2822 | + with self.assertRaises(FileNotFoundError) as cm: |
2823 | + m.write(name, text) |
2824 | + self.assertEqual(cm.exception.filename, |
2825 | + tmp.join(filestore.DOTNAME, name) |
2826 | + ) |
2827 | + tmp.mkdir(filestore.DOTNAME) |
2828 | + |
2829 | + # Should work when file doesn't already exist: |
2830 | + f = m.write(name, text) |
2831 | + self.assertEqual(f, tmp.join(filestore.DOTNAME, name)) |
2832 | + self.check_file(f) |
2833 | + self.assertEqual(open(f, 'r').read(), text) |
2834 | + |
2835 | + # But fail when the file does exists: |
2836 | + with self.assertRaises(FileExistsError) as cm: |
2837 | + m.write(name, 'different text') |
2838 | + self.assertEqual(cm.exception.filename, f) |
2839 | + self.check_file(f) |
2840 | + self.assertEqual(open(f, 'r').read(), text) |
2841 | + |
2842 | + def test_load_v0_doc(self): |
2843 | + doc = { |
2844 | + '_id': rfc3548.random_id(), |
2845 | + 'time': time.time(), |
2846 | + 'type': 'dmedia/store', |
2847 | + 'plugin': 'filestore', |
2848 | + 'copies': 1, |
2849 | + } |
2850 | + doc0 = { |
2851 | + '_id': rfc3548.random_id(), |
2852 | + 'time': time.time(), |
2853 | + 'type': 'dmedia/store', |
2854 | + 'plugin': 'filestore', |
2855 | + 'copies': 1, |
2856 | + } |
2857 | + |
2858 | + tmp = TempDir() |
2859 | + tmp.mkdir(filestore.DOTNAME) |
2860 | + m = migration.Migration(tmp.dir) |
2861 | + self.assertIsNone(m.load_v0_doc()) |
2862 | + |
2863 | + # Should load store.json if it is the only one presest: |
2864 | + m.write('store.json', json.dumps(doc)) |
2865 | + self.assertEqual(m.load_v0_doc(), doc) |
2866 | + |
2867 | + # But should load store0.json if *both* are presest: |
2868 | + m.write('store0.json', json.dumps(doc0)) |
2869 | + self.assertEqual(m.load_v0_doc(), doc0) |
2870 | + |
2871 | + # Likewise, should load store0.json if it is the only one presest: |
2872 | + tmp = TempDir() |
2873 | + tmp.mkdir(filestore.DOTNAME) |
2874 | + m = migration.Migration(tmp.dir) |
2875 | + self.assertIsNone(m.load_v0_doc()) |
2876 | + m.write('store0.json', json.dumps(doc0)) |
2877 | + self.assertEqual(m.load_v0_doc(), doc0) |
2878 | + |
2879 | + def test_get_v1_doc(self): |
2880 | + tmp = TempDir() |
2881 | + tmp.mkdir(filestore.DOTNAME) |
2882 | + m = migration.Migration(tmp.dir) |
2883 | + |
2884 | + new1 = m.get_v1_doc() |
2885 | + filestore.check_doc(new1) |
2886 | + new2 = m.get_v1_doc() |
2887 | + filestore.check_doc(new2) |
2888 | + self.assertNotEqual(new1['_id'], new2['_id']) |
2889 | + |
2890 | + # store.json, not isdb32(old_id): |
2891 | + tmp = TempDir() |
2892 | + tmp.mkdir(filestore.DOTNAME) |
2893 | + m = migration.Migration(tmp.dir) |
2894 | + _id = 'Z' * 24 # Z is not in DB32ALPHABET |
2895 | + ts = time.time() |
2896 | + old = {'_id': _id, 'time': ts, 'copies': 1} |
2897 | + m.write('store.json', json.dumps(old)) |
2898 | + new = m.get_v1_doc() |
2899 | + filestore.check_doc(new) |
2900 | + self.assertEqual(new, { |
2901 | + '_id': 'S' * 24, |
2902 | + 'time': ts, |
2903 | + 'type': 'dmedia/store', |
2904 | + 'plugin': 'filestore', |
2905 | + 'copies': 1, |
2906 | + }) |
2907 | + |
2908 | + # store.json, old['migrated'] is True: |
2909 | + tmp = TempDir() |
2910 | + tmp.mkdir(filestore.DOTNAME) |
2911 | + m = migration.Migration(tmp.dir) |
2912 | + _id = 'A' * 24 # A is in both DB32ALPHABET and B32ALPHABET |
2913 | + ts = time.time() |
2914 | + old = {'_id': _id, 'time': ts, 'copies': 2, 'migrated': True} |
2915 | + m.write('store.json', json.dumps(old)) |
2916 | + new = m.get_v1_doc() |
2917 | + filestore.check_doc(new) |
2918 | + self.assertEqual(new, { |
2919 | + '_id': _id, |
2920 | + 'time': ts, |
2921 | + 'type': 'dmedia/store', |
2922 | + 'plugin': 'filestore', |
2923 | + 'copies': 2, |
2924 | + }) |
2925 | + |
2926 | + # store.json, not isb32(old_id): |
2927 | + tmp = TempDir() |
2928 | + tmp.mkdir(filestore.DOTNAME) |
2929 | + m = migration.Migration(tmp.dir) |
2930 | + _id = '9' * 24 # 9 is not in B32ALPHABET |
2931 | + ts = time.time() |
2932 | + old = {'_id': _id, 'time': ts, 'copies': 0} |
2933 | + m.write('store.json', json.dumps(old)) |
2934 | + new = m.get_v1_doc() |
2935 | + filestore.check_doc(new) |
2936 | + self.assertEqual(new, { |
2937 | + '_id': _id, |
2938 | + 'time': ts, |
2939 | + 'type': 'dmedia/store', |
2940 | + 'plugin': 'filestore', |
2941 | + 'copies': 0, |
2942 | + }) |
2943 | + |
2944 | + # store.json, ambigous last case: |
2945 | + tmp = TempDir() |
2946 | + tmp.mkdir(filestore.DOTNAME) |
2947 | + m = migration.Migration(tmp.dir) |
2948 | + _id = 'A' * 24 # A is in both DB32ALPHABET and B32ALPHABET |
2949 | + ts = time.time() |
2950 | + old = {'_id': _id, 'time': ts, 'copies': 1} |
2951 | + m.write('store.json', json.dumps(old)) |
2952 | + new = m.get_v1_doc() |
2953 | + filestore.check_doc(new) |
2954 | + self.assertEqual(new, { |
2955 | + '_id': '3' * 24, |
2956 | + 'time': ts, |
2957 | + 'type': 'dmedia/store', |
2958 | + 'plugin': 'filestore', |
2959 | + 'copies': 1, |
2960 | + }) |
2961 | + |
2962 | + # store0.json is always interpreted as a V0, Base32 doc: |
2963 | + tmp = TempDir() |
2964 | + tmp.mkdir(filestore.DOTNAME) |
2965 | + m = migration.Migration(tmp.dir) |
2966 | + v0_id = rfc3548.random_id() |
2967 | + v1_id = migration.b32_to_db32(v0_id) |
2968 | + self.assertNotEqual(v0_id, v1_id) |
2969 | + ts = time.time() |
2970 | + old = {'_id': v0_id, 'time': ts, 'copies': 1} |
2971 | + m.write('store0.json', json.dumps(old)) |
2972 | + new = m.get_v1_doc() |
2973 | + filestore.check_doc(new) |
2974 | + self.assertEqual(new, { |
2975 | + '_id': v1_id, |
2976 | + 'time': ts, |
2977 | + 'type': 'dmedia/store', |
2978 | + 'plugin': 'filestore', |
2979 | + 'copies': 1, |
2980 | + }) |
2981 | + |
2982 | + # store0.json always takes precedence over store.json: |
2983 | + _id = '8' * 24 # 8 is not in B32ALPHABET |
2984 | + old = {'_id': _id, 'time': 1234.5678, 'copies': 1} |
2985 | + m.write('store.json', json.dumps(old)) |
2986 | + self.assertEqual(m.get_v1_doc(), new) |
2987 | + |
2988 | + def test_v0_path(self): |
2989 | + tmp = TempDir() |
2990 | + m = migration.Migration(tmp.dir) |
2991 | + for i in range(100): |
2992 | + _id = rfc3548.random_id(30) |
2993 | + self.assertEqual(m.v0_path(_id), |
2994 | + tmp.join(filestore.DOTNAME, 'files0', _id[:2], _id[2:]) |
2995 | + ) |
2996 | + |
2997 | + def test_v1_path(self): |
2998 | + tmp = TempDir() |
2999 | + m = migration.Migration(tmp.dir) |
3000 | + for i in range(100): |
3001 | + _id = dbase32.random_id(30) |
3002 | + self.assertEqual(m.v1_path(_id), |
3003 | + tmp.join(filestore.DOTNAME, 'files', _id[:2], _id[2:]) |
3004 | + ) |
3005 | + |
3006 | + def test_create_v0_files(self): |
3007 | + tmp = TempDir() |
3008 | + m = migration.Migration(tmp.dir) |
3009 | + |
3010 | + # It shouldn't create basedir |
3011 | + with self.assertRaises(FileNotFoundError) as cm: |
3012 | + m.create_v0_files() |
3013 | + self.assertEqual(cm.exception.filename, |
3014 | + tmp.join(filestore.DOTNAME, 'files') |
3015 | + ) |
3016 | + tmp.mkdir(filestore.DOTNAME) |
3017 | + |
3018 | + # Now it should work: |
3019 | + self.assertIsNone(m.create_v0_files()) |
3020 | + files = tmp.join(filestore.DOTNAME, 'files') |
3021 | + self.check_dir(files) |
3022 | + for name in migration.B32NAMES: |
3023 | + self.check_dir(path.join(files, name)) |
3024 | + self.assertEqual(sorted(os.listdir(files)), list(migration.B32NAMES)) |
3025 | + |
3026 | + # Should fail if .dmedia/files/ already exists: |
3027 | + with self.assertRaises(FileExistsError) as cm: |
3028 | + m.create_v0_files() |
3029 | + self.assertEqual(cm.exception.filename, files) |
3030 | + |
3031 | + def test_create_v1_files(self): |
3032 | + tmp = TempDir() |
3033 | + m = migration.Migration(tmp.dir) |
3034 | + |
3035 | + # It shouldn't create basedir |
3036 | + with self.assertRaises(FileNotFoundError) as cm: |
3037 | + m.create_v1_files() |
3038 | + self.assertEqual(cm.exception.filename, |
3039 | + tmp.join(filestore.DOTNAME, 'files') |
3040 | + ) |
3041 | + tmp.mkdir(filestore.DOTNAME) |
3042 | + |
3043 | + # Now it should work: |
3044 | + self.assertIsNone(m.create_v1_files()) |
3045 | + files = tmp.join(filestore.DOTNAME, 'files') |
3046 | + self.check_dir(files) |
3047 | + for name in filestore.DB32NAMES: |
3048 | + self.check_dir(path.join(files, name)) |
3049 | + self.assertEqual(sorted(os.listdir(files)), list(filestore.DB32NAMES)) |
3050 | + |
3051 | + # Should fail if .dmedia/files/ already exists: |
3052 | + with self.assertRaises(FileExistsError) as cm: |
3053 | + m.create_v1_files() |
3054 | + self.assertEqual(cm.exception.filename, files) |
3055 | + |
3056 | + def test_move_v0_files_to_files0(self): |
3057 | + tmp = TempDir() |
3058 | + tmp.mkdir(filestore.DOTNAME) |
3059 | + m = migration.Migration(tmp.dir) |
3060 | + m.create_v0_files() |
3061 | + self.assertIsNone(m.move_v0_files_to_files0()) |
3062 | + |
3063 | + # Check files/ |
3064 | + files = tmp.join(filestore.DOTNAME, 'files') |
3065 | + self.check_dir(files) |
3066 | + for name in filestore.DB32NAMES: |
3067 | + self.check_dir(path.join(files, name)) |
3068 | + self.assertEqual(sorted(os.listdir(files)), list(filestore.DB32NAMES)) |
3069 | + |
3070 | + # Check files0/ |
3071 | + files0 = tmp.join(filestore.DOTNAME, 'files0') |
3072 | + self.check_dir(files0) |
3073 | + for name in migration.B32NAMES: |
3074 | + self.check_dir(path.join(files0, name)) |
3075 | + self.assertEqual(sorted(os.listdir(files0)), list(migration.B32NAMES)) |
3076 | + |
3077 | |
3078 | === modified file 'filestore/tests/test_misc.py' |
3079 | --- filestore/tests/test_misc.py 2013-04-28 17:02:28 +0000 |
3080 | +++ filestore/tests/test_misc.py 2013-05-15 00:48:33 +0000 |
3081 | @@ -30,7 +30,7 @@ |
3082 | from hashlib import md5 |
3083 | from skein import skein512 |
3084 | |
3085 | -from dbase32 import db32enc, db32dec |
3086 | +from dbase32 import db32enc, db32dec, isdb32, random_id, RANDOM_B32LEN |
3087 | from dbase32.rfc3548 import b32enc, b32dec |
3088 | |
3089 | from . import TempDir |
3090 | @@ -250,24 +250,91 @@ |
3091 | |
3092 | |
3093 | class TestTempFileStore(TestCase): |
3094 | + def test_create(self): |
3095 | + """ |
3096 | + Test the TempFileStore.create() classmethod. |
3097 | + """ |
3098 | + fs = misc.TempFileStore.create() |
3099 | + self.assertIsInstance(fs, filestore.FileStore) |
3100 | + self.assertIsInstance(fs, misc.TempFileStore) |
3101 | + self.assertTrue(fs.parentdir.startswith('/tmp/TempFileStore.')) |
3102 | + self.assertIsInstance(fs.id, str) |
3103 | + self.assertEqual(len(fs.id), RANDOM_B32LEN) |
3104 | + self.assertTrue(isdb32(fs.id)) |
3105 | + self.assertIsInstance(fs.copies, int) |
3106 | + self.assertEqual(fs.copies, 1) |
3107 | + |
3108 | + store_id = random_id() |
3109 | + fs = misc.TempFileStore.create(store_id, 2) |
3110 | + self.assertIsInstance(fs, filestore.FileStore) |
3111 | + self.assertIsInstance(fs, misc.TempFileStore) |
3112 | + self.assertTrue(fs.parentdir.startswith('/tmp/TempFileStore.')) |
3113 | + self.assertIsInstance(fs.id, str) |
3114 | + self.assertEqual(len(fs.id), RANDOM_B32LEN) |
3115 | + self.assertTrue(isdb32(fs.id)) |
3116 | + self.assertEqual(fs.id, store_id) |
3117 | + self.assertIsInstance(fs.copies, int) |
3118 | + self.assertEqual(fs.copies, 2) |
3119 | + |
3120 | + label = 'Dmedia' + random_id(10).lower() |
3121 | + fs = misc.TempFileStore.create(label=label) |
3122 | + self.assertIsInstance(fs, filestore.FileStore) |
3123 | + self.assertIsInstance(fs, misc.TempFileStore) |
3124 | + self.assertTrue(fs.parentdir.startswith('/tmp/TempFileStore.')) |
3125 | + self.assertIsInstance(fs.id, str) |
3126 | + self.assertEqual(len(fs.id), RANDOM_B32LEN) |
3127 | + self.assertTrue(isdb32(fs.id)) |
3128 | + self.assertIsInstance(fs.copies, int) |
3129 | + self.assertEqual(fs.copies, 1) |
3130 | + self.assertEqual(fs.doc['label'], label) |
3131 | + |
3132 | def test_init(self): |
3133 | fs = misc.TempFileStore() |
3134 | self.assertIsInstance(fs, filestore.FileStore) |
3135 | self.assertTrue(fs.parentdir.startswith('/tmp/TempFileStore.')) |
3136 | - self.assertIsNone(fs.id) |
3137 | - self.assertEqual(fs.copies, 0) |
3138 | + self.assertIsInstance(fs.id, str) |
3139 | + self.assertEqual(len(fs.id), RANDOM_B32LEN) |
3140 | + self.assertTrue(isdb32(fs.id)) |
3141 | + self.assertIsInstance(fs.copies, int) |
3142 | + self.assertEqual(fs.copies, 1) |
3143 | |
3144 | - fs = misc.TempFileStore('foobar', 2) |
3145 | + store_id = random_id() |
3146 | + fs = misc.TempFileStore(store_id, 2) |
3147 | self.assertIsInstance(fs, filestore.FileStore) |
3148 | self.assertTrue(fs.parentdir.startswith('/tmp/TempFileStore.')) |
3149 | - self.assertEqual(fs.id, 'foobar') |
3150 | + self.assertIsInstance(fs.id, str) |
3151 | + self.assertEqual(len(fs.id), RANDOM_B32LEN) |
3152 | + self.assertTrue(isdb32(fs.id)) |
3153 | + self.assertEqual(fs.id, store_id) |
3154 | + self.assertIsInstance(fs.copies, int) |
3155 | self.assertEqual(fs.copies, 2) |
3156 | |
3157 | - fs = misc.TempFileStore(_id='hooray', copies=3) |
3158 | + label = 'Dmedia' + random_id(10).lower() |
3159 | + fs = misc.TempFileStore(label=label) |
3160 | self.assertIsInstance(fs, filestore.FileStore) |
3161 | self.assertTrue(fs.parentdir.startswith('/tmp/TempFileStore.')) |
3162 | - self.assertEqual(fs.id, 'hooray') |
3163 | - self.assertEqual(fs.copies, 3) |
3164 | + self.assertIsInstance(fs.id, str) |
3165 | + self.assertEqual(len(fs.id), RANDOM_B32LEN) |
3166 | + self.assertTrue(isdb32(fs.id)) |
3167 | + self.assertIsInstance(fs.copies, int) |
3168 | + self.assertEqual(fs.copies, 1) |
3169 | + self.assertEqual(fs.doc['label'], label) |
3170 | + |
3171 | + def test_repr(self): |
3172 | + # With auto-generated ID: |
3173 | + fs = misc.TempFileStore() |
3174 | + self.assertTrue(isdb32(fs.id) and len(fs.id) == 24) |
3175 | + self.assertEqual(repr(fs), |
3176 | + 'TempFileStore({!r}, {!r})'.format(fs.parentdir, fs.id) |
3177 | + ) |
3178 | + |
3179 | + # With explicit ID: |
3180 | + store_id = random_id() |
3181 | + fs = misc.TempFileStore(store_id) |
3182 | + self.assertEqual(fs.id, store_id) |
3183 | + self.assertEqual(repr(fs), |
3184 | + 'TempFileStore({!r}, {!r})'.format(fs.parentdir, fs.id) |
3185 | + ) |
3186 | |
3187 | def test_del(self): |
3188 | fs = misc.TempFileStore() |