Merge lp:~jderose/dmedia/dbus3 into lp:dmedia

Proposed by Jason Gerard DeRose
Status: Merged
Merged at revision: 291
Proposed branch: lp:~jderose/dmedia/dbus3
Merge into: lp:dmedia
Diff against target: 1259 lines (+489/-546)
17 files modified
debian/control (+2/-0)
dmedia-browser (+3/-1)
dmedia-cli (+146/-230)
dmedia-gtk (+1/-1)
dmedia-service (+132/-123)
dmedia-service3 (+0/-181)
dmedia/__init__.py (+1/-0)
dmedia/constants.py (+1/-3)
dmedia/core.py (+6/-0)
dmedia/service/api.py (+2/-2)
dmedia/service/avahi.py (+116/-0)
dmedia/transfers.py (+2/-2)
dmedia/units.py (+13/-0)
setup.py (+1/-2)
share/org.freedesktop.Dmedia.service (+1/-1)
test-cli.py (+33/-0)
test-service.py (+29/-0)
To merge this branch: bzr merge lp:~jderose/dmedia/dbus3
Reviewer Review Type Date Requested Status
dmedia Dev Pending
Review via email: mp+88806@code.launchpad.net

Description of the change

The actual porting to python3-dbus was tiny, but then I also had to merge everything into a single real service, which is what most of the change is.

I also did a bit of cleanup and refinement:

1. Renamed DBus bus name from 'org.freedesktop.DMedia' to 'org.freedesktop.Dmedia' to be consistent with the way we now write 'Dmedia'

2. Moved Avahi integration into new dmedia.service.avahi module so `dmedia-service` is more managable

3. Revamped the `dmedia-cli` script to be simpler and more maintainable, yet hopefully retain all the good UX enhancements that David Green did

4. Added Dmedia.Resolve() DBus method... this is needed for the Novacut render server, and is the start of the API 3rd party apps will use to integrate with Dmedia

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/control'
2--- debian/control 2011-12-29 07:01:47 +0000
3+++ debian/control 2012-01-17 04:20:28 +0000
4@@ -11,6 +11,8 @@
5 Package: dmedia
6 Architecture: all
7 Depends: ${misc:Depends}, python3 (>= 3.2),
8+ python3-gi | python3-gobject,
9+ python3-dbus,
10 python3-filestore (>= 12.01),
11 python3-microfiber (>= 12.01),
12 python3-userwebkit (>= 12.01),
13
14=== modified file 'dmedia-browser'
15--- dmedia-browser 2012-01-15 15:32:45 +0000
16+++ dmedia-browser 2012-01-17 04:20:28 +0000
17@@ -24,18 +24,20 @@
18 from urllib.parse import urlparse
19
20 from userwebkit import BaseApp
21+import dmedia
22 from dmedia import local
23
24
25 class App(BaseApp):
26 name = 'dmedia'
27 dbname = 'dmedia-0'
28+ version = dmedia.__version__
29 title = 'Dmedia Browser'
30
31 splash = 'splash.html'
32 page = 'browser.html'
33
34- proxy_bus = 'org.freedesktop.DMedia'
35+ proxy_bus = dmedia.BUS
36 local = None
37
38 def dmedia_resolver(self, uri):
39
40=== modified file 'dmedia-cli'
41--- dmedia-cli 2011-10-24 21:22:09 +0000
42+++ dmedia-cli 2012-01-17 04:20:28 +0000
43@@ -27,234 +27,150 @@
44 Command line tool for talking to dmedia DBus services.
45 """
46
47-from sys import argv, stderr
48+import optparse
49+from collections import OrderedDict
50+import sys
51+from gettext import ngettext
52+from os import path
53+
54+import dbus
55+
56 import dmedia
57-from dmedia.service.api import DMedia
58-
59-class DBusCLI (object):
60- """
61- DBusCLI allows you to easily create command line interfaces from
62- DBus APIs.
63-
64- Example usage:
65- my_cli = DBusCLI(
66- 'mycli', '1.0', 'org.freedesktop.My',
67- '/', 'org.freedesktop.My'
68- )
69- my_cli.add_action("name1", function1, "usage of name1", "description of name1")
70- my_cli.add_dbus_action("MethodName", "usage of MethodName", "description of MethodName")
71- if __name__ == '__main__':
72- my_cli.main(*sys.argv[1:])
73- """
74- def __init__(self, exec_name, version):
75- """
76- `exec_name` is the name of the executable of the command line
77- program. It should be what the user needs to run to use the
78- program.
79-
80- `version` is the version string of the command line program.
81-
82- `dbus_bus` is the dbus bus name of the dbus API.
83-
84- `dbus_object` is the dbus object of the dbus API.
85-
86- `dbus_iface` is the dbus interface of the dbus API.
87- """
88- self._exec = exec_name
89- self._version = version
90- self._action_methods = {}
91- self._action_help = {}
92- self._add_default_actions()
93- self._dmedia = DMedia()
94-
95- def has_action(self, name):
96- """
97- Return true if the action `name` exists,
98- false otherwise.
99- """
100- return (name in self._action_methods and name in self._action_help)
101-
102- def get_action_method(self, name):
103- """
104- Return the function for action `name`.
105- """
106- return self._action_methods[name]
107-
108- def get_usage(self):
109- """
110- Return the usage string for this program.
111- """
112- return """Usage:
113-\t%s ACTION [ARGUMENTS]
114-
115-Run:
116-\t%s help ACTION
117-for help with a specific action.
118-
119-Run:
120-\t%s actions
121-for a list of available actions.
122-""" % (self._exec, self._exec, self._exec)
123-
124- def get_help(self, *names):
125- """
126- Return the help string for action(s) `names`.
127- If no names are specified, the help string will show
128- help for all available actions.
129- """
130- if len(names) == 0:
131- names = self.get_actions()
132- return "\n".join(map(
133- lambda name: "Action `%s`. Usage:\n\t%s %s %s\n%s\n" % (
134- (name, self._exec, name) + self._action_help[name]
135- ),
136- names
137- ))
138-
139- def get_actions(self):
140- """
141- Return all available actions as a tuple.
142- """
143- return tuple(self._action_methods)
144-
145- def show_usage(self):
146- """
147- Display the usage string for this program.
148- """
149- print(self.get_usage())
150-
151- def show_help(self, *names):
152- """
153- Display the help string for `names`. See `get_help`.
154- """
155- print(self.get_help(*names))
156-
157- def show_actions(self):
158- """
159- Display all available actions, each on a separate line.
160- """
161- print("\n".join(self.get_actions()))
162-
163- def show_version(self):
164- """
165- Display the version of this program.
166- """
167- print(self._version)
168-
169- def dbus_run(self, name, *args):
170- """
171- Run the dbus method `name` with arguments `args`.
172- Catch any errors and print them to stderr.
173- """
174- try:
175- method = getattr(self._dmedia, name)
176- print(str(method(*args)))
177- except Exception as e:
178- stderr.write("Error: {}\n".format(e))
179-
180- def run_action(self, name, *args):
181- """
182- Run the action `name` with arguments `args`.
183- """
184- if self.has_action(name):
185- self.get_action_method(name)(*args)
186- else:
187- stderr.write("No such action `%s`\n" % name)
188- print(self.get_usage())
189-
190- def run_action_with_bus(self, bus, name, *args):
191- """
192- Run the action `name` with arguments `args`.
193- Use a custom dbus bus name `bus`.
194- """
195- self._bus = bus
196- self.run_action(name, *args)
197-
198- def add_action(self, name, function=lambda *a:None, usage="", description=""):
199- """
200- Add an action to the command line program.
201- `name` is the name of the action.
202- `function` is the function to be called for this action.
203- `usage` is a string describing the arguments of the action to the user.
204- `description` is a string describing what the action does.
205- """
206- self._action_methods[name] = function
207- self._action_help[name] = (usage, description)
208-
209- def add_dbus_action(self, name, usage="", description=""):
210- """
211- Add an action to the command line program based upon a dbus method.
212- `name` is the name of the action and the name of the dbus method.
213- `usage` is a string describing the arguments of the action to the user.
214- `description` is a string describing what the action does.
215- """
216- self.add_action(
217- name,
218- lambda *args:self.dbus_run(name, *args),
219- usage,
220- description
221- )
222-
223- def _add_default_actions(self):
224- #version
225- self.add_action(
226- "version",
227- self.show_version,
228- "",
229- "Display the version of this program."
230- )
231- #help
232- self.add_action(
233- "help",
234- self.show_help,
235- "[ACTION_1] [ACTION_2] ... [ACTION_N]",
236- "Display help information for each of the listed actions. If no actions are listed, help will be displayed for all available actions."
237- )
238- #usage
239- self.add_action(
240- "usage",
241- self.show_usage,
242- "",
243- "Display a usage message."
244- )
245- #actions
246- self.add_action(
247- "actions",
248- self.show_actions,
249- "",
250- "Display the list of available actions."
251- )
252- #bus
253- self.add_action(
254- "bus",
255- self.run_action_with_bus,
256- "BUS_NAME ACTION [ARGUMENTS]",
257- "Run an action using the dbus bus `BUS_NAME` for any dbus interaction."
258- )
259-
260- def main(self, *args):
261- """
262- Run the command line program with the specified arguments.
263- """
264- args = list(args)
265- if len(args) < 1:
266- self.run_action("usage")
267- else:
268- self.run_action(args.pop(0), *args)
269-
270-dmedia_cli = DBusCLI("dmedia-cli", dmedia.__version__)
271-
272-dmedia_cli.add_dbus_action("Version", "", "Display the version of running `dmedia-service`.")
273-dmedia_cli.add_dbus_action("Kill", "", "Shutdown `dmedia-service`.")
274-dmedia_cli.add_dbus_action("AddFileStore", "PARENT_DIR", "Add a new dmedia file store. Create it in the `PARENT_DIR` directory (eg. /home/username).")
275-dmedia_cli.add_dbus_action("RemoveFileStore", "PARENT_DIR", "Remove an existing dmedia file store.")
276-#dmedia_cli.add_dbus_action("GetDoc", "DOC_ID", "Get a document from CouchDB. `DOC_ID` is the _id of the document.")
277-#dmedia_cli.add_dbus_action("Upload", "FILE_ID STORE_ID", "Upload the file with id `FILE_ID` to the remote store with id `STORE_ID`.")
278-#dmedia_cli.add_dbus_action("Download", "FILE_ID STORE_ID", "Download the file with id `FILE_ID` from the remote store with id `STORE_ID`.")
279-#dmedia_cli.add_dbus_action("ListTransfers", "", "List active uploads and downloads.")
280-dmedia_cli.add_dbus_action("GetEnv", "", "Display dmedia env as JSON.")
281-#dmedia_cli.add_dbus_action("GetAuthURL", "", "Get URL with basic auth user and password.")
282-#dmedia_cli.add_dbus_action("HasApp", "", "")
283-
284-if __name__ == '__main__':
285- dmedia_cli.main(*argv[1:])
286+from dmedia.units import minsec
287+
288+
289+methods = OrderedDict()
290+session = dbus.SessionBus()
291+
292+
293+def error(msg, code=1):
294+ print('ERROR:', msg)
295+ sys.exit(code)
296+
297+
298+def print_methods():
299+ print('DBus methods on {}:'.format(dmedia.BUS))
300+ width = max(len(name) for name in methods)
301+ for name in methods:
302+ cls = methods[name]
303+ print(' {} {}'.format(name.ljust(width), cls.__doc__))
304+
305+
306+def print_usage(cls):
307+ print('Usage:')
308+ print(' ', *cls.usage())
309+
310+
311+class MethodMeta(type):
312+ def __new__(meta, name, bases, dict):
313+ cls = type.__new__(meta, name, bases, dict)
314+ if not name.startswith('_'):
315+ methods[name] = cls
316+ return cls
317+
318+
319+class _Method(metaclass=MethodMeta):
320+ args = tuple()
321+
322+ def __init__(self, bus):
323+ self.bus = bus
324+ self.proxy = session.get_object(bus, '/')
325+
326+ @classmethod
327+ def usage(cls):
328+ script = path.basename(sys.argv[0])
329+ cmd = [script, cls.__name__]
330+ cmd.extend(arg.upper() for arg in cls.args)
331+ return cmd
332+
333+ def run(self, args):
334+ args = self.validate_args(*args)
335+ method = self.proxy.get_dbus_method(self.__class__.__name__)
336+ return self.format_output(method(*args))
337+
338+ def validate_args(self, *args):
339+ return args
340+
341+ def format_output(self, output):
342+ return output
343+
344+
345+class Version(_Method):
346+ 'Show version of running `dmedia-service`'
347+
348+
349+class Kill(_Method):
350+ 'Shutdown `dmedia-service`'
351+
352+ def format_output(self, seconds):
353+ return '{} was running for {}'.format(self.bus, minsec(seconds))
354+
355+
356+class GetEnv(_Method):
357+ 'Get the CouchDB and Dmedia environment info'
358+
359+
360+class LocalDmedia(_Method):
361+ 'Get the _local/dmedia document (shows file-stores)'
362+
363+
364+class LocalPeers(_Method):
365+ 'Get the _local/peers document'
366+
367+
368+class AddFileStore(_Method):
369+ 'Add a file storage location'
370+
371+ args = ['directory']
372+
373+ def validate_args(self, directory):
374+ return [path.abspath(directory)]
375+
376+
377+class RemoveFileStore(AddFileStore):
378+ 'Add a file storage location'
379+
380+
381+class Resolve(_Method):
382+ 'Resolve Dmedia file ID into a regular file path'
383+
384+ args = ['file_id']
385+
386+
387+parser = optparse.OptionParser(
388+ usage='%prog METHOD [ARGS...]',
389+ version=dmedia.__version__,
390+)
391+parser.add_option('--bus',
392+ help='DBus bus name; default is {!r}'.format(dmedia.BUS),
393+ default=dmedia.BUS
394+)
395+(options, args) = parser.parse_args()
396+
397+
398+if len(args) == 0:
399+ parser.print_help()
400+ print('')
401+ print_methods()
402+ sys.exit(0)
403+
404+name = args[0]
405+args = args[1:]
406+if name not in methods:
407+ print_methods()
408+ print('')
409+ error('Unknown method {!r}'.format(name))
410+
411+cls = methods[name]
412+if len(args) != len(cls.args):
413+ print_usage(cls)
414+ print('')
415+ msg = ngettext(
416+ '{!r} takes exactly {} argument',
417+ '{!r} takes exactly {} arguments',
418+ len(cls.args)
419+ )
420+ error(msg.format(name, len(cls.args)))
421+
422+method = cls(options.bus)
423+print(method.run(args))
424
425=== modified file 'dmedia-gtk'
426--- dmedia-gtk 2012-01-04 10:10:10 +0000
427+++ dmedia-gtk 2012-01-17 04:20:28 +0000
428@@ -157,7 +157,7 @@
429 splash = 'splash.html'
430 page = 'index.html'
431 title = 'Dmedia'
432- proxy_bus = 'org.freedesktop.DMedia'
433+ proxy_bus = dmedia.BUS
434
435 signals = {
436 'create_project': ['title'],
437
438=== modified file 'dmedia-service'
439--- dmedia-service 2012-01-03 02:31:47 +0000
440+++ dmedia-service 2012-01-17 04:20:28 +0000
441@@ -1,4 +1,4 @@
442-#!/usr/bin/python
443+#!/usr/bin/python3
444
445 # dmedia: distributed media library
446 # Copyright (C) 2011 Novacut Inc
447@@ -22,159 +22,168 @@
448 # Jason Gerard DeRose <jderose@novacut.com>
449
450 """
451-Hacky shim till it's possible to implement DBus servers in PyGI.
452-
453-Note that because this script must be Python2, it can't import anything from the
454-`dmedia` package as that will only be installed under Python3.
455+Dmedia DBus service on org.freedesktop.Dmedia.
456 """
457
458-import argparse
459-import os
460-from os import path
461-from subprocess import Popen
462+import time
463+start_time = time.time()
464+
465+import optparse
466 import logging
467-import time
468-import sys
469 import json
470+from os import path
471
472-import xdg.BaseDirectory
473 import dbus
474 import dbus.service
475 from dbus.mainloop.glib import DBusGMainLoop
476-from dc3lib.microfiber import Database, NotFound
477-
478-
479-__version__ = '12.01.0'
480-BUS = 'org.freedesktop.DMedia'
481+from microfiber import Database, NotFound
482+from gi.repository import GObject
483+
484+import dmedia
485+from dmedia.core import Core, start_file_server
486+from dmedia.service.dbus import UDisks
487+from dmedia.service.avahi import Avahi
488+
489+
490+BUS = dmedia.BUS
491 IFACE = BUS
492 log = logging.getLogger()
493-service3 = path.join(path.dirname(path.abspath(__file__)), 'dmedia-service3')
494-assert path.isfile(service3)
495
496+GObject.threads_init()
497 DBusGMainLoop(set_as_default=True)
498 session = dbus.SessionBus()
499
500
501-def configure_logging():
502- format = [
503- '%(levelname)s',
504- '%(message)s',
505- ]
506- script = path.abspath(sys.argv[0])
507- namespace = path.basename(script)
508- cache = path.join(xdg.BaseDirectory.xdg_cache_home, 'dmedia')
509- if not path.exists(cache):
510- os.makedirs(cache)
511- filename = path.join(cache, namespace + '.log')
512- if path.exists(filename):
513- os.rename(filename, filename + '.previous')
514- logging.basicConfig(
515- filename=filename,
516- filemode='w',
517- level=logging.DEBUG,
518- format='\t'.join(format),
519- )
520- logging.info('script: %r', script)
521- logging.info('dmedia.__file__: %r', __file__)
522- logging.info('dmedia.__version__: %r', __version__)
523-
524-
525-class DMedia(dbus.service.Object):
526- def __init__(self, bus, mainloop):
527- self._killed = False
528- self._bus = bus
529- self._mainloop = mainloop
530-
531- log.info('Binding to %r', bus)
532- super(DMedia, self).__init__(session, object_path='/')
533- self._busname = dbus.service.BusName(bus, session)
534-
535- self._dc3 = session.get_object('org.freedesktop.DC3', '/')
536- self._machine_id = None
537- env = self._get_env()
538-
539- log.info('Starting %r', service3)
540- self._child = Popen([service3, '--bus', bus])
541- db = Database('dmedia-0', env)
542- while True:
543- try:
544- time.sleep(0.1)
545- doc = db.get('_local/dmedia')
546- self._machine_id = doc['machine_id']
547- log.info('machine_id = %r', self._machine_id)
548- break
549- except NotFound:
550- pass
551-
552- def _get_env(self):
553- log.info('Calling DC3.GetEnv()')
554- env = json.loads(self._dc3.GetEnv())
555- env['machine_id'] = self._machine_id
556- return env
557+def dumps(obj):
558+ return json.dumps(obj, sort_keys=True, separators=(',', ': '), indent=4)
559+
560+
561+class Service(dbus.service.Object):
562+ httpd = None
563+ avahi = None
564+
565+ def __init__(self, bus, env_s):
566+ self.bus = bus
567+ self.env_s = env_s
568+ self.mainloop = GObject.MainLoop()
569+ log.info('DBus: binding to %r', bus)
570+ super().__init__(session, object_path='/')
571+ self.busname = dbus.service.BusName(bus, session)
572+
573+ def start_core(self):
574+ if self.env_s is None:
575+ self.dc3 = session.get_object('org.freedesktop.DC3', '/')
576+ env = json.loads(self.dc3.GetEnv())
577+ else:
578+ env = json.loads(self.env_s)
579+ self.udisks = UDisks()
580+ self.core = Core(env, self.udisks.get_parentdir_info)
581+ self.env_s = dumps(self.core.env)
582+ if len(self.core.stores) == 0:
583+ self.core.add_filestore('/home')
584+ self.udisks.connect('store_added', self.on_store_added)
585+ self.udisks.connect('store_removed', self.on_store_removed)
586+ self.udisks.monitor()
587+
588+ def start_httpd(self):
589+ (self.httpd, self.port) = start_file_server(self.core.env)
590+ self.avahi = Avahi(self.core.env, self.port)
591+ self.avahi.run()
592+
593+ def run(self):
594+ self.start_core()
595+ self.start_httpd()
596+ self.mainloop.run()
597+
598+ def kill(self):
599+ if self.avahi is not None:
600+ self.avahi.free()
601+ if self.httpd is not None:
602+ self.httpd.terminate()
603+ self.httpd.join()
604+ self.mainloop.quit()
605+
606+ def on_store_added(self, udisks, obj, parentdir, partition, drive):
607+ log.info('UDisks store_added: %r', parentdir)
608+ try:
609+ self.AddFileStore(parentdir)
610+ except Exception:
611+ log.exception('Could not add FileStore %r', parentdir)
612+
613+ def on_store_removed(self, udisks, obj, parentdir):
614+ log.info('UDisks store_removed: %r', parentdir)
615+ try:
616+ self.RemoveFileStore(parentdir)
617+ except Exception:
618+ log.exception('Could not remove FileStore %r', parentdir)
619
620 @dbus.service.method(IFACE, in_signature='', out_signature='s')
621 def Version(self):
622 """
623 Return dmedia version.
624 """
625- return __version__
626+ return dmedia.__version__
627
628- @dbus.service.method(IFACE, in_signature='', out_signature='')
629+ @dbus.service.method(IFACE, in_signature='', out_signature='i')
630 def Kill(self):
631 """
632 Kill the `dmedia-service` process.
633 """
634- if self._killed:
635- return
636- self._killed = True
637- log.info('Killing dmedia core service on %r', self._bus)
638- self.FwdKill()
639- self._child.wait()
640- self._mainloop.quit()
641-
642- @dbus.service.signal(IFACE, signature='')
643- def FwdKill(self):
644- pass
645+ self.kill()
646+ return int(time.time() - start_time)
647
648 @dbus.service.method(IFACE, in_signature='', out_signature='s')
649 def GetEnv(self):
650 """
651 Return dmedia env as JSON string.
652 """
653- return json.dumps(self._get_env())
654-
655- @dbus.service.method(IFACE, in_signature='s', out_signature='')
656+ return self.env_s
657+
658+ @dbus.service.method(IFACE, in_signature='', out_signature='s')
659+ def LocalDmedia(self):
660+ """
661+ Return the _local/dmedia doc.
662+ """
663+ return dumps(self.core.db.get('_local/dmedia'))
664+
665+ @dbus.service.method(IFACE, in_signature='', out_signature='s')
666+ def LocalPeers(self):
667+ """
668+ Return the _local/peers doc.
669+ """
670+ return dumps(self.core.db.get('_local/peers'))
671+
672+ @dbus.service.method(IFACE, in_signature='s', out_signature='s')
673 def AddFileStore(self, parentdir):
674- self.FwdAddFileStore(parentdir)
675-
676- @dbus.service.signal(IFACE, signature='s')
677- def FwdAddFileStore(self, parentdir):
678- pass
679-
680- @dbus.service.method(IFACE, in_signature='s', out_signature='')
681+ fs = self.core.add_filestore(parentdir)
682+ return self.LocalDmedia()
683+
684+ @dbus.service.method(IFACE, in_signature='s', out_signature='s')
685 def RemoveFileStore(self, parentdir):
686- self.FwdRemoveFileStore(parentdir)
687-
688- @dbus.service.signal(IFACE, signature='s')
689- def FwdRemoveFileStore(self, parentdir):
690- pass
691-
692-
693-parser = argparse.ArgumentParser(
694- description='DBus service @{}'.format(BUS),
695-)
696-parser.add_argument('--version', action='version', version=__version__)
697-parser.add_argument('--bus',
698- default=BUS,
699- help='DBus bus name; default is %(default)r',
700-)
701-args = parser.parse_args()
702-configure_logging()
703-
704-from gi.repository import GObject
705-GObject.threads_init()
706-
707-mainloop = GObject.MainLoop()
708-dmedia = DMedia(args.bus, mainloop)
709-mainloop.run()
710-dmedia.Kill()
711+ fs = self.core.remove_filestore(parentdir)
712+ return self.LocalDmedia()
713+
714+ @dbus.service.method(IFACE, in_signature='s', out_signature='s')
715+ def Resolve(self, _id):
716+ return self.core.resolve(_id)
717+
718+
719+parser = optparse.OptionParser(
720+ version=dmedia.__version__,
721+)
722+parser.add_option('--env',
723+ help='JSON-encoded CouchDB environment (for unit testing)',
724+)
725+parser.add_option('--bus',
726+ help='DBus bus name; default is {!r}'.format(BUS),
727+ default=BUS
728+)
729+(options, args) = parser.parse_args()
730+
731+
732+dmedia.configure_logging()
733+service = Service(options.bus, options.env)
734+try:
735+ service.run()
736+finally:
737+ service.kill()
738
739=== removed file 'dmedia-service3'
740--- dmedia-service3 2011-12-27 10:02:37 +0000
741+++ dmedia-service3 1970-01-01 00:00:00 +0000
742@@ -1,181 +0,0 @@
743-#!/usr/bin/python3
744-
745-# dmedia: distributed media library
746-# Copyright (C) 2011 Novacut Inc
747-#
748-# This file is part of `dmedia`.
749-#
750-# `dmedia` is free software: you can redistribute it and/or modify it under
751-# the terms of the GNU Affero General Public License as published by the Free
752-# Software Foundation, either version 3 of the License, or (at your option) any
753-# later version.
754-#
755-# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
756-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
757-# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
758-# details.
759-#
760-# You should have received a copy of the GNU Affero General Public License along
761-# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
762-#
763-# Authors:
764-# Jason Gerard DeRose <jderose@novacut.com>
765-
766-"""
767-The real Python3 dmedia DBus service (well, almost).
768-"""
769-
770-import sys
771-import json
772-from os import path
773-import optparse
774-import logging
775-
776-from gi.repository import GObject
777-from microfiber import NotFound
778-
779-import dmedia
780-from dmedia.service import dbus
781-from dmedia.service.dbus import session, UDisks
782-from dmedia.core import Core, start_file_server
783-
784-GObject.threads_init()
785-log = logging.getLogger()
786-
787-
788-class DMedia:
789- def __init__(self, bus):
790- self.mainloop = GObject.MainLoop()
791- dc3 = session.get('org.freedesktop.DC3', '/')
792- env = json.loads(dc3.GetEnv())
793- self.udisks = UDisks()
794- self.core = Core(env, self.udisks.get_parentdir_info)
795- if len(self.core.stores) == 0:
796- self.core.add_filestore('/home')
797- self._fwd = {
798- 'AddFileStore': self.AddFileStore,
799- 'RemoveFileStore': self.RemoveFileStore,
800- 'Kill': self.Kill,
801- }
802- if bus:
803- log.info('Listing to forwards from %r', bus)
804- self.proxy = session.get(bus, '/', 'org.freedesktop.DMedia')
805- self.proxy.connect('g-signal', self.on_g_signal)
806- self.avahi = dbus.system.get(
807- 'org.freedesktop.Avahi',
808- '/',
809- 'org.freedesktop.Avahi.Server'
810- )
811- try:
812- self._peers = self.core.db.get('_local/peers')
813- except NotFound:
814- self._peers = {'_id': '_local/peers'}
815- self._peers['peers'] = {}
816- self.core.db.save(self._peers)
817- self.udisks.connect('store_added', self.on_store_added)
818- self.udisks.connect('store_removed', self.on_store_removed)
819- self.udisks.monitor()
820-
821- def run(self):
822- (self.httpd, self.port) = start_file_server(self.core.env)
823- self.group = dbus.system.get(
824- 'org.freedesktop.Avahi',
825- self.avahi.EntryGroupNew(),
826- 'org.freedesktop.Avahi.EntryGroup'
827- )
828- self.group.AddService('(iiussssqaay)',
829- -1, # Interface
830- 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
831- 0, # Flags
832- self.core.machine_id,
833- '_dmedia._tcp',
834- '', # Domain, default to .local
835- '', # Host, default to localhost
836- self.port, # Port
837- None # TXT record
838- )
839- self.group.Commit()
840- browser_path = self.avahi.ServiceBrowserNew('(iissu)',
841- -1, # Interface
842- 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
843- '_dmedia._tcp',
844- 'local',
845- 0 # Flags
846- )
847- self.browser = dbus.system.get(
848- 'org.freedesktop.Avahi',
849- browser_path,
850- 'org.freedesktop.Avahi.ServiceBrowser'
851- )
852- self.browser.connect('g-signal', self.on_browser_g_signal)
853- self.mainloop.run()
854-
855- def on_store_added(self, udisks, obj, parentdir, partition, drive):
856- log.info('UDisks store_added: %r', parentdir)
857- try:
858- self.AddFileStore(parentdir)
859- except Exception:
860- log.exception('Could not add FileStore %r', parentdir)
861-
862- def on_store_removed(self, udisks, obj, parentdir):
863- log.info('UDisks store_removed: %r', parentdir)
864- try:
865- self.RemoveFileStore(parentdir)
866- except Exception:
867- log.exception('Could not remove FileStore %r', parentdir)
868-
869- def on_g_signal(self, proxy, sender, signal, params):
870- if signal.startswith('Fwd'):
871- name = signal[3:]
872- args = params.unpack()
873- try:
874- self._fwd[name](*args)
875- except KeyError:
876- pass
877-
878- def on_browser_g_signal(self, proxy, sender, signal, params):
879- if signal == 'ItemNew':
880- (interface, protocol, name, _type, domain, flags) = params.unpack()
881- if name != self.core.machine_id: # Ignore what we publish ourselves
882- (ip, port) = self.avahi.ResolveService('(iisssiu)',
883- interface, protocol, name, _type, domain, -1, 0
884- )[7:9]
885- url = 'http://{}:{}/'.format(ip, port)
886- log.info('New peer %r at %r', name, url)
887- self._peers['peers'][name] = url
888- self.core.db.save(self._peers)
889- elif signal == 'ItemRemove':
890- (interface, protocol, name, _type, domain, flags) = params.unpack()
891- log.info('Removing peer %r', name)
892- try:
893- del self._peers['peers'][name]
894- self.core.db.save(self._peers)
895- except KeyError:
896- pass
897-
898- def AddFileStore(self, parentdir):
899- self.core.add_filestore(path.abspath(parentdir))
900-
901- def RemoveFileStore(self, parentdir):
902- self.core.remove_filestore(path.abspath(parentdir))
903-
904- def Kill(self):
905- self.group.Reset()
906- self.httpd.terminate()
907- self.httpd.join()
908- self.mainloop.quit()
909-
910-
911-parser = optparse.OptionParser(
912- usage='Usage: %prog FILE',
913- version=dmedia.__version__,
914-)
915-parser.add_option('--bus',
916- help='If provided, expect Python2 shim on this DBus bus',
917-)
918-(options, args) = parser.parse_args()
919-
920-dmedia.configure_logging()
921-service = DMedia(options.bus)
922-service.run()
923-service.Kill()
924
925=== modified file 'dmedia/__init__.py'
926--- dmedia/__init__.py 2011-12-27 07:21:46 +0000
927+++ dmedia/__init__.py 2012-01-17 04:20:28 +0000
928@@ -27,6 +27,7 @@
929 """
930
931 __version__ = '12.01.0'
932+BUS = 'org.freedesktop.Dmedia'
933
934
935 def configure_logging():
936
937=== modified file 'dmedia/constants.py'
938--- dmedia/constants.py 2012-01-03 02:12:57 +0000
939+++ dmedia/constants.py 2012-01-17 04:20:28 +0000
940@@ -45,10 +45,8 @@
941
942
943 # D-Bus releated:
944-BUS = 'org.freedesktop.DMedia'
945+BUS = 'org.freedesktop.Dmedia'
946 IFACE = BUS
947-IMPORT_BUS = 'org.freedesktop.DMediaImporter'
948-IMPORT_IFACE = IMPORT_BUS
949 DC_BUS = 'org.desktopcouch.CouchDB'
950 DC_INTERFACE = DC_BUS
951
952
953=== modified file 'dmedia/core.py'
954--- dmedia/core.py 2012-01-02 23:49:23 +0000
955+++ dmedia/core.py 2012-01-17 04:20:28 +0000
956@@ -39,6 +39,7 @@
957 from microfiber import Database, NotFound, random_id2
958 from filestore import FileStore, check_root_hash, check_id
959
960+import dmedia
961 from dmedia import schema
962 from dmedia.local import LocalStores
963 from dmedia.views import init_views
964@@ -133,6 +134,7 @@
965 self.db.save(machine)
966 self.machine_id = self.local['machine_id']
967 self.env['machine_id'] = self.machine_id
968+ self.env['version'] = dmedia.__version__
969 log.info('machine_id = %r', self.machine_id)
970
971 def _init_stores(self):
972@@ -207,3 +209,7 @@
973 assert set(self.local['stores']) == set(self.stores.parentdirs)
974 assert set(s['id'] for s in self.local['stores'].values()) == set(self.stores.ids)
975
976+ def resolve(self, _id):
977+ doc = self.db.get(_id)
978+ fs = self.stores.choose_local_store(doc)
979+ return fs.stat(_id).name
980
981=== modified file 'dmedia/service/api.py'
982--- dmedia/service/api.py 2011-10-24 21:36:05 +0000
983+++ dmedia/service/api.py 2012-01-17 04:20:28 +0000
984@@ -30,10 +30,10 @@
985
986 class DMedia:
987 """
988- Talk to "org.freedesktop.DMedia".
989+ Talk to "org.freedesktop.Dmedia".
990 """
991
992- def __init__(self, bus='org.freedesktop.DMedia'):
993+ def __init__(self, bus='org.freedesktop.Dmedia'):
994 self.bus = bus
995 self._proxy = None
996
997
998=== added file 'dmedia/service/avahi.py'
999--- dmedia/service/avahi.py 1970-01-01 00:00:00 +0000
1000+++ dmedia/service/avahi.py 2012-01-17 04:20:28 +0000
1001@@ -0,0 +1,116 @@
1002+# dmedia: distributed media library
1003+# Copyright (C) 2011 Novacut Inc
1004+#
1005+# This file is part of `dmedia`.
1006+#
1007+# `dmedia` is free software: you can redistribute it and/or modify it under the
1008+# terms of the GNU Affero General Public License as published by the Free
1009+# Software Foundation, either version 3 of the License, or (at your option) any
1010+# later version.
1011+#
1012+# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
1013+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
1014+# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
1015+# details.
1016+#
1017+# You should have received a copy of the GNU Affero General Public License along
1018+# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
1019+#
1020+# Authors:
1021+# Jason Gerard DeRose <jderose@novacut.com>
1022+
1023+"""
1024+Advertise Dmedia HTTP server over Avahi, discover other peers.
1025+"""
1026+
1027+import logging
1028+
1029+from microfiber import Database, NotFound
1030+
1031+from dmedia.service.dbus import system
1032+
1033+
1034+PEERS = '_local/peers'
1035+log = logging.getLogger()
1036+
1037+
1038+class Avahi:
1039+ group = None
1040+
1041+ def __init__(self, env, port):
1042+ self.avahi = system.get(
1043+ 'org.freedesktop.Avahi',
1044+ '/',
1045+ 'org.freedesktop.Avahi.Server'
1046+ )
1047+ self.db = Database('dmedia-0', env)
1048+ self.machine_id = env['machine_id']
1049+ self.port = port
1050+
1051+ def __del__(self):
1052+ self.free()
1053+
1054+ def run(self):
1055+ try:
1056+ self.peers = self.db.get(PEERS)
1057+ if self.peers.get('peers') != {}:
1058+ self.peers['peers'] = {}
1059+ self.db.save(self.peers)
1060+ except NotFound:
1061+ self.peers = {'_id': PEERS, 'peers': {}}
1062+ self.db.save(self.peers)
1063+ self.group = system.get(
1064+ 'org.freedesktop.Avahi',
1065+ self.avahi.EntryGroupNew(),
1066+ 'org.freedesktop.Avahi.EntryGroup'
1067+ )
1068+ log.info('Avahi: advertising %r on port %r', self.machine_id, self.port)
1069+ self.group.AddService('(iiussssqaay)',
1070+ -1, # Interface
1071+ 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
1072+ 0, # Flags
1073+ self.machine_id,
1074+ '_dmedia._tcp',
1075+ '', # Domain, default to .local
1076+ '', # Host, default to localhost
1077+ self.port, # Port
1078+ None # TXT record
1079+ )
1080+ self.group.Commit()
1081+ browser_path = self.avahi.ServiceBrowserNew('(iissu)',
1082+ -1, # Interface
1083+ 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
1084+ '_dmedia._tcp',
1085+ 'local',
1086+ 0 # Flags
1087+ )
1088+ self.browser = system.get(
1089+ 'org.freedesktop.Avahi',
1090+ browser_path,
1091+ 'org.freedesktop.Avahi.ServiceBrowser'
1092+ )
1093+ self.browser.connect('g-signal', self.on_g_signal)
1094+
1095+ def free(self):
1096+ if self.group is not None:
1097+ self.group.Reset()
1098+
1099+ def on_g_signal(self, proxy, sender, signal, params):
1100+ if signal == 'ItemNew':
1101+ (interface, protocol, name, _type, domain, flags) = params.unpack()
1102+ if name != self.machine_id: # Ignore what we publish ourselves
1103+ (ip, port) = self.avahi.ResolveService('(iisssiu)',
1104+ interface, protocol, name, _type, domain, -1, 0
1105+ )[7:9]
1106+ url = 'http://{}:{}/'.format(ip, port)
1107+ log.info('Avahi: new peer %r at %r', name, url)
1108+ self.peers['peers'][name] = url
1109+ self.db.save(self.peers)
1110+ elif signal == 'ItemRemove':
1111+ (interface, protocol, name, _type, domain, flags) = params.unpack()
1112+ log.info('Avahi: removing peer %r', name)
1113+ try:
1114+ del self.peers['peers'][name]
1115+ self.db.save(self.peers)
1116+ except KeyError:
1117+ pass
1118
1119=== modified file 'dmedia/transfers.py'
1120--- dmedia/transfers.py 2011-09-22 10:20:44 +0000
1121+++ dmedia/transfers.py 2012-01-17 04:20:28 +0000
1122@@ -41,7 +41,7 @@
1123 _uploaders = {}
1124 _downloaders = {}
1125
1126-# Note: should probably export each download on the org.freedesktop.DMedia bus
1127+# Note: should probably export each download on the org.freedesktop.Dmedia bus
1128 # at the object path /downloads/FILE_ID
1129
1130 def download_key(file_id, store_id):
1131@@ -63,7 +63,7 @@
1132 """
1133 return '/downloads/' + file_id
1134
1135-# Note: should probably export each upload on the org.freedesktop.DMedia bus
1136+# Note: should probably export each upload on the org.freedesktop.Dmedia bus
1137 # at the object path /uploads/FILE_ID/REMOTE_ID
1138
1139 def upload_key(file_id, store_id):
1140
1141=== modified file 'dmedia/units.py'
1142--- dmedia/units.py 2011-10-08 11:18:57 +0000
1143+++ dmedia/units.py 2012-01-17 04:20:28 +0000
1144@@ -68,3 +68,16 @@
1145 return (
1146 '{:.3g} {}'.format(s, BYTES10[i])
1147 )
1148+
1149+
1150+def minsec(seconds):
1151+ """
1152+ Format *seconds* as a M:SS string with minutes and seconds.
1153+
1154+ For example:
1155+
1156+ >>> minsec(123)
1157+ '2:03'
1158+
1159+ """
1160+ return '{:d}:{:02d}'.format(seconds // 60, seconds % 60)
1161
1162=== modified file 'setup.py'
1163--- setup.py 2012-01-15 15:21:24 +0000
1164+++ setup.py 2012-01-17 04:20:28 +0000
1165@@ -159,12 +159,11 @@
1166 ('lib/dmedia',
1167 [
1168 'dmedia-service',
1169- 'dmedia-service3',
1170 'share/init-filestore',
1171 ]
1172 ),
1173 ('share/dbus-1/services/',
1174- ['share/org.freedesktop.DMedia.service']
1175+ ['share/org.freedesktop.Dmedia.service']
1176 ),
1177 ],
1178 )
1179
1180=== renamed file 'share/org.freedesktop.DMedia.service' => 'share/org.freedesktop.Dmedia.service'
1181--- share/org.freedesktop.DMedia.service 2011-04-10 11:02:23 +0000
1182+++ share/org.freedesktop.Dmedia.service 2012-01-17 04:20:28 +0000
1183@@ -1,3 +1,3 @@
1184 [D-BUS Service]
1185-Name=org.freedesktop.DMedia
1186+Name=org.freedesktop.Dmedia
1187 Exec=/usr/lib/dmedia/dmedia-service
1188
1189=== added file 'test-cli.py'
1190--- test-cli.py 1970-01-01 00:00:00 +0000
1191+++ test-cli.py 2012-01-17 04:20:28 +0000
1192@@ -0,0 +1,33 @@
1193+#!/usr/bin/python3
1194+
1195+from usercouch.misc import TempCouch
1196+from microfiber import random_id
1197+import json
1198+import time
1199+from subprocess import Popen, check_output
1200+
1201+bus = 'tmp' + random_id() + '.Dmedia'
1202+
1203+def call(*args):
1204+ cmd = ('./dmedia-cli', '--bus', bus) + args
1205+ print(check_output(cmd).decode('utf-8'))
1206+
1207+# Check that printing help doesn't connect to the DBus service:
1208+call()
1209+
1210+# Start a tmp couchdb and the dbus service on a random bus name:
1211+tmpcouch = TempCouch()
1212+env = tmpcouch.bootstrap()
1213+cmd = ['./dmedia-service', '--bus', bus, '--env', json.dumps(env)]
1214+child = Popen(cmd)
1215+time.sleep(1)
1216+
1217+try:
1218+ call('Version')
1219+ call('GetEnv')
1220+ call('LocalDmedia')
1221+ call('LocalPeers')
1222+ call('RemoveFileStore', '/home/')
1223+ call('AddFileStore', '/home/')
1224+finally:
1225+ call('Kill')
1226
1227=== added file 'test-service.py'
1228--- test-service.py 1970-01-01 00:00:00 +0000
1229+++ test-service.py 2012-01-17 04:20:28 +0000
1230@@ -0,0 +1,29 @@
1231+#!/usr/bin/python3
1232+
1233+from usercouch.misc import TempCouch
1234+from microfiber import random_id
1235+import json
1236+import time
1237+from subprocess import Popen
1238+from dmedia.service.dbus import session
1239+
1240+bus = 'tmp' + random_id() + '.Dmedia'
1241+tmpcouch = TempCouch()
1242+env = tmpcouch.bootstrap()
1243+
1244+cmd = ['./dmedia-service', '--bus', bus, '--env', json.dumps(env)]
1245+
1246+child = Popen(cmd)
1247+time.sleep(2)
1248+
1249+proxy = session.get(bus, '/', 'org.freedesktop.Dmedia')
1250+try:
1251+ print(proxy.Version())
1252+ print(proxy.GetEnv())
1253+ print(proxy.LocalDmedia())
1254+ print(proxy.LocalPeers())
1255+ print(proxy.RemoveFileStore('(s)', '/home'))
1256+ print(proxy.AddFileStore('(s)', '/home'))
1257+finally:
1258+ print(proxy.Kill())
1259+

Subscribers

People subscribed via source and target branches