Exemplo n.º 1
0
class FilesystemNotifier(base.NotifierBase):
    def __init__(self):
        super(FilesystemNotifier, self).__init__()

        self.notifier = INotify()
        self.notifier.startReading()

    def setFilename(self, filename):
        if self._path is not None:
            self.notifier.ignore(FilePath(self._path))
        super(FilesystemNotifier, self).setFilename(filename)
        if self._path is not None:
            self.notifier.watch(FilePath(self._path), callbacks=[self.__notify])

    def stop(self):
        if self.notifier is not None:
            self.notifier.stopReading()
            self.notifier = None

    def __notify(self, handle, filepath, mask):
        myName = self._filename
        if myName is not None:
            if filepath.basename() == os.path.split(myName)[-1]:
                if self._check(myName) and myName:
                    self.stamp = os.stat(myName).st_mtime
                    self.onFileChanged()

    def onFileChanged(self):
        raise NotImplementedError
Exemplo n.º 2
0
class FilesystemNotifier(base.NotifierBase):
    def __init__(self):
        super(FilesystemNotifier, self).__init__()

        self.notifier = INotify()
        self.notifier.startReading()

    def setFilename(self, filename):
        if self._path is not None:
            self.notifier.ignore(FilePath(self._path))
        super(FilesystemNotifier, self).setFilename(filename)
        if self._path is not None:
            self.notifier.watch(FilePath(self._path),
                                callbacks=[self.__notify])

    def stop(self):
        if self.notifier is not None:
            self.notifier.stopReading()
            self.notifier = None

    def __notify(self, handle, filepath, mask):
        myName = self._filename
        if myName is not None:
            if filepath.basename() == os.path.split(myName)[-1]:
                if self._check(myName) and myName:
                    self.stamp = os.stat(myName).st_mtime
                    self.onFileChanged()

    def onFileChanged(self):
        raise NotImplementedError
Exemplo n.º 3
0
class FSStore(BackendStore):
    '''
    .. versionchanged:: 0.9.0
        Migrated from louie/dispatcher to EventDispatcher
    '''

    logCategory = 'fs_store'

    implements = ['MediaServer']

    description = '''MediaServer exporting files from the file-system'''

    options = [
        {
            'option':
            'name',
            'type':
            'string',
            'default':
            'my media',
            'help':
            'the name under this MediaServer '
            'shall show up with on other UPnP clients',
        },
        {
            'option': 'version',
            'type': 'int',
            'default': 2,
            'enum': (2, 1),
            'help': 'the highest UPnP version this MediaServer shall support',
            'level': 'advance',
        },
        {
            'option': 'uuid',
            'type': 'string',
            'help': 'the unique (UPnP) identifier for this MediaServer,'
            ' usually automatically set',
            'level': 'advance',
        },
        {
            'option': 'content',
            'type': 'string',
            'default': xdg_content(),
            'help': 'the path(s) this MediaServer shall export',
        },
        {
            'option': 'ignore_patterns',
            'type': 'string',
            'help':
            'list of regex patterns, matching filenames will be ignored',  # noqa: E501
        },
        {
            'option': 'enable_inotify',
            'type': 'string',
            'default': 'yes',
            'help': 'enable real-time monitoring of the content folders',
        },
        {
            'option': 'enable_destroy',
            'type': 'string',
            'default': 'no',
            'help': 'enable deleting a file via an UPnP method',
        },
        {
            'option':
            'import_folder',
            'type':
            'string',
            'help':
            'The path to store files imported via an UPnP method, '
            'if empty the Import method is disabled',
        },
    ]

    def __init__(self, server, **kwargs):
        BackendStore.__init__(self, server, **kwargs)
        self.next_id = 1000
        self.name = kwargs.get('name', 'my media')
        self.content = kwargs.get('content', None)
        if self.content is not None:
            if isinstance(self.content, str):
                self.content = [self.content]
            cl = []
            for a in self.content:
                cl += a.split(',')
            self.content = cl
        else:
            self.content = xdg_content()
            self.content = [x[0] for x in self.content]
        if self.content is None:
            self.content = 'tests/content'
        if not isinstance(self.content, list):
            self.content = [self.content]
        self.content = set([os.path.abspath(x) for x in self.content])
        ignore_patterns = kwargs.get('ignore_patterns', [])
        self.store = {}

        self.inotify = None

        if kwargs.get('enable_inotify', 'yes') == 'yes':
            if INotify:
                try:
                    self.inotify = INotify()
                    self.inotify.startReading()
                except Exception as msg:
                    self.error(f'inotify disabled: {msg}')
                    self.inotify = None
            else:
                self.info(f'{no_inotify_reason}')
        else:
            self.info('FSStore content auto-update disabled upon user request')

        if kwargs.get('enable_destroy', 'no') == 'yes':
            self.upnp_DestroyObject = self.hidden_upnp_DestroyObject

        self.import_folder = kwargs.get('import_folder', None)
        if self.import_folder is not None:
            self.import_folder = os.path.abspath(self.import_folder)
            if not os.path.isdir(self.import_folder):
                self.import_folder = None

        self.ignore_file_pattern = re.compile(r'|'.join([r'^\..*'] +
                                                        list(ignore_patterns)))
        parent = None
        self.update_id = 0
        if (len(self.content) > 1
                or utils.means_true(kwargs.get('create_root', False))
                or self.import_folder is not None):
            UPnPClass = classChooser('root')
            id = str(self.getnextID())
            try:
                parent = self.store[id] = FSItem(
                    id,
                    parent,
                    'media',
                    'root',
                    self.urlbase,
                    UPnPClass,
                    update=True,
                    store=self,
                )
            except Exception as e:
                self.error(
                    f'Error on setting self.store[id], Error on FSItem: {e}')
                exit(1)

        if self.import_folder is not None:
            id = str(self.getnextID())
            self.store[id] = FSItem(
                id,
                parent,
                self.import_folder,
                'directory',
                self.urlbase,
                UPnPClass,
                update=True,
                store=self,
            )
            self.import_folder_id = id
        for bytesPath in self.content:
            if isinstance(bytesPath, (list, tuple)):
                path = str(bytesPath[0])
            else:
                path = str(bytesPath)
            if self.ignore_file_pattern.match(path):
                continue
            try:
                self.walk(path, parent, self.ignore_file_pattern)
            except Exception as msg:
                self.warning(f'on walk of {path!r}: {msg!r}')
                import traceback

                self.debug(traceback.format_exc())

        self.wmc_mapping.update({'14': '0', '15': '0', '16': '0', '17': '0'})

        self.init_completed = True

    def __repr__(self):
        return self.__class__.__name__

    def release(self):
        if self.inotify is not None:
            self.inotify.stopReading()

    def len(self):
        return len(self.store)

    def get_by_id(self, id):
        # print('get_by_id', id, type(id))
        # we have referenced ids here when we are in WMC mapping mode
        if isinstance(id, str):
            id = id.split('@', 1)[0]
        elif isinstance(id, bytes):
            id = id.decode('utf-8').split('@', 1)[0]
        elif isinstance(id, int):
            id = str(id)
        # try:
        #    id = int(id)
        # except ValueError:
        #    id = 1000

        if id == '0':
            id = '1000'
        # print('get_by_id 2', id)
        try:
            r = self.store[id]
        except KeyError:
            r = None
        # print('get_by_id 3', r)
        return r

    def get_id_by_name(self, parent='0', name=''):
        self.info(f'get_id_by_name {parent} ({type(parent)}) {name}')
        try:
            parent = self.store[parent]
            self.debug(f'{parent} {len(parent.children):d}')
            for child in parent.children:
                # if not isinstance(name, unicode):
                #    name = name.decode('utf8')
                self.debug(f'{child.get_name()} {child.get_realpath()} ' +
                           f'{name == child.get_realpath()}')
                if name == child.get_realpath():
                    return child.id
        except Exception as e:
            self.error(f'get_id_by_name: {e!r}')
            import traceback

            self.info(traceback.format_exc())
        self.debug('get_id_by_name not found')

        return None

    def get_url_by_name(self, parent='0', name=''):
        self.info(f'get_url_by_name {parent!r} {name!r}')
        id = self.get_id_by_name(parent, name)
        # print 'get_url_by_name', id
        if id is None:
            return ''
        return self.store[id].url

    def update_config(self, **kwargs):
        self.info(f'update_config: {kwargs}')
        if 'content' in kwargs:
            new_content = kwargs['content']
            new_content = set(
                [os.path.abspath(x) for x in new_content.split(',')])
            new_folders = new_content.difference(self.content)
            obsolete_folders = self.content.difference(new_content)
            self.debug(f'new folders: {new_folders}\n'
                       f'obsolete folders: {obsolete_folders}')
            for folder in obsolete_folders:
                self.remove_content_folder(folder)
            for folder in new_folders:
                self.add_content_folder(folder)
            self.content = new_content

    def add_content_folder(self, path):
        path = os.path.abspath(path)
        if path not in self.content:
            self.content.add(path)
            self.walk(path, self.store['1000'], self.ignore_file_pattern)

    def remove_content_folder(self, path):
        path = os.path.abspath(path)
        if path in self.content:
            id = self.get_id_by_name('1000', path)
            self.remove(id)
            self.content.remove(path)

    def walk(self, path, parent=None, ignore_file_pattern=''):
        self.debug(f'walk {path}')
        containers = []
        parent = self.append(path, parent)
        if parent is not None:
            containers.append(parent)
        while len(containers) > 0:
            container = containers.pop()
            try:
                self.debug(f'adding {container.location!r}')
                self.info(f'walk.adding: {container.location}')
                for child in container.location.children():
                    if ignore_file_pattern.match(child.basename()) is not None:
                        continue
                    new_container = self.append(child.path, container)
                    if new_container is not None:
                        containers.append(new_container)
            except UnicodeDecodeError:
                self.warning(
                    f'UnicodeDecodeError - there is something wrong with a ' +
                    f'file located in {container.get_path()!r}')

    def create(self, mimetype, path, parent):
        self.debug(f'create  {mimetype} {path} {type(path)} {parent}')
        UPnPClass = classChooser(mimetype)
        if UPnPClass is None:
            return None

        id = self.getnextID()
        if mimetype in ('root', 'directory'):
            id = str(id)
        else:
            _, ext = os.path.splitext(path)
            id = str(id) + ext.lower()
        update = False
        if hasattr(self, 'update_id'):
            update = True

        self.store[id] = FSItem(
            id,
            parent,
            path,
            mimetype,
            self.urlbase,
            UPnPClass,
            update=True,
            store=self,
        )
        if hasattr(self, 'update_id'):
            self.update_id += 1
            # print(self.update_id)
            if self.server:
                if hasattr(self.server, 'content_directory_server'):
                    self.server.content_directory_server.set_variable(
                        0, 'SystemUpdateID', self.update_id)
            if parent is not None:
                value = (parent.get_id(), parent.get_update_id())
                if self.server:
                    if hasattr(self.server, 'content_directory_server'):
                        self.server.content_directory_server.set_variable(
                            0, 'ContainerUpdateIDs', value)

        return id

    def append(self, bytes_path, parent):
        path = str(bytes_path)
        self.debug(f'append  {path} {type(path)} {parent}')
        if not os.path.exists(path):
            self.warning(f'path {path!r} not available - ignored')
            return None

        if stat.S_ISFIFO(os.stat(path).st_mode):
            self.warning(f'path {path!r} is a FIFO - ignored')
            return None

        try:
            mimetype, _ = mimetypes.guess_type(path, strict=False)
            if mimetype is None:
                if os.path.isdir(path):
                    mimetype = 'directory'
            if mimetype is None:
                return None

            id = self.create(mimetype, path, parent)

            if mimetype == 'directory':
                if self.inotify is not None:
                    mask = (IN_CREATE
                            | IN_DELETE
                            | IN_MOVED_FROM
                            | IN_MOVED_TO
                            | IN_CHANGED)
                    self.inotify.watch(
                        FilePath(os.path.abspath(path)),
                        mask=mask,
                        autoAdd=False,
                        callbacks=[partial(self.notify, parameter=id)],
                    )
                return self.store[id]
        except OSError as os_msg:
            # seems we have some permissions issues along the content path
            self.warning(f'path {path} isn\'t accessible, error {os_msg}')

        return None

    def remove(self, id):
        self.debug(f'FSSTore remove id: {id}')
        try:
            item = self.store[id]
            parent = item.get_parent()
            item.remove()
            del self.store[id]
            if hasattr(self, 'update_id'):
                self.update_id += 1
                if self.server:
                    self.server.content_directory_server.set_variable(
                        0, 'SystemUpdateID', self.update_id)
                # value = f'{parent.get_id():d},{parent_get_update_id():d}'
                value = (parent.get_id(), parent.get_update_id())
                if self.server:
                    self.server.content_directory_server.set_variable(
                        0, 'ContainerUpdateIDs', value)

        except KeyError:
            pass

    def notify(self, ignore, path, mask, parameter=None):
        self.info(
            'Event %s on %s - parameter %r',
            ', '.join([fl for fl in _FLAG_TO_HUMAN if fl[0] == mask][0]),
            path.path,
            parameter,
        )

        if mask & IN_CHANGED:
            # FIXME react maybe on access right changes, loss of read rights?
            # print(f'{path} was changed, parent {parameter:d} ({iwp.path})')
            pass

        if mask & IN_DELETE or mask & IN_MOVED_FROM:
            self.info(f'{path.path} was deleted, '
                      f'parent {parameter!r} ({path.parent.path})')
            id = self.get_id_by_name(parameter, path.path)
            if id is not None:
                self.remove(id)
        if mask & IN_CREATE or mask & IN_MOVED_TO:
            if mask & IN_ISDIR:
                self.info(f'directory {path.path} was created, '
                          f'parent {parameter!r} ({path.parent.path})')
            else:
                self.info(f'file {path.path} was created, '
                          f'parent {parameter!r} ({path.parent.path})')
            if self.get_id_by_name(parameter, path.path) is None:
                if path.isdir():
                    self.walk(
                        path.path,
                        self.get_by_id(parameter),
                        self.ignore_file_pattern,
                    )
                else:
                    if self.ignore_file_pattern.match(parameter) is None:
                        self.append(str(path.path),
                                    str(self.get_by_id(parameter)))

    def getnextID(self):
        ret = self.next_id
        self.next_id += 1
        return ret

    def backend_import(self, item, data):
        try:
            f = open(item.get_path(), 'w+b')
            if hasattr(data, 'read'):
                data = data.read()
            f.write(data)
            f.close()
            item.rebuild(self.urlbase)
            return 200
        except IOError:
            self.warning(f'import of file {item.get_path()} failed')
        except Exception as msg:
            import traceback

            self.warning(traceback.format_exc())
        return 500

    def upnp_init(self):
        self.current_connection_id = None
        if self.server:
            self.server.connection_manager_server.set_variable(
                0,
                'SourceProtocolInfo',
                [
                    f'internal:{self.server.coherence.hostname}:audio/mpeg:*',
                    'http-get:*:audio/mpeg:*',
                    f'internal:{self.server.coherence.hostname}:video/mp4:*',
                    'http-get:*:video/mp4:*',
                    f'internal:{self.server.coherence.hostname}:application/ogg:*',  # noqa: E501
                    'http-get:*:application/ogg:*',
                    f'internal:{self.server.coherence.hostname}:video/x-msvideo:*',  # noqa: E501
                    'http-get:*:video/x-msvideo:*',
                    f'internal:{self.server.coherence.hostname}:video/mpeg:*',
                    'http-get:*:video/mpeg:*',
                    f'internal:{self.server.coherence.hostname}:video/avi:*',
                    'http-get:*:video/avi:*',
                    f'internal:{self.server.coherence.hostname}:video/divx:*',
                    'http-get:*:video/divx:*',
                    f'internal:{self.server.coherence.hostname}:video/quicktime:*',  # noqa: E501
                    'http-get:*:video/quicktime:*',
                    f'internal:{self.server.coherence.hostname}:image/gif:*',
                    'http-get:*:image/gif:*',
                    f'internal:{self.server.coherence.hostname}:image/jpeg:*',
                    'http-get:*:image/jpeg:*'
                    # 'http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=11;'
                    # 'DLNA.ORG_FLAGS=01700000000000000000000000000000',
                    # 'http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;'
                    # 'DLNA.ORG_OP=11;DLNA.ORG_FLAGS'
                    # '=01700000000000000000000000000000',
                    # 'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;'
                    # 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS'
                    # '=00f00000000000000000000000000000',
                    # 'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;'
                    # 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS'
                    # '=00f00000000000000000000000000000',
                    # 'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;'
                    # 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS'
                    # '=00f00000000000000000000000000000',
                    # 'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;'
                    # 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS'
                    # '=00f00000000000000000000000000000',
                    # 'http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;'
                    # 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS'
                    # '=01700000000000000000000000000000',
                    # 'http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;'
                    # 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS'
                    # '=01700000000000000000000000000000',
                ],
                default=True,
            )
            self.server.content_directory_server.set_variable(
                0, 'SystemUpdateID', self.update_id)
            # self.server.content_directory_server.set_variable(
            #     0, 'SortCapabilities', '*')

    def upnp_ImportResource(self, *args, **kwargs):
        SourceURI = kwargs['SourceURI']
        DestinationURI = kwargs['DestinationURI']

        if DestinationURI.endswith('?import'):
            id = DestinationURI.split('/')[-1]
            id = id[:-7]  # remove the ?import
        else:
            return failure.Failure(errorCode(718))

        item = self.get_by_id(id)
        if item is None:
            return failure.Failure(errorCode(718))

        def gotPage(headers):
            # print('gotPage', headers)
            content_type = headers.get('content-type', [])
            if not isinstance(content_type, list):
                content_type = list(content_type)
            if len(content_type) > 0:
                extension = mimetypes.guess_extension(content_type[0],
                                                      strict=False)
                item.set_path(None, extension)
            shutil.move(tmp_path, item.get_path())
            item.rebuild(self.urlbase)
            if hasattr(self, 'update_id'):
                self.update_id += 1
                if self.server:
                    if hasattr(self.server, 'content_directory_server'):
                        self.server.content_directory_server.set_variable(
                            0, 'SystemUpdateID', self.update_id)
                if item.parent is not None:
                    value = (item.parent.get_id(), item.parent.get_update_id())
                    if self.server:
                        if hasattr(self.server, 'content_directory_server'):
                            self.server.content_directory_server.set_variable(
                                0, 'ContainerUpdateIDs', value)

        def gotError(error, url):
            self.warning(f'error requesting {url}')
            self.info(error)
            os.unlink(tmp_path)
            return failure.Failure(errorCode(718))

        tmp_fp, tmp_path = tempfile.mkstemp()
        os.close(tmp_fp)

        utils.downloadPage(SourceURI,
                           tmp_path).addCallbacks(gotPage, gotError, None,
                                                  None, [SourceURI], None)

        transfer_id = 0  # FIXME

        return {'TransferID': transfer_id}

    def upnp_CreateObject(self, *args, **kwargs):
        # print(f'CreateObject {kwargs}')
        if kwargs['ContainerID'] == 'DLNA.ORG_AnyContainer':
            if self.import_folder is not None:
                ContainerID = self.import_folder_id
            else:
                return failure.Failure(errorCode(712))
        else:
            ContainerID = kwargs['ContainerID']
        Elements = kwargs['Elements']

        parent_item = self.get_by_id(ContainerID)
        if parent_item is None:
            return failure.Failure(errorCode(710))
        if parent_item.item.restricted:
            return failure.Failure(errorCode(713))

        if len(Elements) == 0:
            return failure.Failure(errorCode(712))

        elt = DIDLElement.fromString(Elements)
        if elt.numItems() != 1:
            return failure.Failure(errorCode(712))

        item = elt.getItems()[0]
        if item.parentID == 'DLNA.ORG_AnyContainer':
            item.parentID = ContainerID
        if (item.id != '' or item.parentID != ContainerID
                or item.restricted is True or item.title == ''):
            return failure.Failure(errorCode(712))

        if '..' in item.title or '~' in item.title or os.sep in item.title:
            return failure.Failure(errorCode(712))

        if item.upnp_class == 'object.container.storageFolder':
            if len(item.res) != 0:
                return failure.Failure(errorCode(712))
            path = os.path.join(parent_item.get_path(), item.title)
            id = self.create('directory', path, parent_item)
            try:
                os.mkdir(path)
            except Exception:
                self.remove(id)
                return failure.Failure(errorCode(712))

            if self.inotify is not None:
                mask = (IN_CREATE
                        | IN_DELETE
                        | IN_MOVED_FROM
                        | IN_MOVED_TO
                        | IN_CHANGED)
                self.inotify.watch(
                    path,
                    mask=mask,
                    autoAdd=False,
                    callbacks=[partial(self.notify, parameter=id)],
                )

            new_item = self.get_by_id(id)
            didl = DIDLElement()
            didl.addItem(new_item.item)
            return {'ObjectID': id, 'Result': didl.toString()}

        if item.upnp_class.startswith('object.item'):
            _, _, content_format, _ = item.res[0].protocolInfo.split(':')
            extension = mimetypes.guess_extension(content_format, strict=False)
            path = os.path.join(parent_item.get_realpath(),
                                item.title + extension)
            id = self.create('item', path, parent_item)

            new_item = self.get_by_id(id)
            for res in new_item.item.res:
                res.importUri = new_item.url + '?import'
                res.data = None
            didl = DIDLElement()
            didl.addItem(new_item.item)
            return {'ObjectID': id, 'Result': didl.toString()}

        return failure.Failure(errorCode(712))

    def hidden_upnp_DestroyObject(self, *args, **kwargs):
        ObjectID = kwargs['ObjectID']

        item = self.get_by_id(ObjectID)
        if item is None:
            return failure.Failure(errorCode(701))

        self.info(f'upnp_DestroyObject: {item.location}')
        try:
            item.location.remove()
        except Exception as msg:
            self.error(f'upnp_DestroyObject [{Exception}]: {msg}')
            return failure.Failure(errorCode(715))

        return {}
Exemplo n.º 4
0
class INotifyObserver(object):
    def __init__(self, fp):
        self.fp = fp
        self.setup_done = Deferred()
        if settings["activate_inotify"]:
            self.setup_inotify()
        self.inotify_callbacks = []

    def setup_inotify(self):
        """
            Setup the INotifier watcher.
        """
        self.notifier = INotify()
        self.notifier.startReading()
        filepath = FilePath(self.fp.name)
        self.notifier.watch(filepath, callbacks=[self.inotify])

        # The setup_done is used mainly in testing
        self.setup_done.callback(True)
        self.setup_done = Deferred()


    def register_inotify_callback(self, callback):
        self.inotify_callbacks.append(callback)

    def inotify_callback(self, jdata):
        """
            Must be overwritten by a child.
        """
        raise NotImplementedError

    def inotify(self, ignored, filepath, mask):
        """
            Callback for the INotify. It should call the sse resource with the
            changed layouts in the layout file if there are changes in the
            layout file.
            It calls the inotify_callback method first which must be overwritten
            by its children.
        """
        hmask = humanReadableMask(mask)

        # Some editors move the file triggering several events in inotify. All
        # of them change some attribute of the file, so if that event happens,
        # see if there are changes and alert the sse resource in that case.
        if 'attrib' in hmask or 'modify' in hmask:
            self.fp.close()
            self.fp = open(self.fp.name, 'r')
            self.fp.seek(0)

            jdata = {}
            self.inotify_callback(jdata)

            changes = 0
            for _, l in jdata.items():
                changes += len(l)

            if changes > 0:
                for callback in self.inotify_callbacks:
                    callback(jdata)
                log.msg("Change in %s" % self.fp.name)

        # Some editors move the file and inotify lose track of the file, so the
        # notifier must be restarted when some attribute changed is received.
        if 'attrib' in hmask:
            self.notifier.stopReading()
            self.setup_inotify()