Example #1
0
 def __init__(self, address, data_dir, capacity, pyro_uri, ntp_server=None):
     """
     Inicializa una instancia de un servidor TagFS compartido en la red.
     
     @type address: C{str}
     @param address: Dirección IP de la interfaz donde debe escuchar esta
        instancia del servidor de TagFS.        
     
     @type data_dir: C{str}
     @param data_dir: Ruta absoluta al directorio utilizado para almacenar
         los archivos y otros datos relacionados con el funcionamiento
         del servidor.
         
     @type capacity: C{int}
     @param capacity: Capacidad de almacenamiento en bytes de este servidor.
         TagFS garantizará que la capacidad utilizada por todos los
         archivos almacenados en este servidor no sobrepasará esta
         capacidad.
         
     @type pyro_uri: C{str}
     @param pyro_uri: URI de PyRO correspondiente a este servidor.
     
     @type ntp_server: C{str}
     @para ntp_server: Host del servidor NTP que se utilizará para obtener
         el tiempo durante el proceso de sincronización de los servidores.
     """
     self._address = address
     self._data_dir = data_dir
     if not os.path.isdir(self._data_dir):
         os.mkdir(self._data_dir)
     self._pyro_uri = pyro_uri
     self._init_index()
     self._init_files()
     self._init_status(capacity)
     self._init_autodiscovery()
     self._mrsw_lock = sync.mrsw()
     # Keep this last! This requires the index, locks, etc.
     self._sync_thread = threading.Thread(target=self._sync_servers)
     self._sync_thread.start()
     # TODO Change to use an NTP or local.
     if ntp_server:
         self._time_provider = NTPTimeProvider(ntp_server)
     else:
         self._time_provider = LocalTimeProvider()
Example #2
0
 def __init__(self,  address, data_dir, capacity, pyro_uri, ntp_server=None):
     """
     Inicializa una instancia de un servidor TagFS compartido en la red.
     
     @type address: C{str}
     @param address: Dirección IP de la interfaz donde debe escuchar esta
        instancia del servidor de TagFS.        
     
     @type data_dir: C{str}
     @param data_dir: Ruta absoluta al directorio utilizado para almacenar
         los archivos y otros datos relacionados con el funcionamiento
         del servidor.
         
     @type capacity: C{int}
     @param capacity: Capacidad de almacenamiento en bytes de este servidor.
         TagFS garantizará que la capacidad utilizada por todos los
         archivos almacenados en este servidor no sobrepasará esta
         capacidad.
         
     @type pyro_uri: C{str}
     @param pyro_uri: URI de PyRO correspondiente a este servidor.
     
     @type ntp_server: C{str}
     @para ntp_server: Host del servidor NTP que se utilizará para obtener
         el tiempo durante el proceso de sincronización de los servidores.
     """
     self._address = address
     self._data_dir = data_dir
     if not os.path.isdir(self._data_dir):
         os.mkdir(self._data_dir)
     self._pyro_uri = pyro_uri
     self._init_index()
     self._init_files()
     self._init_status(capacity)
     self._init_autodiscovery()
     self._mrsw_lock = sync.mrsw()
     # Keep this last! This requires the index, locks, etc.
     self._sync_thread = threading.Thread(target=self._sync_servers)
     self._sync_thread.start()
     # TODO Change to use an NTP or local.
     if ntp_server:
         self._time_provider = NTPTimeProvider(ntp_server)
     else:
         self._time_provider = LocalTimeProvider()
Example #3
0
class RemoteTagFSServer(object):
    """
    Servidor TagFS compartido en la red utilizando Pyro. 
    """
    def __init__(self, address, data_dir, capacity, pyro_uri, ntp_server=None):
        """
        Inicializa una instancia de un servidor TagFS compartido en la red.
        
        @type address: C{str}
        @param address: Dirección IP de la interfaz donde debe escuchar esta
           instancia del servidor de TagFS.        
        
        @type data_dir: C{str}
        @param data_dir: Ruta absoluta al directorio utilizado para almacenar
            los archivos y otros datos relacionados con el funcionamiento
            del servidor.
            
        @type capacity: C{int}
        @param capacity: Capacidad de almacenamiento en bytes de este servidor.
            TagFS garantizará que la capacidad utilizada por todos los
            archivos almacenados en este servidor no sobrepasará esta
            capacidad.
            
        @type pyro_uri: C{str}
        @param pyro_uri: URI de PyRO correspondiente a este servidor.
        
        @type ntp_server: C{str}
        @para ntp_server: Host del servidor NTP que se utilizará para obtener
            el tiempo durante el proceso de sincronización de los servidores.
        """
        self._address = address
        self._data_dir = data_dir
        if not os.path.isdir(self._data_dir):
            os.mkdir(self._data_dir)
        self._pyro_uri = pyro_uri
        self._init_index()
        self._init_files()
        self._init_status(capacity)
        self._init_autodiscovery()
        self._mrsw_lock = sync.mrsw()
        # Keep this last! This requires the index, locks, etc.
        self._sync_thread = threading.Thread(target=self._sync_servers)
        self._sync_thread.start()
        # TODO Change to use an NTP or local.
        if ntp_server:
            self._time_provider = NTPTimeProvider(ntp_server)
        else:
            self._time_provider = LocalTimeProvider()

    def _init_index(self):
        """
        Inicializa el índice, implementado utilizando Whoosh, que contiene
        la información acerca de los archivos almacenados en este servidor
        de TagFS.
        """
        self._encoding = 'utf-8'
        self._index_schema = whoosh.fields.Schema(
            hash=whoosh.fields.ID(stored=True, unique=True),
            tags=whoosh.fields.KEYWORD(stored=True,
                                       lowercase=True,
                                       scorable=True,
                                       field_boost=2.0),
            description=whoosh.fields.TEXT(stored=True),
            name=whoosh.fields.TEXT(stored=True),
            owner=whoosh.fields.STORED(),
            group=whoosh.fields.STORED(),
            perms=whoosh.fields.STORED(),
            size=whoosh.fields.STORED(),
            path=whoosh.fields.STORED(),
            type=whoosh.fields.STORED(),
            time=whoosh.fields.STORED(),
            action=whoosh.fields.STORED(),
        )
        index_dir = os.path.join(self._data_dir, 'index')
        if not os.path.isdir(index_dir):
            os.mkdir(index_dir)
            self._index = whoosh.index.create_in(index_dir, self._index_schema)
        else:
            self._index = whoosh.index.open_dir(index_dir)

    def _init_files(self):
        """
        Inicializa el directorio que contiene los archivos almacenados 
        en este servidor de TagFS. time.mktime(time.gmtime())Los archivos no se almacenarán directamente
        en la raíz de este directorio sino en un serie de directorios anidados
        para evitar que este directorio tenga muchas entradas y se haga
        muy lento el acceso a un archivo.
        """
        self._files_dir = os.path.join(self._data_dir, 'files')
        if not os.path.isdir(self._files_dir):
            os.mkdir(self._files_dir)

    def _init_status(self, capacity):
        """
        Inicializa el diccionario de estado de este servidor de TagFS. Este 
        diccionario es el que se les envía a los clientes como respuesta
        del método C{status()}.
        
        Actualmente este diccionario contiene información acerca de la 
        capacidad de almacenamiento de este servidor TagFS y la cantidad
        de espacio libre.
        
        @type capacity: C{int}
        @param capacity: Capacidad de almacenamiento en bytes de este servidor.
            TagFS garantizará que la capacidad utilizada por todos los
            archivos almacenados en este servidor no sobrepasará esta
            capacidad.        
        """
        self._status = {}
        self._status['capacity'] = long(capacity)
        # Calculate the space used in the data directory.
        data_dir_size = 0L
        get_file_size_root = lambda root, file: os.path.getsize(
            os.path.join(root, file))
        for root, _, files in os.walk(self._data_dir):
            get_file_size = lambda file: get_file_size_root(root, file)
            data_dir_size += sum(map(get_file_size, files))
        self._status['empty_space'] = capacity - data_dir_size

    def _init_autodiscovery(self):
        """
        Inicializa el descubrimiento automático de los servidores.
        """
        self._servers = {}
        self._servers_mutex = threading.Lock()
        self.addService = self._server_added
        self.removeService = self._server_removed
        self._zeroconf = Zeroconf.Zeroconf(self._address)
        self._zeroconf_browser = Zeroconf.ServiceBrowser(
            self._zeroconf, ZEROCONF_SERVICE_TYPE, self)

    def _server_added(self, zeroconf, service_type, service_name):
        """
        Método ejecutado cuando se descubre un nuevo servidor TagFS.
        
        @type zeroconf: C{Zeroconf}
        @param zeroconf: Instancia del servidor implementando la comunicación
            mediante Zeroconf Multicast DNS Service Discovery.
        
        @type service_type: C{str}
        @param service_type: Nombre completamente calificado del tipo
            de servicio que fue descubierto.
        
        @type service_name: C{str}
        @param service_name: Nombre completamente calificado del nombre 
            del servicio que fue descubierto.
        """
        pyro_uri = service_name[:-(len(service_type) + 1)]
        if pyro_uri != self._pyro_uri:
            with self._servers_mutex:
                pyro_proxy = Pyro.core.getProxyForURI(pyro_uri)
                self._servers[pyro_uri] = pyro_proxy

    def _server_removed(self, zeroconf, service_type, service_name):
        """
        Método ejecutado cuando un servidor TagFS deja de estar disponible.
        
        @type zeroconf: C{Zeroconf}
        @param zeroconf: Instancia del servidor implementando la comunicación
            mediante Zeroconf Multicast DNS Service Discovery.
            
        @type service_type: C{str}
        @param service_type: Nombre completamente calificado del tipo
            de servicio que fue descubierto.
        
        @type service_name: C{str}
        @param service_name: Nombre completamente calificado del nombre 
            del servicio que fue descubierto.                    
        """
        pyro_uri = service_name[:-(len(service_type) + 1)]
        if pyro_uri != self._pyro_uri:
            with self._servers_mutex:
                del self._servers[pyro_uri]

    def _sync_servers(self):
        """
        Método que garantiza la sincronización de los servidores.
        """
        sync_sleep = 30  # seconds
        self._sync_cond = threading.Condition()
        self._sync_continue = True
        while self._sync_continue:
            with self._servers_mutex:
                self._mrsw_lock.write_in()
                try:
                    searcher = self._index.searcher()
                    reader = self._index.reader()
                    for hash in reader.lexicon('hash'):
                        action = self.action(hash, True)
                        best_server = None
                        best_action = None
                        for server in self._servers.itervalues():
                            server_action = server.action(hash)
                            if (server_action
                                    and server_action[1] > action[1]):
                                best_action = server_action
                                best_server = server
                        if best_action:
                            if best_action[0] == 'delete':
                                self.remove(hash, True)
                            else:
                                self._update_file(hash, best_server, True)
                finally:
                    self._mrsw_lock.write_out()
            self._sync_cond.acquire()
            self._sync_cond.wait(sync_sleep)
            self._sync_cond.release()

    def _update_file(self, file_hash, server, safe=False):
        """
        Actualiza un fichero existente en este servidor.
        
        Actualiza un fichero almacenado en este servidor por el fichero 
        identificado por el mismo hash en el servidor dado.
        
        @type file_hash: C{str}
        @param file_hash: Identificador del archivo cuyos datos se quiere 
            obtener. Este hash identifica al archivo únicamente dentro del 
            sistema de ficheros distribuidos, a partir de sus etiquetas y 
            su nombre.
            
        @type server: TODO
        @param server: Servidor que contiene el fichero del que se va a 
        actualizar el local.
        
        @type safe: C{bool}
        @param safe: Expresa si la ejecución es "thread safe" o si el 
            método se tiene que encargar de la sincronización. Es falso por
            defecto
        """
        data = server.get(file_hash)
        info = server.info(file_hash)
        self.put(data, info, safe)

    def action(self, file_hash, safe=False):
        """
        Obtiene la última acción que se realizó sobre ese equipo.
        
        @type file_hash: C{str}
        @param file_hash: Identificador del archivo cuyos datos se quiere 
            obtener. Este hash identifica al archivo únicamente dentro del 
            sistema de ficheros distribuidos, a partir de sus etiquetas y 
            su nombre.
            
        @rtype: C{tuple}
        @return: Tupla que contiene como primer valor la acción que se realizó 
            sobre este y el identificador de tiempo en el que se realizó.
            
        @type safe: C{bool}
        @param safe: Expresa si la ejecución es "thread safe" o si el 
            método se tiene que encargar de la sincronización. Es falso por
            defecto
        """
        if not safe:
            self._mrsw_lock.read_in()
        try:
            searcher = self._index.searcher()
            doc = searcher.document(hash=file_hash.decode(self._encoding))
            if doc is not None:
                return (doc['action'], doc['time'])
            else:
                return None
        finally:
            if not safe:
                self._mrsw_lock.read_out()

    def status(self):
        """
        Brinda información a los clientes TagFS acerca del estado de este
        servidor. Por ejemplo: cantidad de espacio disponible para almacenar
        nuevos archivos.
        
        @rtype: C{dict}
        @return: Diccionario que contiene información acerca del estado 
            del servidor.
        """
        self._mrsw_lock.read_in()
        try:
            return self._status
        finally:
            self._mrsw_lock.read_out()

    def get(self, file_hash, safe=False):
        """
        Obtiene el contenido del archivo identificado por C{file_hash}
        
        @type file_hash: C{str}
        @param file_hash: Identificador del archivo cuyos datos se quiere 
            obtener. Este hash identifica al archivo únicamente dentro del 
            sistema de ficheros distribuidos, a partir de sus etiquetas y 
            su nombre.
            
        @rtype: C{str}
        @return: Contenido del archivo identificado por C{file_hash} si
            este archivo existe, C{None} si no hay almacenado en este
            servidor un archivo identificado por el hash dado.
            
        @type safe: C{bool}
        @param safe: Expresa si la ejecución es "thread safe" o si el 
            método se tiene que encargar de la sincronización. Es falso por
            defecto
        """
        if not safe:
            self._mrsw_lock.read_in()
        try:
            searcher = self._index.searcher()
            doc = searcher.document(hash=file_hash.decode(self._encoding))
            if doc is not None and doc['action'] != 'delete':
                file_path = os.path.join(self._files_dir, doc['path'])
                with open(file_path, 'rb') as file:
                    return file.read()
            else:
                return None
        finally:
            if not safe:
                self._mrsw_lock.read_out()

    def put(self, file_data, file_info, safe=False):
        """
        Almacena un nuevo archivo en este servidor del sistema de 
        archivos distribuidos.
        
        @type file_data: C{str}
        @param file_data: Contenido del archivo que se quiere almacenar.
        
        @type file_info: C{dict}
        @param file_info: Diccionario con los metadatos del archivo.
        
        @type safe: C{bool}
        @param safe: Expresa si la ejecución es "thread safe" o si el 
            método se tiene que encargar de la sincronización. Es falso por
            defecto         
        """
        if not safe:
            self._mrsw_lock.write_in()
        try:
            # Save the file in the files directory.
            file_name = file_info['name']
            file_hash = hashlib.md5(
                u' '.join(
                    [tag.decode(self._encoding)
                     for tag in file_info['tags']]) +
                file_name.decode(self._encoding)).hexdigest()
            file_path = os.path.join(self._files_dir,
                                     os.path.sep.join(file_hash[0:5]),
                                     file_name)
            if not os.path.isdir(os.path.dirname(file_path)):
                os.makedirs(os.path.dirname(file_path))
            with open(file_path, 'w') as file:
                file.write(file_data)

            mod_time = ''
            if not time in file_info:
                mod_time = str(self._time_provider.get_time()).decode(
                    self._encoding)
            else:
                mod_time = file_info['time'].decode(self._encoding)

            # Add the metadata of the file to the index.
            file_path = file_path[len(self._files_dir) + 1:]
            writer = self._index.writer()
            writer.delete_by_term('hash', file_hash.decode(self._encoding))
            writer.add_document(
                hash=file_hash.decode(self._encoding),
                tags=u' '.join(
                    [tag.decode(self._encoding) for tag in file_info['tags']]),
                description=file_info['description'].decode(self._encoding),
                name=file_info['name'].decode(self._encoding),
                size=file_info['size'].decode(self._encoding),
                owner=file_info['owner'].decode(self._encoding),
                group=file_info['group'].decode(self._encoding),
                perms=file_info['perms'].decode(self._encoding),
                path=file_path.decode(self._encoding),
                type=magic.whatis(file_data),
                time=mod_time,
                action=u'add')
            writer.commit()

            # Update the empty space of this server.
            self._status['empty_space'] -= long(file_info['size'])
        finally:
            if not safe:
                self._mrsw_lock.write_out()

    def remove(self, file_hash, safe=False):
        """
        Elimina un archivo almacenado en este servidor. Si este servidor
        no tiene almacenado un archivo identificado con el hash dado
        no se realizará ninguna acción.
        
        @type file_hash: C{str}
        @param file_hash: Identificador del archivo que se quiere eliminar. 
            Este hash identifica al archivo únicamente dentro del sistema de 
            ficheros distribuido a partir de sus etiquetas y su nombre.
            
        @type safe: C{bool}
        @param safe: Expresa si la ejecución es "thread safe" o si el 
            método se tiene que encargar de la sincronización. Es falso por
            defecto
        """
        if not safe:
            self._mrsw_lock.write_in()
        try:
            searcher = self._index.searcher()
            doc = searcher.document(hash=file_hash.decode(self._encoding))
            if doc is not None and doc['action'] != 'delete':
                # Remove the file from the files directory.
                os.remove(os.path.join(self._files_dir, doc['path']))

                # Remove the metadata of the file from the index.
                writer = self._index.writer()
                writer.delete_by_term('hash', file_hash.decode(self._encoding))
                writer.add_document(hash=file_hash.decode(self._encoding),
                                    time=self._time_provider.get_time(),
                                    action=u'delete')
                writer.commit()

                # Update the empty space in this server.
                self._status['empty_space'] += long(doc['size'])
        finally:
            if not safe:
                self._mrsw_lock.write_out()

    def list(self, tags):
        """
        Lista los archivos almacenados en este servidor que tienen todos
        los tags especificados en el conjunto C{tags}.
        
        @type tags: C{set}
        @param tags: Conjunto de tags que deben tener los archivos.
        
        @rtype: C{set}
        @return: Conjunto con los hash de los archivos que tienen los tags 
            especificados mediante el conjunto C{tags}.
        """
        self._mrsw_lock.read_in()
        try:
            searcher = self._index.searcher()
            tags_terms = [tag.decode(self._encoding).lower() for tag in tags]
            query = whoosh.query.And(
                [whoosh.query.Term('tags', term) for term in tags_terms])
            return set([result['hash'] for result in searcher.search(query)])
        finally:
            self._mrsw_lock.read_out()

    def search(self, text):
        """
        Realiza una búsqueda de texto libre en los tags, la descripción y el 
        nombre de los archivos almacenados en este servidor.
        
        @type text: C{str}
        @param text: Texto de la búsqueda que se quiere realizar.
        
        @rtype: C{set}
        @return: Conjunto con los hash de los archivos que son relevantes 
            para la búsqueda de texto libre C{text}.
        """
        self._mrsw_lock.read_in()
        try:
            searcher = self._index.searcher()
            default_fields = ['tags', 'description', 'name']
            parser = whoosh.qparser.MultifieldParser(default_fields,
                                                     schema=self._index_schema)
            query = parser.parse(text.decode(self._encoding))
            return set([result['hash'] for result in searcher.search(query)])
        finally:
            self._mrsw_lock.read_out()

    def info(self, file_hash, safe=False):
        """
        Obtiene información a partir del hash de un archivo.
        
        @type file_hash: C{str}
        @param file_hash: Identificador del archivo cuya información se quiere 
            obtener. Este hash identifica al archivo únicamente dentro del 
            sistema de ficheros distribuidos a partir de sus etiquetas y su 
            nombre.
            
        @rtype: C{dict}
        @return: Diccionario con los metadatos del archivo si este servidor
            tiene almacenado un archivo identificado por el hash dado,
            C{None} en caso contrario.
        
        @type safe: C{bool}
        @param safe: Expresa si la ejecución es "thread safe" o si el 
            método se tiene que encargar de la sincronización. Es falso por
            defecto
        """
        if not safe:
            self._mrsw_lock.read_in()
        try:
            searcher = self._index.searcher()
            doc = searcher.document(hash=file_hash.decode(self._encoding))
            if doc is not None and doc['action'] != 'delete':
                info = {}
                info['hash'] = doc['hash']
                info['tags'] = set(doc['tags'].split())
                info['description'] = doc['description']
                info['name'] = doc['name']
                info['size'] = doc['size']
                info['type'] = doc['type']
                info['owner'] = doc['owner']
                info['group'] = doc['group']
                info['perms'] = doc['perms']
                info['time'] = doc['time']
                return info
            else:
                return None
        finally:
            if not safe:
                self._mrsw_lock.read_out()

    def get_all_tags(self):
        """
        Obtiene un conjunto con todas los tags que tiene algún archivo 
        almacenado en este servidor.
        
        @rtype: C{set}
        @return: Conjunto con las etiquetas en este servidor.
        """
        reader = self._index.reader()
        searcher = self._index.searcher()
        tags = set()
        for tag in reader.lexicon('tags'):
            # Ugly hack! Returns tags that were removed from the index?!
            query = whoosh.query.Term('tags', tag)
            results = searcher.search(query, limit=1)
            if len(results) > 0:
                tags.add(tag)
        return tags

    def get_popular_tags(self, number):
        """
        Obtiene un conjunto con los tags más populares de los archivos 
        almacenados en este servidor.
        
        @type number: C{int}
        @param number: Cantidad de tags populares deseadas.
        
        @rtype: C{set}
        @return: Conjunto de tuplas de la forma (frecuencia, tag) 
        """
        reader = self._index.reader()
        return set(reader.most_distinctive_terms('tags', number, u''))

    def terminate(self):
        """
        Este método es utilizado por la instancia de la clase C{TagFSServer} para
        indicar que va a dejar de estar disponible en la red este servidor y que
        se deben guardar todos los recursos abiertos.
        """
        self._sync_continue = False
        self._sync_cond.acquire()
        self._sync_cond.notify()
        self._sync_cond.release()
        self._sync_thread.join()
        self._index.close()
Example #4
0
class RemoteTagFSServer(object):
    """
    Servidor TagFS compartido en la red utilizando Pyro. 
    """

    def __init__(self,  address, data_dir, capacity, pyro_uri, ntp_server=None):
        """
        Inicializa una instancia de un servidor TagFS compartido en la red.
        
        @type address: C{str}
        @param address: Dirección IP de la interfaz donde debe escuchar esta
           instancia del servidor de TagFS.        
        
        @type data_dir: C{str}
        @param data_dir: Ruta absoluta al directorio utilizado para almacenar
            los archivos y otros datos relacionados con el funcionamiento
            del servidor.
            
        @type capacity: C{int}
        @param capacity: Capacidad de almacenamiento en bytes de este servidor.
            TagFS garantizará que la capacidad utilizada por todos los
            archivos almacenados en este servidor no sobrepasará esta
            capacidad.
            
        @type pyro_uri: C{str}
        @param pyro_uri: URI de PyRO correspondiente a este servidor.
        
        @type ntp_server: C{str}
        @para ntp_server: Host del servidor NTP que se utilizará para obtener
            el tiempo durante el proceso de sincronización de los servidores.
        """
        self._address = address
        self._data_dir = data_dir
        if not os.path.isdir(self._data_dir):
            os.mkdir(self._data_dir)
        self._pyro_uri = pyro_uri
        self._init_index()
        self._init_files()
        self._init_status(capacity)
        self._init_autodiscovery()
        self._mrsw_lock = sync.mrsw()
        # Keep this last! This requires the index, locks, etc.
        self._sync_thread = threading.Thread(target=self._sync_servers)
        self._sync_thread.start()
        # TODO Change to use an NTP or local.
        if ntp_server:
            self._time_provider = NTPTimeProvider(ntp_server)
        else:
            self._time_provider = LocalTimeProvider()
        
    def _init_index(self):
        """
        Inicializa el índice, implementado utilizando Whoosh, que contiene
        la información acerca de los archivos almacenados en este servidor
        de TagFS.
        """
        self._encoding = 'utf-8'
        self._index_schema = whoosh.fields.Schema(
            hash=whoosh.fields.ID(stored=True, unique=True),
            tags=whoosh.fields.KEYWORD(stored=True, lowercase=True, 
                                       scorable=True, field_boost=2.0),
            description=whoosh.fields.TEXT(stored=True),
            name=whoosh.fields.TEXT(stored=True),
            owner=whoosh.fields.STORED(),
            group=whoosh.fields.STORED(),
            perms=whoosh.fields.STORED(),
            size=whoosh.fields.STORED(),
            path=whoosh.fields.STORED(),
            type=whoosh.fields.STORED(),
            time=whoosh.fields.STORED(),
            action=whoosh.fields.STORED(),
        )
        index_dir = os.path.join(self._data_dir, 'index')
        if not os.path.isdir(index_dir):
            os.mkdir(index_dir)
            self._index = whoosh.index.create_in(index_dir, self._index_schema)
        else:
            self._index = whoosh.index.open_dir(index_dir)
        
    def _init_files(self):
        """
        Inicializa el directorio que contiene los archivos almacenados 
        en este servidor de TagFS. time.mktime(time.gmtime())Los archivos no se almacenarán directamente
        en la raíz de este directorio sino en un serie de directorios anidados
        para evitar que este directorio tenga muchas entradas y se haga
        muy lento el acceso a un archivo.
        """
        self._files_dir = os.path.join(self._data_dir, 'files')
        if not os.path.isdir(self._files_dir):
            os.mkdir(self._files_dir)
            
    def _init_status(self, capacity):
        """
        Inicializa el diccionario de estado de este servidor de TagFS. Este 
        diccionario es el que se les envía a los clientes como respuesta
        del método C{status()}.
        
        Actualmente este diccionario contiene información acerca de la 
        capacidad de almacenamiento de este servidor TagFS y la cantidad
        de espacio libre.
        
        @type capacity: C{int}
        @param capacity: Capacidad de almacenamiento en bytes de este servidor.
            TagFS garantizará que la capacidad utilizada por todos los
            archivos almacenados en este servidor no sobrepasará esta
            capacidad.        
        """
        self._status = {}
        self._status['capacity'] = long(capacity)
        # Calculate the space used in the data directory.
        data_dir_size = 0L
        get_file_size_root = lambda root, file: os.path.getsize(os.path.join(root, file))
        for root, _, files in os.walk(self._data_dir):
            get_file_size = lambda file: get_file_size_root(root, file)
            data_dir_size += sum(map(get_file_size, files))
        self._status['empty_space'] = capacity - data_dir_size
        
    def _init_autodiscovery(self):
        """
        Inicializa el descubrimiento automático de los servidores.
        """
        self._servers = {}
        self._servers_mutex = threading.Lock()                
        self.addService = self._server_added
        self.removeService = self._server_removed
        self._zeroconf = Zeroconf.Zeroconf(self._address)
        self._zeroconf_browser = Zeroconf.ServiceBrowser(self._zeroconf, ZEROCONF_SERVICE_TYPE, self)
        
    def _server_added(self, zeroconf, service_type, service_name):
        """
        Método ejecutado cuando se descubre un nuevo servidor TagFS.
        
        @type zeroconf: C{Zeroconf}
        @param zeroconf: Instancia del servidor implementando la comunicación
            mediante Zeroconf Multicast DNS Service Discovery.
        
        @type service_type: C{str}
        @param service_type: Nombre completamente calificado del tipo
            de servicio que fue descubierto.
        
        @type service_name: C{str}
        @param service_name: Nombre completamente calificado del nombre 
            del servicio que fue descubierto.
        """
        pyro_uri = service_name[:-(len(service_type) + 1)]
        if pyro_uri != self._pyro_uri:
            with self._servers_mutex:
                pyro_proxy = Pyro.core.getProxyForURI(pyro_uri)            
                self._servers[pyro_uri] = pyro_proxy
        
    def _server_removed(self, zeroconf, service_type, service_name):
        """
        Método ejecutado cuando un servidor TagFS deja de estar disponible.
        
        @type zeroconf: C{Zeroconf}
        @param zeroconf: Instancia del servidor implementando la comunicación
            mediante Zeroconf Multicast DNS Service Discovery.
            
        @type service_type: C{str}
        @param service_type: Nombre completamente calificado del tipo
            de servicio que fue descubierto.
        
        @type service_name: C{str}
        @param service_name: Nombre completamente calificado del nombre 
            del servicio que fue descubierto.                    
        """
        pyro_uri = service_name[:-(len(service_type) + 1)]
        if pyro_uri != self._pyro_uri:
            with self._servers_mutex:
                del self._servers[pyro_uri]
            
    def _sync_servers(self):
        """
        Método que garantiza la sincronización de los servidores.
        """
        sync_sleep = 30 # seconds
        self._sync_cond = threading.Condition()
        self._sync_continue = True
        while self._sync_continue:
            with self._servers_mutex:
                self._mrsw_lock.write_in()
                try:
                    searcher = self._index.searcher()
                    reader = self._index.reader()
                    for hash in reader.lexicon('hash'):
                        action = self.action(hash, True)
                        best_server = None
                        best_action = None
                        for server in self._servers.itervalues():
                            server_action = server.action(hash)
                            if (server_action and 
                                server_action[1] > action[1]):
                                best_action = server_action
                                best_server = server
                        if best_action:
                            if best_action[0] == 'delete':
                                self.remove(hash, True)
                            else:
                                self._update_file(hash, best_server, True)                                                    
                finally:
                    self._mrsw_lock.write_out()
            self._sync_cond.acquire()
            self._sync_cond.wait(sync_sleep)
            self._sync_cond.release()
            
    def _update_file(self, file_hash, server, safe=False):
        """
        Actualiza un fichero existente en este servidor.
        
        Actualiza un fichero almacenado en este servidor por el fichero 
        identificado por el mismo hash en el servidor dado.
        
        @type file_hash: C{str}
        @param file_hash: Identificador del archivo cuyos datos se quiere 
            obtener. Este hash identifica al archivo únicamente dentro del 
            sistema de ficheros distribuidos, a partir de sus etiquetas y 
            su nombre.
            
        @type server: TODO
        @param server: Servidor que contiene el fichero del que se va a 
        actualizar el local.
        
        @type safe: C{bool}
        @param safe: Expresa si la ejecución es "thread safe" o si el 
            método se tiene que encargar de la sincronización. Es falso por
            defecto
        """
        data = server.get(file_hash)
        info = server.info(file_hash)
        self.put(data, info, safe)
            
    def action(self, file_hash, safe=False):
        """
        Obtiene la última acción que se realizó sobre ese equipo.
        
        @type file_hash: C{str}
        @param file_hash: Identificador del archivo cuyos datos se quiere 
            obtener. Este hash identifica al archivo únicamente dentro del 
            sistema de ficheros distribuidos, a partir de sus etiquetas y 
            su nombre.
            
        @rtype: C{tuple}
        @return: Tupla que contiene como primer valor la acción que se realizó 
            sobre este y el identificador de tiempo en el que se realizó.
            
        @type safe: C{bool}
        @param safe: Expresa si la ejecución es "thread safe" o si el 
            método se tiene que encargar de la sincronización. Es falso por
            defecto
        """
        if not safe:
            self._mrsw_lock.read_in()
        try:
            searcher = self._index.searcher()
            doc = searcher.document(hash=file_hash.decode(self._encoding))
            if doc is not None:
                return (doc['action'], doc['time'])
            else:
                return None
        finally:
            if not safe:
                self._mrsw_lock.read_out()
        
    def status(self):
        """
        Brinda información a los clientes TagFS acerca del estado de este
        servidor. Por ejemplo: cantidad de espacio disponible para almacenar
        nuevos archivos.
        
        @rtype: C{dict}
        @return: Diccionario que contiene información acerca del estado 
            del servidor.
        """
        self._mrsw_lock.read_in()
        try:
            return self._status
        finally:
            self._mrsw_lock.read_out()
        
    def get(self, file_hash, safe=False):
        """
        Obtiene el contenido del archivo identificado por C{file_hash}
        
        @type file_hash: C{str}
        @param file_hash: Identificador del archivo cuyos datos se quiere 
            obtener. Este hash identifica al archivo únicamente dentro del 
            sistema de ficheros distribuidos, a partir de sus etiquetas y 
            su nombre.
            
        @rtype: C{str}
        @return: Contenido del archivo identificado por C{file_hash} si
            este archivo existe, C{None} si no hay almacenado en este
            servidor un archivo identificado por el hash dado.
            
        @type safe: C{bool}
        @param safe: Expresa si la ejecución es "thread safe" o si el 
            método se tiene que encargar de la sincronización. Es falso por
            defecto
        """
        if not safe:
            self._mrsw_lock.read_in()
        try:
            searcher = self._index.searcher()
            doc = searcher.document(hash=file_hash.decode(self._encoding))
            if doc is not None and doc['action'] != 'delete':
                file_path = os.path.join(self._files_dir, doc['path'])
                with open(file_path, 'rb') as file:
                    return file.read()
            else:
                return None
        finally:
            if not safe:
                self._mrsw_lock.read_out()
        
    def put(self, file_data, file_info, safe=False):
        """
        Almacena un nuevo archivo en este servidor del sistema de 
        archivos distribuidos.
        
        @type file_data: C{str}
        @param file_data: Contenido del archivo que se quiere almacenar.
        
        @type file_info: C{dict}
        @param file_info: Diccionario con los metadatos del archivo.
        
        @type safe: C{bool}
        @param safe: Expresa si la ejecución es "thread safe" o si el 
            método se tiene que encargar de la sincronización. Es falso por
            defecto         
        """
        if not safe:
            self._mrsw_lock.write_in()
        try:
            # Save the file in the files directory.
            file_name = file_info['name']
            file_hash = hashlib.md5(u' ' .join([tag.decode(self._encoding) 
                                              for tag in file_info['tags']]) 
                                    + file_name.decode(self._encoding)).hexdigest()
            file_path = os.path.join(self._files_dir, os.path.sep.join(file_hash[0:5]), file_name)
            if not os.path.isdir(os.path.dirname(file_path)):
                os.makedirs(os.path.dirname(file_path))
            with open(file_path, 'w') as file:
                file.write(file_data)
                
            mod_time =''
            if not time in file_info:
                mod_time = str(self._time_provider.get_time()).decode(self._encoding)
            else:
                mod_time = file_info['time'].decode(self._encoding)
    
            # Add the metadata of the file to the index.
            file_path = file_path[len(self._files_dir) + 1:]
            writer = self._index.writer()
            writer.delete_by_term('hash', file_hash.decode(self._encoding))
            writer.add_document(
                hash=file_hash.decode(self._encoding),
                tags=u' '.join([tag.decode(self._encoding) for tag in file_info['tags']]),
                description=file_info['description'].decode(self._encoding),
                name=file_info['name'].decode(self._encoding),
                size=file_info['size'].decode(self._encoding),
                owner=file_info['owner'].decode(self._encoding),
                group=file_info['group'].decode(self._encoding),
                perms=file_info['perms'].decode(self._encoding),
                path=file_path.decode(self._encoding),
                type=magic.whatis(file_data),
                time=mod_time,
                action=u'add'                
            )               
            writer.commit()
    
            # Update the empty space of this server.
            self._status['empty_space'] -= long(file_info['size'])
        finally:
            if not safe:
                self._mrsw_lock.write_out()
        
    def remove(self, file_hash, safe=False):
        """
        Elimina un archivo almacenado en este servidor. Si este servidor
        no tiene almacenado un archivo identificado con el hash dado
        no se realizará ninguna acción.
        
        @type file_hash: C{str}
        @param file_hash: Identificador del archivo que se quiere eliminar. 
            Este hash identifica al archivo únicamente dentro del sistema de 
            ficheros distribuido a partir de sus etiquetas y su nombre.
            
        @type safe: C{bool}
        @param safe: Expresa si la ejecución es "thread safe" o si el 
            método se tiene que encargar de la sincronización. Es falso por
            defecto
        """
        if not safe:
            self._mrsw_lock.write_in()
        try:
            searcher = self._index.searcher()
            doc = searcher.document(hash=file_hash.decode(self._encoding))
            if doc is not None and doc['action'] != 'delete':                
                # Remove the file from the files directory.
                os.remove(os.path.join(self._files_dir, doc['path']))
                
                # Remove the metadata of the file from the index.
                writer = self._index.writer()
                writer.delete_by_term('hash', file_hash.decode(self._encoding))
                writer.add_document(
                    hash=file_hash.decode(self._encoding),
                    time=self._time_provider.get_time(),
                    action=u'delete'                
                )
                writer.commit()

                # Update the empty space in this server.
                self._status['empty_space'] += long(doc['size'])
        finally:
            if not safe:
                self._mrsw_lock.write_out()
        
    def list(self, tags):
        """
        Lista los archivos almacenados en este servidor que tienen todos
        los tags especificados en el conjunto C{tags}.
        
        @type tags: C{set}
        @param tags: Conjunto de tags que deben tener los archivos.
        
        @rtype: C{set}
        @return: Conjunto con los hash de los archivos que tienen los tags 
            especificados mediante el conjunto C{tags}.
        """
        self._mrsw_lock.read_in()
        try:
            searcher = self._index.searcher()
            tags_terms = [tag.decode(self._encoding).lower() for tag in tags]
            query = whoosh.query.And([whoosh.query.Term('tags', term) for term in tags_terms])
            return set([result['hash'] for result in searcher.search(query)])
        finally:
            self._mrsw_lock.read_out()

    def search(self, text):
        """
        Realiza una búsqueda de texto libre en los tags, la descripción y el 
        nombre de los archivos almacenados en este servidor.
        
        @type text: C{str}
        @param text: Texto de la búsqueda que se quiere realizar.
        
        @rtype: C{set}
        @return: Conjunto con los hash de los archivos que son relevantes 
            para la búsqueda de texto libre C{text}.
        """
        self._mrsw_lock.read_in()
        try:
            searcher = self._index.searcher()
            default_fields = ['tags', 'description', 'name']
            parser = whoosh.qparser.MultifieldParser(default_fields, schema=self._index_schema)
            query = parser.parse(text.decode(self._encoding))
            return set([result['hash'] for result in searcher.search(query)])
        finally:
            self._mrsw_lock.read_out()
                                   
    def info(self, file_hash, safe=False):
        """
        Obtiene información a partir del hash de un archivo.
        
        @type file_hash: C{str}
        @param file_hash: Identificador del archivo cuya información se quiere 
            obtener. Este hash identifica al archivo únicamente dentro del 
            sistema de ficheros distribuidos a partir de sus etiquetas y su 
            nombre.
            
        @rtype: C{dict}
        @return: Diccionario con los metadatos del archivo si este servidor
            tiene almacenado un archivo identificado por el hash dado,
            C{None} en caso contrario.
        
        @type safe: C{bool}
        @param safe: Expresa si la ejecución es "thread safe" o si el 
            método se tiene que encargar de la sincronización. Es falso por
            defecto
        """
        if not safe:
            self._mrsw_lock.read_in()
        try:
            searcher = self._index.searcher()
            doc = searcher.document(hash=file_hash.decode(self._encoding))
            if doc is not None and doc['action'] != 'delete':
                info = {}
                info['hash'] = doc['hash']
                info['tags'] = set(doc['tags'].split())
                info['description'] = doc['description']
                info['name'] = doc['name']
                info['size'] = doc['size']
                info['type'] = doc['type']
                info['owner'] = doc['owner']
                info['group'] = doc['group']
                info['perms'] = doc['perms']
                info['time']=doc['time']
                return info
            else:
                return None
        finally:
            if not safe:
                self._mrsw_lock.read_out()        
            
    def get_all_tags(self):
        """
        Obtiene un conjunto con todas los tags que tiene algún archivo 
        almacenado en este servidor.
        
        @rtype: C{set}
        @return: Conjunto con las etiquetas en este servidor.
        """
        reader = self._index.reader()
        searcher = self._index.searcher()
        tags = set()
        for tag in reader.lexicon('tags'):
            # Ugly hack! Returns tags that were removed from the index?!
            query = whoosh.query.Term('tags', tag)
            results = searcher.search(query, limit=1)
            if len(results) > 0:
                tags.add(tag)
        return tags
    
    def get_popular_tags(self, number):
        """
        Obtiene un conjunto con los tags más populares de los archivos 
        almacenados en este servidor.
        
        @type number: C{int}
        @param number: Cantidad de tags populares deseadas.
        
        @rtype: C{set}
        @return: Conjunto de tuplas de la forma (frecuencia, tag) 
        """        
        reader = self._index.reader()
        return set(reader.most_distinctive_terms('tags', number, u''))
        
    def terminate(self):
        """
        Este método es utilizado por la instancia de la clase C{TagFSServer} para
        indicar que va a dejar de estar disponible en la red este servidor y que
        se deben guardar todos los recursos abiertos.
        """
        self._sync_continue = False
        self._sync_cond.acquire()
        self._sync_cond.notify()
        self._sync_cond.release()
        self._sync_thread.join()
        self._index.close()