Merge lp:~jderose/dmedia/simple-default into lp:dmedia

Proposed by Jason Gerard DeRose
Status: Merged
Merged at revision: 524
Proposed branch: lp:~jderose/dmedia/simple-default
Merge into: lp:dmedia
Diff against target: 668 lines (+233/-250)
8 files modified
debian/dmedia.postinst (+0/-10)
dmedia-cli (+3/-3)
dmedia-service (+6/-14)
dmedia/core.py (+66/-36)
dmedia/tests/base.py (+5/-0)
dmedia/tests/test_core.py (+118/-186)
dmedia/tests/test_util.py (+26/-1)
dmedia/util.py (+9/-0)
To merge this branch: bzr merge lp:~jderose/dmedia/simple-default
Reviewer Review Type Date Requested Status
James Raymond Approve
Review via email: mp+136073@code.launchpad.net

Description of the change

For background, see this bug:
https://bugs.launchpad.net/dmedia/+bug/1082864

Changes include:

 * Removed debian/dmedia.postinst script so /home/.dmedia FileStore is no longer created at package install

 * Reworked Core so it always has a default file store, doesn't use the shared store in /home/.dmedia anymore

 * Moved the private store from ~/.dmedia to ~/.local/share/dmedia/.dmedia

 * Removed SetDefaultStore() DBus method

 * Added Core.purge_store() implementation and test

 * Added PurgeStore() DBus method

 * Added quick and dirty migration functionality for the default store changes, which:

1) If ~/.dmedia exists, it is move to ~/.local/share/dmedia/.dmedia

2) If /home/.dmedia exists, all files there are moved (or copied if needed) into ~/.local/share/dmedia/.dmedia

3) Finally, calls Core.purge_store() to remove any references to copies in /home/.dmedia

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=== removed file 'debian/dmedia.postinst'
2--- debian/dmedia.postinst 2012-07-26 21:05:25 +0000
3+++ debian/dmedia.postinst 1970-01-01 00:00:00 +0000
4@@ -1,10 +0,0 @@
5-#!/bin/sh -e
6-
7-case $1 in
8- configure)
9- # Initalize shared file-store in /home
10- /usr/lib/dmedia/init-filestore /home
11-esac
12-
13-
14-#DEBHELPER#
15
16=== modified file 'dmedia-cli'
17--- dmedia-cli 2012-11-01 11:06:53 +0000
18+++ dmedia-cli 2012-11-26 00:24:26 +0000
19@@ -134,10 +134,10 @@
20 return [path.abspath(directory)]
21
22
23-class SetDefaultStore(_Method):
24- "Set default store to 'private', 'shared', or 'none'"
25+class PurgeStore(_Method):
26+ 'Purge references to a store'
27
28- args = ['value']
29+ args = ['store_id']
30
31
32 class Resolve(_Method):
33
34=== modified file 'dmedia-service'
35--- dmedia-service 2012-11-25 07:25:50 +0000
36+++ dmedia-service 2012-11-26 00:24:26 +0000
37@@ -181,11 +181,6 @@
38 self.snapshot_queue_in.put(dbname)
39 return True
40
41- @dbus.service.method(IFACE, in_signature='as', out_signature='')
42- def SnapshotMany(self, names):
43- for dbname in names:
44- self.Snapshot(dbname)
45-
46 @dbus.service.method(IFACE, in_signature='', out_signature='')
47 def SnapshotAll(self):
48 log.info('Dmedia.SnapshotAll()')
49@@ -209,9 +204,7 @@
50 log.info('Starting CouchDB took %.3f', time.time() - start)
51 self.core = Core(env)
52 self.core.load_identity(self.couch.machine, self.couch.user)
53- self.core.init_default_store()
54- if self.core.local.get('default_store') is None:
55- self.core.set_default_store('shared')
56+ self.core.load_default_filestore(self.couch.basedir)
57 self.env_s = dumps(self.core.env, pretty=True)
58 log.info('Finished core startup in %.3f', time.time() - start)
59 GObject.timeout_add(250, self.on_idle1)
60@@ -468,12 +461,11 @@
61 self.core.create_filestore(parentdir)
62 return self.LocalDmedia()
63
64- @dbus.service.method(IFACE, in_signature='s', out_signature='s')
65- def SetDefaultStore(self, value):
66- value = str(value)
67- log.info('Dmedia.SetDefaultStore(%r)', value)
68- self.core.set_default_store(value)
69- return self.LocalDmedia()
70+ @dbus.service.method(IFACE, in_signature='s', out_signature='')
71+ def PurgeStore(self, store_id):
72+ store_id = str(store_id)
73+ log.info('Dmedia.PurgeStore(%r)', store_id)
74+ start_thread(self.core.purge_store, store_id)
75
76 @dbus.service.method(IFACE, in_signature='s', out_signature='s')
77 def Resolve(self, _id):
78
79=== modified file 'dmedia/core.py'
80--- dmedia/core.py 2012-11-18 11:57:51 +0000
81+++ dmedia/core.py 2012-11-26 00:24:26 +0000
82@@ -42,7 +42,7 @@
83 from base64 import b64encode
84
85 from microfiber import Server, Database, NotFound, Conflict, BulkConflict
86-from filestore import FileStore, check_root_hash, check_id
87+from filestore import FileStore, check_root_hash, check_id, DOTNAME
88
89 import dmedia
90 from dmedia.parallel import start_thread, start_process
91@@ -54,8 +54,10 @@
92
93 log = logging.getLogger()
94 LOCAL_ID = '_local/dmedia'
95+HOME = path.abspath(os.environ['HOME'])
96+if not path.isdir(HOME):
97+ raise Exception('$HOME is not a directory: {!r}'.format(HOME))
98 SHARED = '/home'
99-PRIVATE = path.abspath(os.environ['HOME'])
100
101
102 def start_httpd(couch_env, ssl_config):
103@@ -185,18 +187,37 @@
104 log.exception('Error updating project stats for %r', project_id)
105
106
107+def migrate_shared(srcdir, dstdir):
108+ try:
109+ count = 0
110+ src = FileStore(srcdir)
111+ dst = FileStore(dstdir)
112+ log.info('Migrating files from %r to %r', src, dst)
113+ for st in src:
114+ if dst.exists(st.id):
115+ continue
116+ log.info('Migrating %s %s', st.id, st.size)
117+ try:
118+ os.rename(st.name, dst.path(st.id))
119+ except OSError:
120+ src.copy(st.id, dst)
121+ src.remove(st.id)
122+ count += 1
123+ log.info('Migrating %d files from %r to %r', count, src, dst)
124+ except Exception:
125+ log.exception('Error migrating files from shared FileStore')
126+ return count
127+
128+
129 class Core:
130- def __init__(self, env, private=None, shared=None):
131+ def __init__(self, env):
132 self.env = env
133- self._private = (PRIVATE if private is None else private)
134- self._shared = (SHARED if shared is None else shared)
135 self.db = util.get_db(env, init=True)
136 self.server = self.db.server()
137 self.ms = MetaStore(self.db)
138 self.stores = LocalStores()
139 self.queue = Queue()
140 self.thread = None
141- self.default = None
142 try:
143 self.local = self.db.get(LOCAL_ID)
144 except NotFound:
145@@ -230,23 +251,18 @@
146 self.local['user_id'] = user['_id']
147 self.save_local()
148
149- def init_default_store(self):
150- value = self.local.get('default_store')
151- if value not in ('private', 'shared'):
152- log.info('no default FileStore')
153- self.default = None
154- self._sync_stores()
155- return
156- if value == 'shared' and not util.isfilestore(self._shared):
157- log.warning('Switching to private, no shared FileStore at %r', self._shared)
158- value = 'private'
159- self.local['default_store'] = value
160- self.db.save(self.local)
161- parentdir = (self._private if value == 'private' else self._shared)
162+ def load_default_filestore(self, parentdir):
163+ if util.isfilestore(HOME) and not util.isfilestore(parentdir):
164+ src = FileStore(HOME)
165+ src.init_dirs()
166+ dstdir = path.join(parentdir, DOTNAME)
167+ log.info('Moving %r to %r', src.basedir, dstdir)
168+ os.rename(src.basedir, dstdir)
169+ self.parentdir = parentdir
170 (fs, doc) = util.init_filestore(parentdir)
171- self.default = fs
172- log.info('Connecting default FileStore %r at %r', fs.id, fs.parentdir)
173+ log.info('Default FileStore %s at %r', doc['_id'], parentdir)
174 self._add_filestore(fs, doc)
175+ return fs
176
177 def _sync_stores(self):
178 self.local['stores'] = self.stores.local_stores()
179@@ -270,6 +286,13 @@
180 self._sync_stores()
181
182 def _background_worker(self):
183+ if util.isfilestore(SHARED):
184+ log.info('Running migration...')
185+ process = start_process(migrate_shared, SHARED, self.parentdir)
186+ process.join()
187+ store_id = util.get_filestore_id(SHARED)
188+ if store_id is not None:
189+ self.purge_store(store_id)
190 log.info('Background worker listing to queue...')
191 while True:
192 try:
193@@ -327,19 +350,6 @@
194 def update_project(self, project_id):
195 update_project(self.db, project_id)
196
197- def set_default_store(self, value):
198- if value not in ('private', 'shared', 'none'):
199- raise ValueError(
200- "need 'private', 'shared', or 'none'; got {!r}".format(value)
201- )
202- if self.local.get('default_store') != value:
203- self.local['default_store'] = value
204- self.db.save(self.local)
205- if self.default is not None:
206- self.disconnect_filestore(self.default.parentdir, self.default.id)
207- self.default = None
208- self.init_default_store()
209-
210 def create_filestore(self, parentdir):
211 """
212 Create a new file-store in *parentdir*.
213@@ -412,16 +422,36 @@
214 * ``doc['partial'][store_id]``
215
216 Some scenarios in which you might want to do this:
217-
218+
219 1. The HDD was run over by a bus, the data is gone. We need to
220 embrace reality, the sooner the better.
221
222 2. We're going to format or otherwise repurpose an HDD. Ideally, we
223 would have called `Core2.downgrade_store()` first.
224-
225+
226 Note that this method makes sense for remote cloud stores as well as for
227 local file-stores
228 """
229+ log.info('Purging store %s', store_id)
230+ ids = []
231+ while True:
232+ rows = self.db.view('file', 'stored',
233+ key=store_id,
234+ include_docs=True,
235+ limit=25,
236+ )['rows']
237+ if not rows:
238+ break
239+ ids.extend(r['id'] for r in rows)
240+ docs = [r['doc'] for r in rows]
241+ for doc in docs:
242+ del doc['stored'][store_id]
243+ try:
244+ self.db.save_many(docs)
245+ except BulkConflict:
246+ log.exception('Conflict purging %s', store_id)
247+ log.info('Purged %d references to %s', len(ids), store_id)
248+ return ids
249
250 def stat(self, _id):
251 doc = self.db.get(_id)
252
253=== modified file 'dmedia/tests/base.py'
254--- dmedia/tests/base.py 2012-05-06 22:10:00 +0000
255+++ dmedia/tests/base.py 2012-11-26 00:24:26 +0000
256@@ -33,6 +33,7 @@
257 from random import SystemRandom
258 from zipfile import ZipFile
259
260+import filestore
261 from filestore import File, Leaf, ContentHash, Batch, Hasher, LEAF_SIZE
262 from filestore import scandir
263 from microfiber import random_id
264@@ -42,6 +43,10 @@
265 random = SystemRandom()
266
267
268+def random_file_id():
269+ return random_id(filestore.DIGEST_BYTES)
270+
271+
272 class DummyQueue(object):
273 def __init__(self):
274 self.items = []
275
276=== modified file 'dmedia/tests/test_core.py'
277--- dmedia/tests/test_core.py 2012-10-31 20:42:27 +0000
278+++ dmedia/tests/test_core.py 2012-11-26 00:24:26 +0000
279@@ -41,7 +41,7 @@
280 from dmedia import util, core
281
282 from .couch import CouchCase
283-from .base import TempDir
284+from .base import TempDir, random_file_id
285
286
287 class TestFunctions(TestCase):
288@@ -64,6 +64,32 @@
289 }
290 )
291
292+ def test_migrate_shared(self):
293+ tmp1 = TempDir()
294+ tmp2 = TempDir()
295+ src = filestore.FileStore(tmp1.dir)
296+ dst = filestore.FileStore(tmp2.dir)
297+ st_list = []
298+ for i in range(10):
299+ (file, ch) = tmp1.random_file()
300+ os.rename(file.name, src.path(ch.id))
301+ st = src.stat(ch.id)
302+ assert st.size == ch.file_size
303+ st_list.append(st)
304+ st_list.sort(key=lambda st: st.id)
305+ self.assertEqual(list(src), st_list)
306+ self.assertEqual(list(dst), [])
307+ self.assertEqual(core.migrate_shared(tmp1.dir, tmp2.dir), 10)
308+ self.assertEqual(list(src), [])
309+ self.assertEqual(
310+ [st.id for st in dst],
311+ [st.id for st in st_list]
312+ )
313+ for st in st_list:
314+ ch = dst.verify(st.id)
315+ self.assertEqual(st.size, ch.file_size)
316+ self.assertEqual(dst.stat(st.id).mtime, st.mtime)
317+
318
319 class TestCouchFunctions(CouchCase):
320 def test_projects_iter(self):
321@@ -135,191 +161,6 @@
322 }
323 )
324
325- def test_init_default_store(self):
326- private = TempDir()
327- shared = TempDir()
328- machine_id = random_id()
329-
330- # Test when default_store is missing
331- inst = core.Core(self.env, private.dir, shared.dir)
332- self.assertEqual(inst._private, private.dir)
333- self.assertEqual(inst._shared, shared.dir)
334- inst.init_default_store()
335- self.assertIsNone(inst.default)
336- self.assertEqual(inst.local,
337- {
338- '_id': '_local/dmedia',
339- 'stores': {},
340- }
341- )
342-
343- # Test when default_store is 'private'
344- inst = core.Core(self.env, private.dir, shared.dir)
345- self.assertEqual(inst._private, private.dir)
346- self.assertEqual(inst._shared, shared.dir)
347- inst.local['default_store'] = 'private'
348- self.assertFalse(util.isfilestore(private.dir))
349- inst.init_default_store()
350- self.assertEqual(
351- set(inst.local['stores']),
352- set([private.dir])
353- )
354- store_id = inst.local['stores'][private.dir]['id']
355- (fs1, doc) = util.get_filestore(private.dir, store_id)
356- self.assertEqual(inst.local,
357- {
358- '_id': '_local/dmedia',
359- '_rev': '0-1',
360- 'default_store': 'private',
361- 'stores': {
362- fs1.parentdir: {
363- 'id': fs1.id,
364- 'copies': fs1.copies,
365- }
366- }
367- }
368- )
369-
370- # Again test when default_store is 'private' to make sure local isn't
371- # updated needlessly
372- inst = core.Core(self.env, private.dir, shared.dir)
373- self.assertEqual(inst._private, private.dir)
374- self.assertEqual(inst._shared, shared.dir)
375- inst.init_default_store()
376- self.assertEqual(inst.local,
377- {
378- '_id': '_local/dmedia',
379- '_rev': '0-1',
380- 'default_store': 'private',
381- 'stores': {
382- fs1.parentdir: {
383- 'id': fs1.id,
384- 'copies': fs1.copies,
385- }
386- }
387- }
388- )
389-
390- # Test when default_store is 'shared' (which we're assuming exists)
391- self.assertFalse(util.isfilestore(shared.dir))
392- (fs2, doc) = util.init_filestore(shared.dir)
393- inst = core.Core(self.env, private.dir, shared.dir)
394- self.assertEqual(inst._private, private.dir)
395- self.assertEqual(inst._shared, shared.dir)
396- inst.local['default_store'] = 'shared'
397- inst.init_default_store()
398- self.assertEqual(inst.local,
399- {
400- '_id': '_local/dmedia',
401- '_rev': '0-2',
402- 'default_store': 'shared',
403- 'stores': {
404- fs2.parentdir: {
405- 'id': fs2.id,
406- 'copies': fs2.copies,
407- }
408- }
409- }
410- )
411-
412- # Test when default_store is 'shared' and it doesn't exist, in which
413- # case it should automatically switch to 'private' instead
414- nope = TempDir()
415- inst = core.Core(self.env, private.dir, nope.dir)
416- self.assertEqual(inst._private, private.dir)
417- self.assertEqual(inst._shared, nope.dir)
418- inst.local['default_store'] = 'shared'
419- inst.init_default_store()
420- self.assertEqual(inst.local,
421- {
422- '_id': '_local/dmedia',
423- '_rev': '0-4',
424- 'default_store': 'private',
425- 'stores': {
426- fs1.parentdir: {
427- 'id': fs1.id,
428- 'copies': fs1.copies,
429- }
430- }
431- }
432- )
433-
434- def test_set_default_store(self):
435- private = TempDir()
436- shared = TempDir()
437- (fs1, doc1) = util.init_filestore(private.dir)
438- (fs2, doc2) = util.init_filestore(shared.dir)
439- inst = core.Core(self.env, private.dir, shared.dir)
440- self.assertEqual(inst._private, private.dir)
441- self.assertEqual(inst._shared, shared.dir)
442-
443- with self.assertRaises(ValueError) as cm:
444- inst.set_default_store('foobar')
445- self.assertEqual(
446- str(cm.exception),
447- "need 'private', 'shared', or 'none'; got 'foobar'"
448- )
449- self.assertEqual(inst.local,
450- {
451- '_id': '_local/dmedia',
452- 'stores': {},
453- }
454- )
455-
456- # Test with 'private'
457- inst.set_default_store('private')
458- self.assertEqual(inst.local,
459- {
460- '_id': '_local/dmedia',
461- '_rev': '0-2',
462- 'default_store': 'private',
463- 'stores': {
464- fs1.parentdir: {'id': fs1.id, 'copies': 1},
465- },
466- }
467- )
468-
469- # Test with 'shared'
470- inst.set_default_store('shared')
471- self.assertEqual(inst.local,
472- {
473- '_id': '_local/dmedia',
474- '_rev': '0-5',
475- 'default_store': 'shared',
476- 'stores': {
477- fs2.parentdir: {'id': fs2.id, 'copies': 1},
478- },
479- }
480- )
481-
482- # Test with 'none'
483- inst.set_default_store('none')
484- self.assertEqual(inst.local,
485- {
486- '_id': '_local/dmedia',
487- '_rev': '0-7',
488- 'default_store': 'none',
489- 'stores': {},
490- }
491- )
492-
493- # Test with 'shared' when it doesn't exist
494- nope = TempDir()
495- inst = core.Core(self.env, private.dir, nope.dir)
496- self.assertEqual(inst._private, private.dir)
497- self.assertEqual(inst._shared, nope.dir)
498- inst.set_default_store('shared')
499- self.assertEqual(inst.local,
500- {
501- '_id': '_local/dmedia',
502- '_rev': '0-10',
503- 'default_store': 'private',
504- 'stores': {
505- fs1.parentdir: {'id': fs1.id, 'copies': 1},
506- },
507- }
508- )
509-
510 def test_create_filestore(self):
511 inst = core.Core(self.env)
512
513@@ -502,6 +343,97 @@
514 inst.disconnect_filestore(fs1.parentdir, fs1.id)
515 self.assertEqual(str(cm.exception), repr(fs1.parentdir))
516
517+ def test_purge_store(self):
518+ store_id1 = random_id()
519+ store_id2 = random_id()
520+ store_id3 = random_id()
521+ inst = core.Core(self.env)
522+ db = inst.db
523+
524+ # Test when empty
525+ self.assertEqual(inst.purge_store(store_id1), [])
526+
527+ docs = [
528+ {
529+ '_id': random_file_id(),
530+ 'type': 'dmedia/file',
531+ 'bytes': 1776,
532+ 'stored': {
533+ store_id1: {
534+ 'copies': 1,
535+ 'mtime': 1234567890,
536+ },
537+ store_id2: {
538+ 'copies': 2,
539+ 'mtime': 1234567891,
540+ },
541+ },
542+ }
543+ for i in range(533)
544+ ]
545+ ids = [doc['_id'] for doc in docs]
546+ ids.sort()
547+ db.save_many(docs)
548+
549+ # Test when store isn't present
550+ self.assertEqual(inst.purge_store(store_id3), [])
551+ for doc in docs:
552+ self.assertEqual(db.get(doc['_id']), doc)
553+
554+ # Purge one of the stores, make sure the other remains
555+ self.assertEqual(inst.purge_store(store_id1), ids)
556+ for doc in db.get_many(ids):
557+ _id = doc['_id']
558+ rev = doc.pop('_rev')
559+ self.assertTrue(rev.startswith('2-'))
560+ self.assertEqual(
561+ doc,
562+ {
563+ '_id': _id,
564+ 'type': 'dmedia/file',
565+ 'bytes': 1776,
566+ 'stored': {
567+ store_id2: {
568+ 'copies': 2,
569+ 'mtime': 1234567891,
570+ },
571+ },
572+ }
573+ )
574+
575+ # Purge the other store
576+ self.assertEqual(inst.purge_store(store_id2), ids)
577+ for doc in db.get_many(ids):
578+ _id = doc['_id']
579+ rev = doc.pop('_rev')
580+ self.assertTrue(rev.startswith('3-'))
581+ self.assertEqual(
582+ doc,
583+ {
584+ '_id': _id,
585+ 'type': 'dmedia/file',
586+ 'bytes': 1776,
587+ 'stored': {},
588+ }
589+ )
590+
591+ # Purge both again, make sure no doc changes result:
592+ self.assertEqual(inst.purge_store(store_id1), [])
593+ self.assertEqual(inst.purge_store(store_id2), [])
594+ for doc in db.get_many(ids):
595+ _id = doc['_id']
596+ rev = doc.pop('_rev')
597+ self.assertTrue(rev.startswith('3-'))
598+ self.assertEqual(
599+ doc,
600+ {
601+ '_id': _id,
602+ 'type': 'dmedia/file',
603+ 'bytes': 1776,
604+ 'stored': {},
605+ }
606+ )
607+
608 def test_update_atime(self):
609 inst = core.Core(self.env)
610 _id = random_id()
611
612=== modified file 'dmedia/tests/test_util.py'
613--- dmedia/tests/test_util.py 2012-07-09 14:45:38 +0000
614+++ dmedia/tests/test_util.py 2012-11-26 00:24:26 +0000
615@@ -51,7 +51,32 @@
616 tmp.makedirs('.dmedia')
617 self.assertTrue(util.isfilestore(tmp.dir))
618
619- def test_getfilestore(self):
620+ def test_get_filestore_id(self):
621+ tmp = TempDir()
622+ doc = schema.create_filestore(1)
623+ _id = doc['_id']
624+
625+ # Test when .dmedia/ doesn't exist
626+ self.assertIsNone(util.get_filestore_id(tmp.dir))
627+
628+ # Test when .dmedia/ exists, but store.json doesn't:
629+ tmp.makedirs('.dmedia')
630+ self.assertIsNone(util.get_filestore_id(tmp.dir))
631+
632+ # Test when .dmedia/store.json exists
633+ store = tmp.join('.dmedia', 'store.json')
634+ json.dump(doc, open(store, 'w'))
635+ self.assertEqual(util.get_filestore_id(tmp.dir), _id)
636+
637+ # Test with bad JSON
638+ open(store, 'wb').write(b'bad JSON, no cookie for you')
639+ self.assertIsNone(util.get_filestore_id(tmp.dir))
640+
641+ # Test with non-dict
642+ json.dump('hello', open(store, 'w'))
643+ self.assertIsNone(util.get_filestore_id(tmp.dir))
644+
645+ def test_get_filestore(self):
646 tmp = TempDir()
647 doc = schema.create_filestore(1)
648
649
650=== modified file 'dmedia/util.py'
651--- dmedia/util.py 2012-10-19 00:11:46 +0000
652+++ dmedia/util.py 2012-11-26 00:24:26 +0000
653@@ -43,6 +43,15 @@
654 return path.isdir(path.join(parentdir, DOTNAME))
655
656
657+def get_filestore_id(parentdir):
658+ store = path.join(parentdir, DOTNAME, 'store.json')
659+ try:
660+ doc = json.load(open(store, 'r'))
661+ return doc['_id']
662+ except Exception:
663+ pass
664+
665+
666 def get_filestore(parentdir, store_id, copies=None):
667 store = path.join(parentdir, DOTNAME, 'store.json')
668 doc = json.load(open(store, 'r'))

Subscribers

People subscribed via source and target branches