Merge lp:~jderose/filestore/explicit-create into lp:filestore

Proposed by Jason Gerard DeRose
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
Reviewer Review Type Date Requested Status
James Raymond Approve
Review via email: mp+163821@code.launchpad.net

Description of the change

For background, see this bug:

  https://bugs.launchpad.net/filestore/+bug/1163002

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.__init__() and was kind of a mess. But as this gray area has been vanquished, it's now easy to isolate the upgrade functionality from FileStore.__init__().

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/jderose/BigDrive'
>>> m = Migration(parentdir)
>>> m.needs_upgrade() # Soft upgrade if needed, but wont "create" anything
>>> fs = FileStore(parentdir) # Assumes the file-store layout already exists
>>> fs = FileStore.create(parentdir) # The alternative assumption, it does not exist

If `Migration.needs_upgrade()` returns True, then you (likely) have files that need to be re-hashed in order to migrate them from the V0 to the V1 IDs, which you can do by running `dmedia-v0-v1-upgrade`. This can be very time consuming, but can be resumed from where you left off (and the intermediate result will even be synced between your devices).

Anyway, changes include:

* A file-store layout now must always be explicitly initialized, which is done using the new FileStore.create() classmethod:

   FileStore.create(parentdir, store_id=None, copies=1, **kw)

   If path.join(parentdir, '.dmedia') already exists, this method will fail loudly. A big difference between this and the old FileStore.init_dirs() is this classmethod assigns the FileStore an ID (which you can manually provide via *store_id*) and also writes out the "document" to a JSON file, which for an easier migration from V0 is now in 'filestore.json' instead of 'store.json'. So FileStore.create() is taking responsibility for this part rather than letting Dmedia handle it.

   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.__init__() will now fail if the file-store layout does *not* already exist, and has a slightly different signature:

   FileStore.__init__(parentdir, expected_id=None)

   Note that when "loading" an existing file-store, you can't actually change the ID. The optional `expected_id` kwarg tells FileStore.__init__() to *fail* if the ID in filestore.json doesn't match `expected_id`. This is mostly for subprocesses so that they have a more robust way to stay on the same page with the parent process in terms of the currently connect file-stores.

   Also note that previously you could sort of "override" the copies value, but now this can only be provided in FileStore.create(). FileStore.__init__() simply loads these values from filestore.json.

* Added the low-level `_create_filestore()` function that both `FileStore.create()` and `TempFileStore.__init__()` use:

   _create_filestore(parentdir, store_id, copies, **kw)

   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-<STORE_ID> => .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.__init__()

* 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

To post a comment you must log in.
Revision history for this message
James Raymond (jamesmr) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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()

Subscribers

People subscribed via source and target branches