view danboorufs.py @ 2:85cbd44f98b1 draft

Add license, (try to) respect the PEP 8, and don’t override the file builtin.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Tue, 21 Aug 2012 20:17:49 +0200
parents 63ccd8b0d615
children 880904f1071f
line wrap: on
line source

#!/usr/bin/env python2
# -*- encoding: utf-8 -*-
#
#
# Copyright © 2012 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


from __future__ import with_statement

from errno import ENOENT, ENOTDIR
from sys import argv
from threading import Lock
from time import time

import os

from fuse import FUSE, FuseOSError, Operations, LoggingMixIn


class Danbooru(LoggingMixIn, Operations):
    '''
    Represent a list of images as a filesystem tree, with nice tag filtering.
    '''

    def __init__(self, tagfiles, root):
        '''
        Takes a list of files containing the tags. They have to be named as the
        image, with ".tags" at the end.
        '''
        self.paths = {}
        self.files = {}
        self.tags = {}
        self.cache = {}

        start = time()

        for name in tagfiles:
            filename = name.replace('.tags', '')
            basename = os.path.basename(filename)
            self.paths[basename] = filename
            tags = []
            self.files[basename] = tags
            with open(name, 'r') as tagfile:
                for line in tagfile:
                    for tag in line.split():
                        tag = tag.decode('UTF-8')
                        tag = tag.replace('/', u'�') #XXX
                        tags.append(tag)
                        self.tags.setdefault(tag, []).append(basename)

        print('[%d] Index done.' % (time() - start))

        self.root = root
        self.rwlock = Lock()

    def _split_path(self, path):
        if path == '/':
            return (None, None)

        real_path = path[1:].split('/')

        # Remove the leading - of tag exclusion.
        path = [tag[1:] if tag[0] == '-' else tag for tag in real_path]

        for tag in path[:-1]:
            if tag not in self.tags:
                raise FuseOSError(ENOENT)

        if path[-1] in self.tags:
            return (real_path, None)

        if path[-1] not in self.paths:
            raise FuseOSError(ENOENT)

        return (real_path[:-1], self.paths[real_path[-1]])

    def access(self, path, mode):
        self._split_path(path)

    def getattr(self, path, file_handle=None):
        _, filename = self._split_path(path)
        path = filename if filename else self.root
        stat = os.lstat(path)
        return dict((key, getattr(stat, key)) for key in ('st_atime',
                        'st_ctime', 'st_gid', 'st_mode', 'st_mtime',
                        'st_nlink', 'st_size', 'st_uid'))

    getxattr = None
    listxattr = None

    def open(self, path, flags):
        _, filename = self._split_path(path)
        return os.open(filename, flags)

    def read(self, path, size, offset, file_handle):
        with self.rwlock:
            os.lseek(file_handle, offset, 0)
            return os.read(file_handle, size)

    def readdir(self, path, file_handle):
        if path == '/':
            return ['.', '..'] + self.tags.keys() + self.files.keys()

        tags, filename = self._split_path(path)
        if filename:
            return FuseOSError(ENOTDIR)

        tags = set(tags)

        key = ' '.join(tags)
        if key in self.cache:
            return ['.', '..'] + self.cache[key]

        inclusion_tags = set(tag for tag in tags if tag[0] != '-')
        exclusion_tags = set(tag[1:] for tag in tags if tag[0] == '-')

        # Get the list of the files corresponding to those tags.
        files = reduce((lambda s, t: s.intersection(self.tags[t])),
                       inclusion_tags, set(self.files))
        files -= set([f for f in files
                      if exclusion_tags.intersection(self.files[f])])

        # Those next two steps are for useless tags removal.

        # Get the tags of those files.
        taglist = reduce((lambda s, f: s.union(self.files[f])), files, set())
        taglist -= tags

        # Remove the tags that can’t precise the file list anymore.
        remove = reduce((lambda s, f: s.intersection(self.files[f])), files,
                        taglist)
        taglist -= remove

        self.cache[key] = list(taglist) + list(files)
        return ['.', '..'] + self.cache[key]

    readlink = os.readlink

    def release(self, path, file_handle):
        return os.close(file_handle)

    def statfs(self, path):
        _, filename = self._split_path(path)
        path = filename if filename else self.root
        stv = os.statvfs(path)
        return dict((key, getattr(stv, key)) for key in ('f_bavail', 'f_bfree',
            'f_blocks', 'f_bsize', 'f_favail', 'f_ffree', 'f_files', 'f_flag',
            'f_frsize', 'f_namemax'))

    utimens = os.utime


if __name__ == '__main__':
    if len(argv) < 3:
        print('usage: %s <tag file> [<tag file>...] <mountpoint>' % argv[0])
        exit(1)

    mountpoint = argv.pop()

    fuse = FUSE(Danbooru(argv[1:], os.path.dirname(mountpoint)), mountpoint,
                foreground=True)