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
=== modified file 'debian/control'
--- debian/control 2011-12-29 07:01:47 +0000
+++ debian/control 2012-01-17 04:20:28 +0000
@@ -11,6 +11,8 @@
11Package: dmedia11Package: dmedia
12Architecture: all12Architecture: all
13Depends: ${misc:Depends}, python3 (>= 3.2),13Depends: ${misc:Depends}, python3 (>= 3.2),
14 python3-gi | python3-gobject,
15 python3-dbus,
14 python3-filestore (>= 12.01),16 python3-filestore (>= 12.01),
15 python3-microfiber (>= 12.01),17 python3-microfiber (>= 12.01),
16 python3-userwebkit (>= 12.01),18 python3-userwebkit (>= 12.01),
1719
=== modified file 'dmedia-browser'
--- dmedia-browser 2012-01-15 15:32:45 +0000
+++ dmedia-browser 2012-01-17 04:20:28 +0000
@@ -24,18 +24,20 @@
24from urllib.parse import urlparse24from urllib.parse import urlparse
2525
26from userwebkit import BaseApp26from userwebkit import BaseApp
27import dmedia
27from dmedia import local28from dmedia import local
2829
2930
30class App(BaseApp):31class App(BaseApp):
31 name = 'dmedia'32 name = 'dmedia'
32 dbname = 'dmedia-0'33 dbname = 'dmedia-0'
34 version = dmedia.__version__
33 title = 'Dmedia Browser'35 title = 'Dmedia Browser'
3436
35 splash = 'splash.html'37 splash = 'splash.html'
36 page = 'browser.html'38 page = 'browser.html'
3739
38 proxy_bus = 'org.freedesktop.DMedia'40 proxy_bus = dmedia.BUS
39 local = None41 local = None
40 42
41 def dmedia_resolver(self, uri):43 def dmedia_resolver(self, uri):
4244
=== modified file 'dmedia-cli'
--- dmedia-cli 2011-10-24 21:22:09 +0000
+++ dmedia-cli 2012-01-17 04:20:28 +0000
@@ -27,234 +27,150 @@
27Command line tool for talking to dmedia DBus services.27Command line tool for talking to dmedia DBus services.
28"""28"""
2929
30from sys import argv, stderr30import optparse
31from collections import OrderedDict
32import sys
33from gettext import ngettext
34from os import path
35
36import dbus
37
31import dmedia38import dmedia
32from dmedia.service.api import DMedia39from dmedia.units import minsec
3340
34class DBusCLI (object):41
35 """42methods = OrderedDict()
36 DBusCLI allows you to easily create command line interfaces from43session = dbus.SessionBus()
37 DBus APIs.44
3845
39 Example usage:46def error(msg, code=1):
40 my_cli = DBusCLI(47 print('ERROR:', msg)
41 'mycli', '1.0', 'org.freedesktop.My',48 sys.exit(code)
42 '/', 'org.freedesktop.My'49
43 )50
44 my_cli.add_action("name1", function1, "usage of name1", "description of name1")51def print_methods():
45 my_cli.add_dbus_action("MethodName", "usage of MethodName", "description of MethodName")52 print('DBus methods on {}:'.format(dmedia.BUS))
46 if __name__ == '__main__':53 width = max(len(name) for name in methods)
47 my_cli.main(*sys.argv[1:])54 for name in methods:
48 """55 cls = methods[name]
49 def __init__(self, exec_name, version):56 print(' {} {}'.format(name.ljust(width), cls.__doc__))
50 """57
51 `exec_name` is the name of the executable of the command line58
52 program. It should be what the user needs to run to use the59def print_usage(cls):
53 program.60 print('Usage:')
5461 print(' ', *cls.usage())
55 `version` is the version string of the command line program.62
5663
57 `dbus_bus` is the dbus bus name of the dbus API.64class MethodMeta(type):
5865 def __new__(meta, name, bases, dict):
59 `dbus_object` is the dbus object of the dbus API.66 cls = type.__new__(meta, name, bases, dict)
6067 if not name.startswith('_'):
61 `dbus_iface` is the dbus interface of the dbus API.68 methods[name] = cls
62 """69 return cls
63 self._exec = exec_name70
64 self._version = version71
65 self._action_methods = {}72class _Method(metaclass=MethodMeta):
66 self._action_help = {}73 args = tuple()
67 self._add_default_actions()74
68 self._dmedia = DMedia()75 def __init__(self, bus):
6976 self.bus = bus
70 def has_action(self, name):77 self.proxy = session.get_object(bus, '/')
71 """78
72 Return true if the action `name` exists,79 @classmethod
73 false otherwise.80 def usage(cls):
74 """81 script = path.basename(sys.argv[0])
75 return (name in self._action_methods and name in self._action_help)82 cmd = [script, cls.__name__]
7683 cmd.extend(arg.upper() for arg in cls.args)
77 def get_action_method(self, name):84 return cmd
78 """85
79 Return the function for action `name`.86 def run(self, args):
80 """87 args = self.validate_args(*args)
81 return self._action_methods[name]88 method = self.proxy.get_dbus_method(self.__class__.__name__)
8289 return self.format_output(method(*args))
83 def get_usage(self):90
84 """91 def validate_args(self, *args):
85 Return the usage string for this program.92 return args
86 """93
87 return """Usage:94 def format_output(self, output):
88\t%s ACTION [ARGUMENTS]95 return output
8996
90Run:97
91\t%s help ACTION98class Version(_Method):
92for help with a specific action.99 'Show version of running `dmedia-service`'
93100
94Run:101
95\t%s actions102class Kill(_Method):
96for a list of available actions.103 'Shutdown `dmedia-service`'
97""" % (self._exec, self._exec, self._exec)104
98105 def format_output(self, seconds):
99 def get_help(self, *names):106 return '{} was running for {}'.format(self.bus, minsec(seconds))
100 """107
101 Return the help string for action(s) `names`.108
102 If no names are specified, the help string will show109class GetEnv(_Method):
103 help for all available actions.110 'Get the CouchDB and Dmedia environment info'
104 """111
105 if len(names) == 0:112
106 names = self.get_actions()113class LocalDmedia(_Method):
107 return "\n".join(map(114 'Get the _local/dmedia document (shows file-stores)'
108 lambda name: "Action `%s`. Usage:\n\t%s %s %s\n%s\n" % (115
109 (name, self._exec, name) + self._action_help[name]116
110 ),117class LocalPeers(_Method):
111 names118 'Get the _local/peers document'
112 ))119
113120
114 def get_actions(self):121class AddFileStore(_Method):
115 """122 'Add a file storage location'
116 Return all available actions as a tuple.123
117 """124 args = ['directory']
118 return tuple(self._action_methods)125
119126 def validate_args(self, directory):
120 def show_usage(self):127 return [path.abspath(directory)]
121 """128
122 Display the usage string for this program.129
123 """130class RemoveFileStore(AddFileStore):
124 print(self.get_usage())131 'Add a file storage location'
125132
126 def show_help(self, *names):133
127 """134class Resolve(_Method):
128 Display the help string for `names`. See `get_help`.135 'Resolve Dmedia file ID into a regular file path'
129 """136
130 print(self.get_help(*names))137 args = ['file_id']
131138
132 def show_actions(self):139
133 """140parser = optparse.OptionParser(
134 Display all available actions, each on a separate line.141 usage='%prog METHOD [ARGS...]',
135 """142 version=dmedia.__version__,
136 print("\n".join(self.get_actions()))143)
137144parser.add_option('--bus',
138 def show_version(self):145 help='DBus bus name; default is {!r}'.format(dmedia.BUS),
139 """146 default=dmedia.BUS
140 Display the version of this program.147)
141 """148(options, args) = parser.parse_args()
142 print(self._version)149
143150
144 def dbus_run(self, name, *args):151if len(args) == 0:
145 """152 parser.print_help()
146 Run the dbus method `name` with arguments `args`.153 print('')
147 Catch any errors and print them to stderr.154 print_methods()
148 """155 sys.exit(0)
149 try:156
150 method = getattr(self._dmedia, name)157name = args[0]
151 print(str(method(*args)))158args = args[1:]
152 except Exception as e:159if name not in methods:
153 stderr.write("Error: {}\n".format(e))160 print_methods()
154161 print('')
155 def run_action(self, name, *args):162 error('Unknown method {!r}'.format(name))
156 """163
157 Run the action `name` with arguments `args`.164cls = methods[name]
158 """165if len(args) != len(cls.args):
159 if self.has_action(name):166 print_usage(cls)
160 self.get_action_method(name)(*args)167 print('')
161 else:168 msg = ngettext(
162 stderr.write("No such action `%s`\n" % name)169 '{!r} takes exactly {} argument',
163 print(self.get_usage())170 '{!r} takes exactly {} arguments',
164171 len(cls.args)
165 def run_action_with_bus(self, bus, name, *args):172 )
166 """173 error(msg.format(name, len(cls.args)))
167 Run the action `name` with arguments `args`.174
168 Use a custom dbus bus name `bus`.175method = cls(options.bus)
169 """176print(method.run(args))
170 self._bus = bus
171 self.run_action(name, *args)
172
173 def add_action(self, name, function=lambda *a:None, usage="", description=""):
174 """
175 Add an action to the command line program.
176 `name` is the name of the action.
177 `function` is the function to be called for this action.
178 `usage` is a string describing the arguments of the action to the user.
179 `description` is a string describing what the action does.
180 """
181 self._action_methods[name] = function
182 self._action_help[name] = (usage, description)
183
184 def add_dbus_action(self, name, usage="", description=""):
185 """
186 Add an action to the command line program based upon a dbus method.
187 `name` is the name of the action and the name of the dbus method.
188 `usage` is a string describing the arguments of the action to the user.
189 `description` is a string describing what the action does.
190 """
191 self.add_action(
192 name,
193 lambda *args:self.dbus_run(name, *args),
194 usage,
195 description
196 )
197
198 def _add_default_actions(self):
199 #version
200 self.add_action(
201 "version",
202 self.show_version,
203 "",
204 "Display the version of this program."
205 )
206 #help
207 self.add_action(
208 "help",
209 self.show_help,
210 "[ACTION_1] [ACTION_2] ... [ACTION_N]",
211 "Display help information for each of the listed actions. If no actions are listed, help will be displayed for all available actions."
212 )
213 #usage
214 self.add_action(
215 "usage",
216 self.show_usage,
217 "",
218 "Display a usage message."
219 )
220 #actions
221 self.add_action(
222 "actions",
223 self.show_actions,
224 "",
225 "Display the list of available actions."
226 )
227 #bus
228 self.add_action(
229 "bus",
230 self.run_action_with_bus,
231 "BUS_NAME ACTION [ARGUMENTS]",
232 "Run an action using the dbus bus `BUS_NAME` for any dbus interaction."
233 )
234
235 def main(self, *args):
236 """
237 Run the command line program with the specified arguments.
238 """
239 args = list(args)
240 if len(args) < 1:
241 self.run_action("usage")
242 else:
243 self.run_action(args.pop(0), *args)
244
245dmedia_cli = DBusCLI("dmedia-cli", dmedia.__version__)
246
247dmedia_cli.add_dbus_action("Version", "", "Display the version of running `dmedia-service`.")
248dmedia_cli.add_dbus_action("Kill", "", "Shutdown `dmedia-service`.")
249dmedia_cli.add_dbus_action("AddFileStore", "PARENT_DIR", "Add a new dmedia file store. Create it in the `PARENT_DIR` directory (eg. /home/username).")
250dmedia_cli.add_dbus_action("RemoveFileStore", "PARENT_DIR", "Remove an existing dmedia file store.")
251#dmedia_cli.add_dbus_action("GetDoc", "DOC_ID", "Get a document from CouchDB. `DOC_ID` is the _id of the document.")
252#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`.")
253#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`.")
254#dmedia_cli.add_dbus_action("ListTransfers", "", "List active uploads and downloads.")
255dmedia_cli.add_dbus_action("GetEnv", "", "Display dmedia env as JSON.")
256#dmedia_cli.add_dbus_action("GetAuthURL", "", "Get URL with basic auth user and password.")
257#dmedia_cli.add_dbus_action("HasApp", "", "")
258
259if __name__ == '__main__':
260 dmedia_cli.main(*argv[1:])
261177
=== modified file 'dmedia-gtk'
--- dmedia-gtk 2012-01-04 10:10:10 +0000
+++ dmedia-gtk 2012-01-17 04:20:28 +0000
@@ -157,7 +157,7 @@
157 splash = 'splash.html'157 splash = 'splash.html'
158 page = 'index.html'158 page = 'index.html'
159 title = 'Dmedia'159 title = 'Dmedia'
160 proxy_bus = 'org.freedesktop.DMedia'160 proxy_bus = dmedia.BUS
161161
162 signals = {162 signals = {
163 'create_project': ['title'],163 'create_project': ['title'],
164164
=== modified file 'dmedia-service'
--- dmedia-service 2012-01-03 02:31:47 +0000
+++ dmedia-service 2012-01-17 04:20:28 +0000
@@ -1,4 +1,4 @@
1#!/usr/bin/python1#!/usr/bin/python3
22
3# dmedia: distributed media library3# dmedia: distributed media library
4# Copyright (C) 2011 Novacut Inc4# Copyright (C) 2011 Novacut Inc
@@ -22,159 +22,168 @@
22# Jason Gerard DeRose <jderose@novacut.com>22# Jason Gerard DeRose <jderose@novacut.com>
2323
24"""24"""
25Hacky shim till it's possible to implement DBus servers in PyGI.25Dmedia DBus service on org.freedesktop.Dmedia.
26
27Note that because this script must be Python2, it can't import anything from the
28`dmedia` package as that will only be installed under Python3.
29"""26"""
3027
31import argparse28import time
32import os29start_time = time.time()
33from os import path30
34from subprocess import Popen31import optparse
35import logging32import logging
36import time
37import sys
38import json33import json
34from os import path
3935
40import xdg.BaseDirectory
41import dbus36import dbus
42import dbus.service37import dbus.service
43from dbus.mainloop.glib import DBusGMainLoop38from dbus.mainloop.glib import DBusGMainLoop
44from dc3lib.microfiber import Database, NotFound39from microfiber import Database, NotFound
4540from gi.repository import GObject
4641
47__version__ = '12.01.0'42import dmedia
48BUS = 'org.freedesktop.DMedia'43from dmedia.core import Core, start_file_server
44from dmedia.service.dbus import UDisks
45from dmedia.service.avahi import Avahi
46
47
48BUS = dmedia.BUS
49IFACE = BUS49IFACE = BUS
50log = logging.getLogger()50log = logging.getLogger()
51service3 = path.join(path.dirname(path.abspath(__file__)), 'dmedia-service3')
52assert path.isfile(service3)
5351
52GObject.threads_init()
54DBusGMainLoop(set_as_default=True)53DBusGMainLoop(set_as_default=True)
55session = dbus.SessionBus()54session = dbus.SessionBus()
5655
5756
58def configure_logging():57def dumps(obj):
59 format = [58 return json.dumps(obj, sort_keys=True, separators=(',', ': '), indent=4)
60 '%(levelname)s',59
61 '%(message)s',60
62 ]61class Service(dbus.service.Object):
63 script = path.abspath(sys.argv[0])62 httpd = None
64 namespace = path.basename(script)63 avahi = None
65 cache = path.join(xdg.BaseDirectory.xdg_cache_home, 'dmedia')64
66 if not path.exists(cache):65 def __init__(self, bus, env_s):
67 os.makedirs(cache)66 self.bus = bus
68 filename = path.join(cache, namespace + '.log')67 self.env_s = env_s
69 if path.exists(filename):68 self.mainloop = GObject.MainLoop()
70 os.rename(filename, filename + '.previous')69 log.info('DBus: binding to %r', bus)
71 logging.basicConfig(70 super().__init__(session, object_path='/')
72 filename=filename,71 self.busname = dbus.service.BusName(bus, session)
73 filemode='w',72
74 level=logging.DEBUG,73 def start_core(self):
75 format='\t'.join(format),74 if self.env_s is None:
76 )75 self.dc3 = session.get_object('org.freedesktop.DC3', '/')
77 logging.info('script: %r', script)76 env = json.loads(self.dc3.GetEnv())
78 logging.info('dmedia.__file__: %r', __file__)77 else:
79 logging.info('dmedia.__version__: %r', __version__)78 env = json.loads(self.env_s)
8079 self.udisks = UDisks()
8180 self.core = Core(env, self.udisks.get_parentdir_info)
82class DMedia(dbus.service.Object):81 self.env_s = dumps(self.core.env)
83 def __init__(self, bus, mainloop):82 if len(self.core.stores) == 0:
84 self._killed = False83 self.core.add_filestore('/home')
85 self._bus = bus84 self.udisks.connect('store_added', self.on_store_added)
86 self._mainloop = mainloop85 self.udisks.connect('store_removed', self.on_store_removed)
8786 self.udisks.monitor()
88 log.info('Binding to %r', bus)87
89 super(DMedia, self).__init__(session, object_path='/')88 def start_httpd(self):
90 self._busname = dbus.service.BusName(bus, session)89 (self.httpd, self.port) = start_file_server(self.core.env)
9190 self.avahi = Avahi(self.core.env, self.port)
92 self._dc3 = session.get_object('org.freedesktop.DC3', '/')91 self.avahi.run()
93 self._machine_id = None92
94 env = self._get_env()93 def run(self):
9594 self.start_core()
96 log.info('Starting %r', service3)95 self.start_httpd()
97 self._child = Popen([service3, '--bus', bus])96 self.mainloop.run()
98 db = Database('dmedia-0', env)97
99 while True:98 def kill(self):
100 try:99 if self.avahi is not None:
101 time.sleep(0.1)100 self.avahi.free()
102 doc = db.get('_local/dmedia')101 if self.httpd is not None:
103 self._machine_id = doc['machine_id']102 self.httpd.terminate()
104 log.info('machine_id = %r', self._machine_id)103 self.httpd.join()
105 break104 self.mainloop.quit()
106 except NotFound:105
107 pass106 def on_store_added(self, udisks, obj, parentdir, partition, drive):
108107 log.info('UDisks store_added: %r', parentdir)
109 def _get_env(self):108 try:
110 log.info('Calling DC3.GetEnv()')109 self.AddFileStore(parentdir)
111 env = json.loads(self._dc3.GetEnv())110 except Exception:
112 env['machine_id'] = self._machine_id111 log.exception('Could not add FileStore %r', parentdir)
113 return env112
113 def on_store_removed(self, udisks, obj, parentdir):
114 log.info('UDisks store_removed: %r', parentdir)
115 try:
116 self.RemoveFileStore(parentdir)
117 except Exception:
118 log.exception('Could not remove FileStore %r', parentdir)
114119
115 @dbus.service.method(IFACE, in_signature='', out_signature='s')120 @dbus.service.method(IFACE, in_signature='', out_signature='s')
116 def Version(self):121 def Version(self):
117 """122 """
118 Return dmedia version.123 Return dmedia version.
119 """124 """
120 return __version__125 return dmedia.__version__
121126
122 @dbus.service.method(IFACE, in_signature='', out_signature='')127 @dbus.service.method(IFACE, in_signature='', out_signature='i')
123 def Kill(self):128 def Kill(self):
124 """129 """
125 Kill the `dmedia-service` process.130 Kill the `dmedia-service` process.
126 """131 """
127 if self._killed:132 self.kill()
128 return133 return int(time.time() - start_time)
129 self._killed = True
130 log.info('Killing dmedia core service on %r', self._bus)
131 self.FwdKill()
132 self._child.wait()
133 self._mainloop.quit()
134
135 @dbus.service.signal(IFACE, signature='')
136 def FwdKill(self):
137 pass
138134
139 @dbus.service.method(IFACE, in_signature='', out_signature='s')135 @dbus.service.method(IFACE, in_signature='', out_signature='s')
140 def GetEnv(self):136 def GetEnv(self):
141 """137 """
142 Return dmedia env as JSON string.138 Return dmedia env as JSON string.
143 """139 """
144 return json.dumps(self._get_env())140 return self.env_s
145141
146 @dbus.service.method(IFACE, in_signature='s', out_signature='')142 @dbus.service.method(IFACE, in_signature='', out_signature='s')
143 def LocalDmedia(self):
144 """
145 Return the _local/dmedia doc.
146 """
147 return dumps(self.core.db.get('_local/dmedia'))
148
149 @dbus.service.method(IFACE, in_signature='', out_signature='s')
150 def LocalPeers(self):
151 """
152 Return the _local/peers doc.
153 """
154 return dumps(self.core.db.get('_local/peers'))
155
156 @dbus.service.method(IFACE, in_signature='s', out_signature='s')
147 def AddFileStore(self, parentdir):157 def AddFileStore(self, parentdir):
148 self.FwdAddFileStore(parentdir)158 fs = self.core.add_filestore(parentdir)
149159 return self.LocalDmedia()
150 @dbus.service.signal(IFACE, signature='s')160
151 def FwdAddFileStore(self, parentdir):161 @dbus.service.method(IFACE, in_signature='s', out_signature='s')
152 pass
153
154 @dbus.service.method(IFACE, in_signature='s', out_signature='')
155 def RemoveFileStore(self, parentdir):162 def RemoveFileStore(self, parentdir):
156 self.FwdRemoveFileStore(parentdir)163 fs = self.core.remove_filestore(parentdir)
157164 return self.LocalDmedia()
158 @dbus.service.signal(IFACE, signature='s')165
159 def FwdRemoveFileStore(self, parentdir):166 @dbus.service.method(IFACE, in_signature='s', out_signature='s')
160 pass167 def Resolve(self, _id):
161168 return self.core.resolve(_id)
162169
163parser = argparse.ArgumentParser(170
164 description='DBus service @{}'.format(BUS),171parser = optparse.OptionParser(
165)172 version=dmedia.__version__,
166parser.add_argument('--version', action='version', version=__version__)173)
167parser.add_argument('--bus',174parser.add_option('--env',
168 default=BUS,175 help='JSON-encoded CouchDB environment (for unit testing)',
169 help='DBus bus name; default is %(default)r',176)
170)177parser.add_option('--bus',
171args = parser.parse_args()178 help='DBus bus name; default is {!r}'.format(BUS),
172configure_logging()179 default=BUS
173180)
174from gi.repository import GObject181(options, args) = parser.parse_args()
175GObject.threads_init()182
176183
177mainloop = GObject.MainLoop()184dmedia.configure_logging()
178dmedia = DMedia(args.bus, mainloop)185service = Service(options.bus, options.env)
179mainloop.run()186try:
180dmedia.Kill()187 service.run()
188finally:
189 service.kill()
181190
=== removed file 'dmedia-service3'
--- dmedia-service3 2011-12-27 10:02:37 +0000
+++ dmedia-service3 1970-01-01 00:00:00 +0000
@@ -1,181 +0,0 @@
1#!/usr/bin/python3
2
3# dmedia: distributed media library
4# Copyright (C) 2011 Novacut Inc
5#
6# This file is part of `dmedia`.
7#
8# `dmedia` is free software: you can redistribute it and/or modify it under
9# the terms of the GNU Affero General Public License as published by the Free
10# Software Foundation, either version 3 of the License, or (at your option) any
11# later version.
12#
13# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
14# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
16# details.
17#
18# You should have received a copy of the GNU Affero General Public License along
19# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
20#
21# Authors:
22# Jason Gerard DeRose <jderose@novacut.com>
23
24"""
25The real Python3 dmedia DBus service (well, almost).
26"""
27
28import sys
29import json
30from os import path
31import optparse
32import logging
33
34from gi.repository import GObject
35from microfiber import NotFound
36
37import dmedia
38from dmedia.service import dbus
39from dmedia.service.dbus import session, UDisks
40from dmedia.core import Core, start_file_server
41
42GObject.threads_init()
43log = logging.getLogger()
44
45
46class DMedia:
47 def __init__(self, bus):
48 self.mainloop = GObject.MainLoop()
49 dc3 = session.get('org.freedesktop.DC3', '/')
50 env = json.loads(dc3.GetEnv())
51 self.udisks = UDisks()
52 self.core = Core(env, self.udisks.get_parentdir_info)
53 if len(self.core.stores) == 0:
54 self.core.add_filestore('/home')
55 self._fwd = {
56 'AddFileStore': self.AddFileStore,
57 'RemoveFileStore': self.RemoveFileStore,
58 'Kill': self.Kill,
59 }
60 if bus:
61 log.info('Listing to forwards from %r', bus)
62 self.proxy = session.get(bus, '/', 'org.freedesktop.DMedia')
63 self.proxy.connect('g-signal', self.on_g_signal)
64 self.avahi = dbus.system.get(
65 'org.freedesktop.Avahi',
66 '/',
67 'org.freedesktop.Avahi.Server'
68 )
69 try:
70 self._peers = self.core.db.get('_local/peers')
71 except NotFound:
72 self._peers = {'_id': '_local/peers'}
73 self._peers['peers'] = {}
74 self.core.db.save(self._peers)
75 self.udisks.connect('store_added', self.on_store_added)
76 self.udisks.connect('store_removed', self.on_store_removed)
77 self.udisks.monitor()
78
79 def run(self):
80 (self.httpd, self.port) = start_file_server(self.core.env)
81 self.group = dbus.system.get(
82 'org.freedesktop.Avahi',
83 self.avahi.EntryGroupNew(),
84 'org.freedesktop.Avahi.EntryGroup'
85 )
86 self.group.AddService('(iiussssqaay)',
87 -1, # Interface
88 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
89 0, # Flags
90 self.core.machine_id,
91 '_dmedia._tcp',
92 '', # Domain, default to .local
93 '', # Host, default to localhost
94 self.port, # Port
95 None # TXT record
96 )
97 self.group.Commit()
98 browser_path = self.avahi.ServiceBrowserNew('(iissu)',
99 -1, # Interface
100 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
101 '_dmedia._tcp',
102 'local',
103 0 # Flags
104 )
105 self.browser = dbus.system.get(
106 'org.freedesktop.Avahi',
107 browser_path,
108 'org.freedesktop.Avahi.ServiceBrowser'
109 )
110 self.browser.connect('g-signal', self.on_browser_g_signal)
111 self.mainloop.run()
112
113 def on_store_added(self, udisks, obj, parentdir, partition, drive):
114 log.info('UDisks store_added: %r', parentdir)
115 try:
116 self.AddFileStore(parentdir)
117 except Exception:
118 log.exception('Could not add FileStore %r', parentdir)
119
120 def on_store_removed(self, udisks, obj, parentdir):
121 log.info('UDisks store_removed: %r', parentdir)
122 try:
123 self.RemoveFileStore(parentdir)
124 except Exception:
125 log.exception('Could not remove FileStore %r', parentdir)
126
127 def on_g_signal(self, proxy, sender, signal, params):
128 if signal.startswith('Fwd'):
129 name = signal[3:]
130 args = params.unpack()
131 try:
132 self._fwd[name](*args)
133 except KeyError:
134 pass
135
136 def on_browser_g_signal(self, proxy, sender, signal, params):
137 if signal == 'ItemNew':
138 (interface, protocol, name, _type, domain, flags) = params.unpack()
139 if name != self.core.machine_id: # Ignore what we publish ourselves
140 (ip, port) = self.avahi.ResolveService('(iisssiu)',
141 interface, protocol, name, _type, domain, -1, 0
142 )[7:9]
143 url = 'http://{}:{}/'.format(ip, port)
144 log.info('New peer %r at %r', name, url)
145 self._peers['peers'][name] = url
146 self.core.db.save(self._peers)
147 elif signal == 'ItemRemove':
148 (interface, protocol, name, _type, domain, flags) = params.unpack()
149 log.info('Removing peer %r', name)
150 try:
151 del self._peers['peers'][name]
152 self.core.db.save(self._peers)
153 except KeyError:
154 pass
155
156 def AddFileStore(self, parentdir):
157 self.core.add_filestore(path.abspath(parentdir))
158
159 def RemoveFileStore(self, parentdir):
160 self.core.remove_filestore(path.abspath(parentdir))
161
162 def Kill(self):
163 self.group.Reset()
164 self.httpd.terminate()
165 self.httpd.join()
166 self.mainloop.quit()
167
168
169parser = optparse.OptionParser(
170 usage='Usage: %prog FILE',
171 version=dmedia.__version__,
172)
173parser.add_option('--bus',
174 help='If provided, expect Python2 shim on this DBus bus',
175)
176(options, args) = parser.parse_args()
177
178dmedia.configure_logging()
179service = DMedia(options.bus)
180service.run()
181service.Kill()
1820
=== modified file 'dmedia/__init__.py'
--- dmedia/__init__.py 2011-12-27 07:21:46 +0000
+++ dmedia/__init__.py 2012-01-17 04:20:28 +0000
@@ -27,6 +27,7 @@
27"""27"""
2828
29__version__ = '12.01.0'29__version__ = '12.01.0'
30BUS = 'org.freedesktop.Dmedia'
3031
3132
32def configure_logging():33def configure_logging():
3334
=== modified file 'dmedia/constants.py'
--- dmedia/constants.py 2012-01-03 02:12:57 +0000
+++ dmedia/constants.py 2012-01-17 04:20:28 +0000
@@ -45,10 +45,8 @@
4545
4646
47# D-Bus releated:47# D-Bus releated:
48BUS = 'org.freedesktop.DMedia'48BUS = 'org.freedesktop.Dmedia'
49IFACE = BUS49IFACE = BUS
50IMPORT_BUS = 'org.freedesktop.DMediaImporter'
51IMPORT_IFACE = IMPORT_BUS
52DC_BUS = 'org.desktopcouch.CouchDB'50DC_BUS = 'org.desktopcouch.CouchDB'
53DC_INTERFACE = DC_BUS51DC_INTERFACE = DC_BUS
5452
5553
=== modified file 'dmedia/core.py'
--- dmedia/core.py 2012-01-02 23:49:23 +0000
+++ dmedia/core.py 2012-01-17 04:20:28 +0000
@@ -39,6 +39,7 @@
39from microfiber import Database, NotFound, random_id239from microfiber import Database, NotFound, random_id2
40from filestore import FileStore, check_root_hash, check_id40from filestore import FileStore, check_root_hash, check_id
4141
42import dmedia
42from dmedia import schema43from dmedia import schema
43from dmedia.local import LocalStores44from dmedia.local import LocalStores
44from dmedia.views import init_views45from dmedia.views import init_views
@@ -133,6 +134,7 @@
133 self.db.save(machine)134 self.db.save(machine)
134 self.machine_id = self.local['machine_id']135 self.machine_id = self.local['machine_id']
135 self.env['machine_id'] = self.machine_id136 self.env['machine_id'] = self.machine_id
137 self.env['version'] = dmedia.__version__
136 log.info('machine_id = %r', self.machine_id)138 log.info('machine_id = %r', self.machine_id)
137139
138 def _init_stores(self):140 def _init_stores(self):
@@ -207,3 +209,7 @@
207 assert set(self.local['stores']) == set(self.stores.parentdirs)209 assert set(self.local['stores']) == set(self.stores.parentdirs)
208 assert set(s['id'] for s in self.local['stores'].values()) == set(self.stores.ids)210 assert set(s['id'] for s in self.local['stores'].values()) == set(self.stores.ids)
209211
212 def resolve(self, _id):
213 doc = self.db.get(_id)
214 fs = self.stores.choose_local_store(doc)
215 return fs.stat(_id).name
210216
=== modified file 'dmedia/service/api.py'
--- dmedia/service/api.py 2011-10-24 21:36:05 +0000
+++ dmedia/service/api.py 2012-01-17 04:20:28 +0000
@@ -30,10 +30,10 @@
3030
31class DMedia:31class DMedia:
32 """32 """
33 Talk to "org.freedesktop.DMedia".33 Talk to "org.freedesktop.Dmedia".
34 """34 """
3535
36 def __init__(self, bus='org.freedesktop.DMedia'):36 def __init__(self, bus='org.freedesktop.Dmedia'):
37 self.bus = bus37 self.bus = bus
38 self._proxy = None38 self._proxy = None
3939
4040
=== added file 'dmedia/service/avahi.py'
--- dmedia/service/avahi.py 1970-01-01 00:00:00 +0000
+++ dmedia/service/avahi.py 2012-01-17 04:20:28 +0000
@@ -0,0 +1,116 @@
1# dmedia: distributed media library
2# Copyright (C) 2011 Novacut Inc
3#
4# This file is part of `dmedia`.
5#
6# `dmedia` is free software: you can redistribute it and/or modify it under the
7# terms of the GNU Affero General Public License as published by the Free
8# Software Foundation, either version 3 of the License, or (at your option) any
9# later version.
10#
11# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY
12# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
13# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
14# details.
15#
16# You should have received a copy of the GNU Affero General Public License along
17# with `dmedia`. If not, see <http://www.gnu.org/licenses/>.
18#
19# Authors:
20# Jason Gerard DeRose <jderose@novacut.com>
21
22"""
23Advertise Dmedia HTTP server over Avahi, discover other peers.
24"""
25
26import logging
27
28from microfiber import Database, NotFound
29
30from dmedia.service.dbus import system
31
32
33PEERS = '_local/peers'
34log = logging.getLogger()
35
36
37class Avahi:
38 group = None
39
40 def __init__(self, env, port):
41 self.avahi = system.get(
42 'org.freedesktop.Avahi',
43 '/',
44 'org.freedesktop.Avahi.Server'
45 )
46 self.db = Database('dmedia-0', env)
47 self.machine_id = env['machine_id']
48 self.port = port
49
50 def __del__(self):
51 self.free()
52
53 def run(self):
54 try:
55 self.peers = self.db.get(PEERS)
56 if self.peers.get('peers') != {}:
57 self.peers['peers'] = {}
58 self.db.save(self.peers)
59 except NotFound:
60 self.peers = {'_id': PEERS, 'peers': {}}
61 self.db.save(self.peers)
62 self.group = system.get(
63 'org.freedesktop.Avahi',
64 self.avahi.EntryGroupNew(),
65 'org.freedesktop.Avahi.EntryGroup'
66 )
67 log.info('Avahi: advertising %r on port %r', self.machine_id, self.port)
68 self.group.AddService('(iiussssqaay)',
69 -1, # Interface
70 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
71 0, # Flags
72 self.machine_id,
73 '_dmedia._tcp',
74 '', # Domain, default to .local
75 '', # Host, default to localhost
76 self.port, # Port
77 None # TXT record
78 )
79 self.group.Commit()
80 browser_path = self.avahi.ServiceBrowserNew('(iissu)',
81 -1, # Interface
82 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
83 '_dmedia._tcp',
84 'local',
85 0 # Flags
86 )
87 self.browser = system.get(
88 'org.freedesktop.Avahi',
89 browser_path,
90 'org.freedesktop.Avahi.ServiceBrowser'
91 )
92 self.browser.connect('g-signal', self.on_g_signal)
93
94 def free(self):
95 if self.group is not None:
96 self.group.Reset()
97
98 def on_g_signal(self, proxy, sender, signal, params):
99 if signal == 'ItemNew':
100 (interface, protocol, name, _type, domain, flags) = params.unpack()
101 if name != self.machine_id: # Ignore what we publish ourselves
102 (ip, port) = self.avahi.ResolveService('(iisssiu)',
103 interface, protocol, name, _type, domain, -1, 0
104 )[7:9]
105 url = 'http://{}:{}/'.format(ip, port)
106 log.info('Avahi: new peer %r at %r', name, url)
107 self.peers['peers'][name] = url
108 self.db.save(self.peers)
109 elif signal == 'ItemRemove':
110 (interface, protocol, name, _type, domain, flags) = params.unpack()
111 log.info('Avahi: removing peer %r', name)
112 try:
113 del self.peers['peers'][name]
114 self.db.save(self.peers)
115 except KeyError:
116 pass
0117
=== modified file 'dmedia/transfers.py'
--- dmedia/transfers.py 2011-09-22 10:20:44 +0000
+++ dmedia/transfers.py 2012-01-17 04:20:28 +0000
@@ -41,7 +41,7 @@
41_uploaders = {}41_uploaders = {}
42_downloaders = {}42_downloaders = {}
4343
44# Note: should probably export each download on the org.freedesktop.DMedia bus44# Note: should probably export each download on the org.freedesktop.Dmedia bus
45# at the object path /downloads/FILE_ID45# at the object path /downloads/FILE_ID
4646
47def download_key(file_id, store_id):47def download_key(file_id, store_id):
@@ -63,7 +63,7 @@
63 """63 """
64 return '/downloads/' + file_id64 return '/downloads/' + file_id
6565
66# Note: should probably export each upload on the org.freedesktop.DMedia bus66# Note: should probably export each upload on the org.freedesktop.Dmedia bus
67# at the object path /uploads/FILE_ID/REMOTE_ID67# at the object path /uploads/FILE_ID/REMOTE_ID
6868
69def upload_key(file_id, store_id):69def upload_key(file_id, store_id):
7070
=== modified file 'dmedia/units.py'
--- dmedia/units.py 2011-10-08 11:18:57 +0000
+++ dmedia/units.py 2012-01-17 04:20:28 +0000
@@ -68,3 +68,16 @@
68 return (68 return (
69 '{:.3g} {}'.format(s, BYTES10[i])69 '{:.3g} {}'.format(s, BYTES10[i])
70 )70 )
71
72
73def minsec(seconds):
74 """
75 Format *seconds* as a M:SS string with minutes and seconds.
76
77 For example:
78
79 >>> minsec(123)
80 '2:03'
81
82 """
83 return '{:d}:{:02d}'.format(seconds // 60, seconds % 60)
7184
=== modified file 'setup.py'
--- setup.py 2012-01-15 15:21:24 +0000
+++ setup.py 2012-01-17 04:20:28 +0000
@@ -159,12 +159,11 @@
159 ('lib/dmedia',159 ('lib/dmedia',
160 [160 [
161 'dmedia-service',161 'dmedia-service',
162 'dmedia-service3',
163 'share/init-filestore',162 'share/init-filestore',
164 ]163 ]
165 ),164 ),
166 ('share/dbus-1/services/',165 ('share/dbus-1/services/',
167 ['share/org.freedesktop.DMedia.service']166 ['share/org.freedesktop.Dmedia.service']
168 ),167 ),
169 ],168 ],
170)169)
171170
=== renamed file 'share/org.freedesktop.DMedia.service' => 'share/org.freedesktop.Dmedia.service'
--- share/org.freedesktop.DMedia.service 2011-04-10 11:02:23 +0000
+++ share/org.freedesktop.Dmedia.service 2012-01-17 04:20:28 +0000
@@ -1,3 +1,3 @@
1[D-BUS Service]1[D-BUS Service]
2Name=org.freedesktop.DMedia2Name=org.freedesktop.Dmedia
3Exec=/usr/lib/dmedia/dmedia-service3Exec=/usr/lib/dmedia/dmedia-service
44
=== added file 'test-cli.py'
--- test-cli.py 1970-01-01 00:00:00 +0000
+++ test-cli.py 2012-01-17 04:20:28 +0000
@@ -0,0 +1,33 @@
1#!/usr/bin/python3
2
3from usercouch.misc import TempCouch
4from microfiber import random_id
5import json
6import time
7from subprocess import Popen, check_output
8
9bus = 'tmp' + random_id() + '.Dmedia'
10
11def call(*args):
12 cmd = ('./dmedia-cli', '--bus', bus) + args
13 print(check_output(cmd).decode('utf-8'))
14
15# Check that printing help doesn't connect to the DBus service:
16call()
17
18# Start a tmp couchdb and the dbus service on a random bus name:
19tmpcouch = TempCouch()
20env = tmpcouch.bootstrap()
21cmd = ['./dmedia-service', '--bus', bus, '--env', json.dumps(env)]
22child = Popen(cmd)
23time.sleep(1)
24
25try:
26 call('Version')
27 call('GetEnv')
28 call('LocalDmedia')
29 call('LocalPeers')
30 call('RemoveFileStore', '/home/')
31 call('AddFileStore', '/home/')
32finally:
33 call('Kill')
034
=== added file 'test-service.py'
--- test-service.py 1970-01-01 00:00:00 +0000
+++ test-service.py 2012-01-17 04:20:28 +0000
@@ -0,0 +1,29 @@
1#!/usr/bin/python3
2
3from usercouch.misc import TempCouch
4from microfiber import random_id
5import json
6import time
7from subprocess import Popen
8from dmedia.service.dbus import session
9
10bus = 'tmp' + random_id() + '.Dmedia'
11tmpcouch = TempCouch()
12env = tmpcouch.bootstrap()
13
14cmd = ['./dmedia-service', '--bus', bus, '--env', json.dumps(env)]
15
16child = Popen(cmd)
17time.sleep(2)
18
19proxy = session.get(bus, '/', 'org.freedesktop.Dmedia')
20try:
21 print(proxy.Version())
22 print(proxy.GetEnv())
23 print(proxy.LocalDmedia())
24 print(proxy.LocalPeers())
25 print(proxy.RemoveFileStore('(s)', '/home'))
26 print(proxy.AddFileStore('(s)', '/home'))
27finally:
28 print(proxy.Kill())
29

Subscribers

People subscribed via source and target branches