# upload_sync_controller # # Copyright (C) 2012-2017 Intel Corporation. All rights reserved. # # This program is free software; you can redistribute it and/or modify it # under the terms and conditions of the GNU Lesser General Public License, # version 2.1, as published by the Free Software Foundation. # # This program is distributed in the hope it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA. # # Regis Merlino # import ConfigParser import os import shutil import urllib import dbus import gio import glib from mediaconsole import UPNP, MediaObject, Container, Device class _UscUpnp(UPNP): def __init__(self): UPNP.__init__(self) def get_servers(self): return self._manager.GetServers() class _UscDevice(Device): def __init__(self, path): Device.__init__(self, path) def create_container_in_any(self, name, type, child_types): return self._deviceIF.CreateContainerInAnyContainer(name, type, child_types) class _UscContainer(Container): def __init__(self, path): Container.__init__(self, path) def create_container(self, name, type, child_types): return self._containerIF.CreateContainer(name, type, child_types) def upload(self, name, file_path): return self._containerIF.Upload(name, file_path) class UscError(Exception): """An Upload Sync Controller error.""" def __init__(self, message): """ message: description of the error """ Exception.__init__(self, message) self.message = message def __str__(self): return 'UscError: ' + self.message class _UscStore(object): REMOTE_ID_OPTION = 'remote_id' TYPE_OPTION = 'type' def __init__(self, root_path, server_id): if not os.path.exists(root_path): os.makedirs(root_path) self.__config_path = os.path.join(root_path, server_id + '.conf') self.__config = ConfigParser.ConfigParser() def initialize(self): self.__config.read(self.__config_path) def __write_config(self): with open(self.__config_path, 'wb') as configfile: self.__config.write(configfile) def remove(self): if os.path.exists(self.__config_path): os.remove(self.__config_path) def __parent_deleted(self, path, deleted_containers): for del_path in deleted_containers: if path.startswith(del_path): return True return False def remove_file(self, path): id = self.__config.get(path, self.REMOTE_ID_OPTION) print u'\tDeleting server object {0}'.format(id) try: obj = MediaObject(id) obj.delete() except: pass print u'\tDeleting local cache {0}'.format(path) self.__config.remove_section(path) def sync_deleted_files(self, path): deleted_containers = [] sections = self.__config.sections() if not sections: return for path in sections: if self.__parent_deleted(path, deleted_containers): print u'\tDeleting local cache {0}'.format(path) self.__config.remove_section(path) elif not os.path.exists(path): if self.__config.get(path, self.TYPE_OPTION) == 'container': deleted_containers.append(path + os.sep) self.remove_file(path) self.__write_config() def add_file(self, container, name, parent): new_path = os.path.join(parent, name) if os.path.isdir(new_path): if self.__config.has_section(new_path): id = self.__config.get(new_path, self.REMOTE_ID_OPTION) else: print u'\tCreating server container for {0}'.format(new_path) id = container.create_container(name, 'container', ['*']) print u'\tStoring cached data for {0}'.format(id) self.__config.add_section(new_path) self.__config.set(new_path, self.REMOTE_ID_OPTION, id) self.__config.set(new_path, self.TYPE_OPTION, 'container') new_container = _UscContainer(id) self.sync_added_files(new_container, new_path) elif not self.__config.has_section(new_path): print u'\tUploading file {0}'.format(new_path) (up, id) = container.upload(name, new_path) print u'\tStoring cached data for {0}'.format(id) self.__config.add_section(new_path) self.__config.set(new_path, self.REMOTE_ID_OPTION, id) self.__config.set(new_path, self.TYPE_OPTION, 'item') def sync_added_files(self, container, path): children = os.listdir(path) for child in children: self.add_file(container, child, path) self.__write_config() def set_root(self, path, id): self.__config.add_section(path) self.__config.set(path, self.REMOTE_ID_OPTION, id) self.__config.set(path, self.TYPE_OPTION, 'container') self.__write_config() def object_id_from_path(self, path): return self.__config.get(path, self.REMOTE_ID_OPTION) def rename_file(self, path1, path2): self.__config.add_section(path2) id = self.__config.get(path1, self.REMOTE_ID_OPTION) self.__config.set(path2, self.REMOTE_ID_OPTION, id) type = self.__config.get(path1, self.TYPE_OPTION) self.__config.set(path2, self.TYPE_OPTION, type) self.__config.remove_section(path1) self.__write_config() class UscController(object): """An Upload Sync Controller sample app. The Upload Sync Controller propagates changes in a local folder to media servers (DMS/M-DMS) to be added to their list of available content. Media servers must expose the 'content-synchronization' capability to be managed by this controller. The four main methods are servers(), track(), add_server() and start_sync(). * servers() lists the media servers available on the network * track() is used to set the local folder that is to be synchronized * add_server() is used to add a media server to the list of servers that are to be synchronized * start_sync() launches the servers synchronisation from the local storage Sample usage: >>> controller.servers() >>> controller.track('/home/user/local_folder') >>> controller.add_server('/com/intel/dLeynaServer/server/0') >>> controller.start_sync() """ CONFIG_PATH = os.path.join(os.environ['HOME'], '.config/upload-sync-controller.conf') ROOT_CONTAINER_ID_OPTION = 'root_container_id' DATA_PATH_SECTION = '__paths__' DATA_PATH_OPTION = 'data_path' TRACK_PATH_OPTION = 'track_path' def __init__(self, rel_path = None): """ rel_path: if provided, contains the relative local database path, from the user's HOME directory. If not provided, the local database path will be '$HOME/upload-sync-controller' """ self.__upnp = _UscUpnp() self.__config = ConfigParser.ConfigParser() self.__config.read(UscController.CONFIG_PATH) if rel_path: self.__set_data_path(rel_path) elif not self.__config.has_section(UscController.DATA_PATH_SECTION): self.__set_data_path('upload-sync-controller') self.__store_path = self.__config.get(UscController.DATA_PATH_SECTION, UscController.DATA_PATH_OPTION) try: self.__track_path = self.__config.get( UscController.DATA_PATH_SECTION, UscController.TRACK_PATH_OPTION) except: self.__track_path = None def __write_config(self): with open(UscController.CONFIG_PATH, 'wb') as configfile: self.__config.write(configfile) def __set_data_path(self, rel_path): data_path = os.environ['HOME'] + os.sep + rel_path if not self.__config.has_section(UscController.DATA_PATH_SECTION): self.__config.add_section(UscController.DATA_PATH_SECTION) self.__config.set(UscController.DATA_PATH_SECTION, UscController.DATA_PATH_OPTION, data_path) self.__write_config() def __check_trackable(self, server): try: try: srt = server.get_prop('ServiceResetToken') except: raise UscError("'ServiceResetToken' variable not supported") try: dlna_caps = server.get_prop('DLNACaps') if not 'content-synchronization' in dlna_caps: raise except: raise UscError("'content-synchronization' cap not supported") try: search_caps = server.get_prop('SearchCaps') if not [x for x in search_caps if 'ObjectUpdateID' in x]: raise if not [x for x in search_caps if 'ContainerUpdateID' in x]: raise except: raise UscError("'objectUpdateID' search cap not supported") return srt except UscError as err: print err return None def __need_sync(self, servers): for item in servers: device = _UscDevice(item) uuid = device.get_prop('UDN') if self.__config.has_section(uuid): yield device, uuid, self.__config.has_option(uuid, UscController.ROOT_CONTAINER_ID_OPTION) def __remove_monitor(self, path): for item in self.__monitor_list.keys(): if item.startswith(path): print u'\tStop monitoring {0}'.format(path) del self.__monitor_list[item] def __add_monitor(self, path): print u'\tStart monitoring {0}'.format(path) gfile = gio.File(path) monitor = gfile.monitor_directory(gio.FILE_MONITOR_SEND_MOVED, None) monitor.connect("changed", self.__directory_changed) self.__monitor_list[path] = monitor def __monitor_directory(self, path): self.__add_monitor(path) children = os.listdir(path) for child in children: new_path = os.path.join(path, child) if os.path.isdir(new_path): self.__monitor_directory(new_path) def __start_monitoring(self): self.__monitor_list = {} self.__monitor_directory(self.__track_path) print u'Type ctrl-c to stop.' try: main_loop = glib.MainLoop() main_loop.run() except: print u'Monitoring stopped.' def __directory_changed(self, monitor, file1, file2, evt_type): print if evt_type == gio.FILE_MONITOR_EVENT_CREATED: (parent, name) = os.path.split(file1.get_path()) for store in self.__store_list: parent_id = store.object_id_from_path(parent) container = _UscContainer(parent_id) store.add_file(container, name, parent) if os.path.isdir(file1.get_path()): self.__monitor_directory(file1.get_path()) elif evt_type == gio.FILE_MONITOR_EVENT_MOVED: (parent1, name1) = os.path.split(file1.get_path()) (parent2, name2) = os.path.split(file2.get_path()) renamed = (parent1 == parent2) if file1.get_path() in self.__monitor_list: self.__remove_monitor(file1.get_path()) for store in self.__store_list: object_id = store.object_id_from_path(file1.get_path()) obj = MediaObject(object_id) dlna_managed = obj.get_prop('DLNAManaged') if renamed and dlna_managed['ChangeMeta']: print u'\tRenaming {0} to {1}'.format(name1, name2) props = {'DisplayName' : name2} obj.update(props, []) store.rename_file(file1.get_path(), file2.get_path()) else: store.remove_file(file1.get_path()) parent_id = store.object_id_from_path(parent2) container = _UscContainer(parent_id) store.add_file(container, name2, parent2) if os.path.isdir(file2.get_path()): self.__monitor_directory(file2.get_path()) elif evt_type == gio.FILE_MONITOR_EVENT_DELETED: if file1.get_path() in self.__monitor_list: self.__remove_monitor(file1.get_path()) for store in self.__store_list: store.remove_file(file1.get_path()) else: return print u'Type ctrl-c to stop.' def track(self, track_path): """Sets the local folder that is to be synchronized.""" self.__track_path = track_path self.__config.set(UscController.DATA_PATH_SECTION, UscController.TRACK_PATH_OPTION, track_path) self.__write_config() def start_sync(self): """Performs a synchronization for all the media servers. Displays some progress information during the process. """ if not self.__track_path: print u'Error: track path is not set' return self.__store_list = [] print u'Syncing...' for device, uuid, init in self.__need_sync(self.__upnp.get_servers()): store = _UscStore(self.__store_path, uuid) store.initialize() self.__store_list.append(store) if not init: print u'Performing initial sync for server {0}:'.format(uuid) root = device.create_container_in_any('usc_root', 'container', ['image']) self.__config.set(uuid, UscController.ROOT_CONTAINER_ID_OPTION, root) self.__write_config() store.set_root(self.__track_path, root) else: print u'Performing normal sync for server {0}:'.format(uuid) root = self.__config.get(uuid, UscController.ROOT_CONTAINER_ID_OPTION) print u'\tRoot container is {0}'.format(root) root_container = _UscContainer(root) store.sync_deleted_files(self.__track_path) store.sync_added_files(root_container, self.__track_path) print u'Done.' if len(self.__store_list) == 0: print u'Nothing to do, stopping.' return print u'Now monitoring local changes...' self.__start_monitoring() def servers(self): """Displays media servers available on the network. Displays media servers information as well as the synchronized status. """ print u'Running servers:' for item in self.__upnp.get_servers(): try: server = Container(item) try: folder_name = server.get_prop('FriendlyName') except Exception: folder_name = server.get_prop('DisplayName') device = Device(item) dev_uuid = device.get_prop('UDN') dev_path = device.get_prop('Path') print u'{0:<25} Synchronized({2}) {3} {1}'.format( folder_name, dev_path, self.__config.has_section(dev_uuid), dev_uuid) except dbus.exceptions.DBusException as err: print u'Cannot retrieve properties for ' + item print str(err).strip()[:-1] def synchronized(self): """Displays the list of servers currently synchronized.""" print u'Synchronized servers:' for name in self.__config.sections(): if name != UscController.DATA_PATH_SECTION: print u'{0:<30}'.format(name) def add_server(self, server_path): """Adds a media server to the controller's list. server_path: d-bus path for the media server """ server = Device(server_path) server_uuid = server.get_prop('UDN') if not self.__config.has_section(server_uuid): srt = self.__check_trackable(server) if srt != None: self.__config.add_section(server_uuid) self.__write_config() else: print u"Sorry, the server {0} has no such capability and " \ "will not be synchronized.".format(server_path) def __remove_server(self, server_uuid): try: root = self.__config.get(server_uuid, UscController.ROOT_CONTAINER_ID_OPTION) MediaObject(root).delete() except: pass self.__config.remove_section(server_uuid) store = _UscStore(self.__store_path, server_uuid) store.remove() def remove_server(self, server_path): """Removes a media server from the controller's list. Also removes the server side synchronized file and folders. server_path: d-bus path for the media server """ server = Device(server_path) server_uuid = server.get_prop('UDN') if self.__config.has_section(server_uuid): self.__remove_server(server_uuid) self.__write_config() def reset(self): """Removes all media servers from the controller's list Also removes the server side synchronized file and folders. """ for name in self.__config.sections(): if name != UscController.DATA_PATH_SECTION: self.__remove_server(name) self.__write_config() if __name__ == '__main__': print u'An Upload Sync Controller sample app.' print controller = UscController() controller.servers() print print u'"controller" instance is ready for use.' print u'Type "help(UscController)" for more details and usage samples.'