view danboorufs.py @ 1:63ccd8b0d615 draft

Add tag exclusion support.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Thu, 02 Aug 2012 12:19:31 +0200
parents 215d51f2a82f
children 85cbd44f98b1
line wrap: on
line source

#!/usr/bin/env python2
# -*- encoding: utf-8 -*-

from __future__ import with_statement

from errno import ENOENT, ENOTDIR
from sys import argv, exit
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 file:
                for line in file:
                    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]

        if filter(lambda tag: tag not in self.tags, path[:-1]):
            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, fh=None):
        tags, file = self._split_path(path)
        path = file if file else self.root
        st = os.lstat(path)
        return dict((key, getattr(st, 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):
        tags, file = self._split_path(path)
        return os.open(file, flags)

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

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

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

        tags = set(tags)

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

        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.
        rm = reduce((lambda s, f: s.intersection(self.files[f])), files, taglist)
        taglist -= rm

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

    readlink = os.readlink

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

    def statfs(self, path):
        tags, file = self._split_path(path)
        path = file if file 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)