Exemplo n.º 1
0
    def file_by_mapping(self, srcdirpath):
        ''' Examine the `{TAGGER_TAG_PREFIX_DEFAULT}.file_by` tag for `srcdirpath`.
        Return a mapping of specific tag values to filing locations
        derived via `per_tag_auto_file_map`.

        The file location specification in the tag may be a list or a string
        (for convenient single locations).

        For example, I might tag my downloads directory with:

            {TAGGER_TAG_PREFIX_DEFAULT}.file_by={{"abn":"~/them/me"}}

        indicating that files with an `abn` tag may be filed in the `~/them/me` directory.
        That directory is then walked looking for the tag `abn`,
        and wherever some tag `abn=`*value*` is found on a subdirectory
        a mapping entry for `abn=`*value*=>*subdirectory* is added.

        This results in a direct mapping of specific tag values to filing locations,
        such as:

            {{ Tag('abn','***********') => ['/path/to/them/me/abn-**-***-***-***'] }}

        because the target subdirectory has been tagged with `abn="***********"`.
    '''
        assert isdirpath(srcdirpath)
        assert not srcdirpath.startswith('~')
        assert '~' not in srcdirpath
        fstags = self.fstags
        tagged = fstags[srcdirpath]
        key = tagged.filepath
        try:
            mapping = self._file_by_mappings[key]
        except KeyError:
            mapping = defaultdict(set)
            file_by = self.conf_tag(fstags[srcdirpath].all_tags, 'file_by', {})
            # group the tags by file_by target path
            grouped = defaultdict(set)
            for tag_name, file_to in file_by.items():
                if isinstance(file_to, str):
                    file_to = (file_to, )
                for file_to_path in file_to:
                    if not isabspath(file_to_path):
                        if file_to_path.startswith('~'):
                            file_to_path = expanduser(file_to_path)
                            assert isabspath(file_to_path)
                        else:
                            file_to_path = joinpath(srcdirpath, file_to_path)
                    file_to_path = realpath(file_to_path)
                    grouped[file_to_path].add(tag_name)
            # walk each path for its tag_names of interest
            for file_to_path, tag_names in sorted(grouped.items()):
                with Pfx("%r:%r", file_to_path, tag_names):
                    # accrue destination paths by tag values
                    for bare_key, dstpaths in self.per_tag_auto_file_map(
                            file_to_path, tag_names).items():
                        mapping[bare_key].update(dstpaths)
            self._file_by_mappings[key] = mapping
        return mapping
Exemplo n.º 2
0
def expand_path(path, basedir=None):
    ''' Expand a path specification.
  '''
    path = expanduser(path)
    if basedir and not isabspath(path):
        path = joinpath(basedir, path)
    return path
Exemplo n.º 3
0
    def _run(self, *calargv, subp_options=None):
        ''' Run a Calibre utility command.

        Parameters:
        * `calargv`: an iterable of the calibre command to issue;
          if the command name is not an absolute path
          it is expected to come from `self.CALIBRE_BINDIR_DEFAULT`
        * `subp_options`: optional mapping of keyword arguments
          to pass to `subprocess.run`
    '''
        X("calargv=%r", calargv)
        if subp_options is None:
            subp_options = {}
        subp_options.setdefault('check', True)
        cmd, *calargv = calargv
        if not isabspath(cmd):
            cmd = joinpath(self.CALIBRE_BINDIR_DEFAULT, cmd)
        print("RUN", cmd, *calargv)
        try:
            cp = pfx_call(run, [cmd, *calargv], **subp_options)
        except CalledProcessError as cpe:
            error(
                "run fails, exit code %s:\n  %s",
                cpe.returncode,
                ' '.join(map(repr, cpe.cmd)),
            )
            if cpe.stderr:
                print(cpe.stderr.replace('\n', '  \n'), file=sys.stderr)
            raise
        return cp
Exemplo n.º 4
0
 def datafile_Store(
     self,
     store_name,
     clause_name,
     *,
     path=None,
     basedir=None,
     hashclass=None,
 ):
     ''' Construct a VTDStore from a "datafile" clause.
 '''
     if basedir is None:
         basedir = self.get_default('basedir')
     if path is None:
         path = clause_name
     path = longpath(path)
     if not isabspath(path):
         if path.startswith('./'):
             path = abspath(path)
         else:
             if basedir is None:
                 raise ValueError('relative path %r but no basedir' %
                                  (path, ))
             basedir = longpath(basedir)
             path = joinpath(basedir, path)
     return VTDStore(store_name, path, hashclass=hashclass)
Exemplo n.º 5
0
 def datadir_Store(
     self,
     store_name,
     clause_name,
     *,
     path=None,
     basedir=None,
     hashclass=None,
     raw=False,
 ):
     ''' Construct a DataDirStore from a "datadir" clause.
 '''
     if basedir is None:
         basedir = self.get_default('basedir')
     if path is None:
         path = clause_name
     path = longpath(path)
     if not isabspath(path):
         if path.startswith('./'):
             path = abspath(path)
         else:
             if basedir is None:
                 raise ValueError('relative path %r but no basedir' %
                                  (path, ))
             basedir = longpath(basedir)
             path = joinpath(basedir, path)
     if isinstance(raw, str):
         raw = truthy_word(raw)
     return DataDirStore(store_name, path, hashclass=hashclass, raw=raw)
Exemplo n.º 6
0
class HasFSPath:
    ''' An object with a `.fspath` attribute representing a filesystem location.
  '''
    @require(lambda fspath: isabspath(fspath))
    def __init__(self, fspath):
        self.fspath = fspath

    def pathto(self, subpath):
        ''' The full path to `subpath`, a relative path below `self.fspath`.
    '''
        return joinpath(self.fspath, subpath)
Exemplo n.º 7
0
 def platonic_Store(
     self,
     store_name,
     clause_name,
     *,
     path=None,
     basedir=None,
     follow_symlinks=False,
     meta=None,
     archive=None,
     hashclass=None,
 ):
     ''' Construct a PlatonicStore from a "datadir" clause.
 '''
     if basedir is None:
         basedir = self.get_default('basedir')
     if path is None:
         path = clause_name
         debug("path from clausename: %r", path)
     path = longpath(path)
     debug("longpath(path) ==> %r", path)
     if not isabspath(path):
         if path.startswith('./'):
             path = abspath(path)
             debug("abspath ==> %r", path)
         else:
             if basedir is None:
                 raise ValueError('relative path %r but no basedir' %
                                  (path, ))
             basedir = longpath(basedir)
             debug("longpath(basedir) ==> %r", basedir)
             path = joinpath(basedir, path)
             debug("path ==> %r", path)
     if follow_symlinks is None:
         follow_symlinks = False
     if meta is None:
         meta_store = None
     elif isinstance(meta, str):
         meta_store = Store(meta, self)
     if isinstance(archive, str):
         archive = longpath(archive)
     return PlatonicStore(
         store_name,
         path,
         hashclass=hashclass,
         indexclass=None,
         follow_symlinks=follow_symlinks,
         meta_store=meta_store,
         archive=archive,
         flags_prefix='VT_' + clause_name,
     )
Exemplo n.º 8
0
 def filecache_Store(
     self,
     store_name,
     clause_name,
     *,
     path=None,
     max_files=None,
     max_file_size=None,
     basedir=None,
     backend=None,
     hashclass=None,
 ):
     ''' Construct a FileCacheStore from a "filecache" clause.
 '''
     if basedir is None:
         basedir = self.get_default('basedir')
     if path is None:
         path = clause_name
         debug("path from clausename: %r", path)
     path = longpath(path)
     debug("longpath(path) ==> %r", path)
     if isinstance(max_files, str):
         max_files = scaled_value(max_files)
     if isinstance(max_file_size, str):
         max_file_size = scaled_value(max_file_size)
     if backend is None:
         backend_store = None
     else:
         backend_store = self.Store_from_spec(backend)
     if not isabspath(path):
         if path.startswith('./'):
             path = abspath(path)
             debug("abspath ==> %r", path)
         else:
             if basedir is None:
                 raise ValueError('relative path %r but no basedir' %
                                  (path, ))
             basedir = longpath(basedir)
             debug("longpath(basedir) ==> %r", basedir)
             path = joinpath(basedir, path)
             debug("path ==> %r", path)
     return FileCacheStore(
         store_name,
         backend_store,
         path,
         max_cachefile_size=max_file_size,
         max_cachefiles=max_files,
         hashclass=hashclass,
     )
Exemplo n.º 9
0
 def cmd_fileby(self, argv):
     ''' Usage: {cmd} [-d dirpath] tag_name paths...
       Add paths to the tagger.file_by mapping for the current directory.
       -d dirpath    Adjust the mapping for a different directory.
 '''
     dirpath = '.'
     opts, argv = getopt(argv, 'd:')
     for opt, val in opts:
         with Pfx(opt):
             if opt == '-d':
                 dirpath = val
             else:
                 raise RuntimeError("unhandled option")
     if not argv:
         raise GetoptError("missing tag_name")
     tag_name = argv.pop(0)
     if not Tag.is_valid_name(tag_name):
         raise GetoptError("invalid tag_name: %r" % (tag_name, ))
     if not argv:
         raise GetoptError("missing paths")
     tagged = self.options.fstags[dirpath]
     file_by = tagged.get('tagger.file_by', {})
     paths = file_by.get(tag_name, ())
     if isinstance(paths, str):
         paths = [paths]
     paths = set(paths)
     paths.update(argv)
     homedir = os.environ.get('HOME')
     if homedir and isabspath(homedir):
         homedir_ = homedir + os.sep
         paths = set(
             ('~/' +
              path[len(homedir_):] if path.startswith(homedir_) else path)
             for path in paths)
     file_by[tag_name] = sorted(paths)
     tagged['tagger.file_by'] = file_by
     print("tagger.file_by =", repr(file_by))
Exemplo n.º 10
0
    def file_by_tags(self,
                     path: str,
                     prune_inherited=False,
                     no_link=False,
                     do_remove=False):
        ''' Examine a file's tags.
        Where those tags imply a location, link the file to that location.
        Return the list of links made.

        Parameters:
        * `path`: the source path to file
        * `prune_inherited`: optional, default `False`:
          prune the inherited tags from the direct tags on the target
        * `no_link`: optional, default `False`;
          do not actually make the hard link, just report the target
        * `do_remove`: optional, default `False`;
          remove source files if successfully linked

        Note: if `path` is already linked to an implied location
        that location is also included in the returned list.

        The filing process is as follows:
        - for each target directory, initially `dirname(path)`,
          look for a filing map on tag `file_by_mapping`
        - for each directory in that mapping which matches a tag from `path`,
          queue it as an additional target directory
        - if there were no matching directories, file `path` at the current
          target directory under the filename
          returned by `{TAGGER_TAG_PREFIX_DEFAULT}.auto_name`
    '''
        if do_remove and no_link:
            raise ValueError("do_remove and no_link may not both be true")
        fstags = self.fstags
        # start the queue with the resolved `path`
        tagged = fstags[path]
        srcpath = tagged.filepath
        tags = tagged.all_tags
        # a queue of reference directories
        q = ListQueue((dirname(srcpath), ))
        linked_to = []
        seen = set()
        for refdirpath in unrepeated(q, signature=abspath, seen=seen):
            with Pfx(refdirpath):
                # places to redirect this file
                mapping = self.file_by_mapping(refdirpath)
                interesting_tag_names = {tag.name for tag in mapping.keys()}
                # locate specific filing locations in the refdirpath
                refile_to = set()
                for tag_name in sorted(interesting_tag_names):
                    with Pfx("tag_name %r", tag_name):
                        if tag_name not in tags:
                            continue
                        bare_tag = Tag(tag_name, tags[tag_name])
                        try:
                            target_dirs = mapping.get(bare_tag, ())
                        except TypeError as e:
                            warning("  %s not mapped (%s), skipping", bare_tag,
                                    e)
                            continue
                        if not target_dirs:
                            continue
                        # collect other filing locations
                        refile_to.update(target_dirs)
                # queue further locations if they are new
                if refile_to:
                    new_refile_to = set(map(abspath, refile_to)) - seen
                    if new_refile_to:
                        q.extend(new_refile_to)
                        continue
                # file locally (no new locations)
                dstbase = self.auto_name(srcpath, refdirpath, tags)
                with Pfx("%s => %s", refdirpath, dstbase):
                    dstpath = dstbase if isabspath(dstbase) else joinpath(
                        refdirpath, dstbase)
                    if existspath(dstpath):
                        if not samefile(srcpath, dstpath):
                            warning("already exists, skipping")
                        continue
                    if no_link:
                        linked_to.append(dstpath)
                    else:
                        linkto_dirpath = dirname(dstpath)
                        if not isdirpath(linkto_dirpath):
                            pfx_call(os.mkdir, linkto_dirpath)
                        try:
                            pfx_call(os.link, srcpath, dstpath)
                        except OSError as e:
                            warning("cannot link to %r: %s", dstpath, e)
                        else:
                            linked_to.append(dstpath)
                            fstags[dstpath].update(tags)
                            if prune_inherited:
                                fstags[dstpath].prune_inherited()
        if linked_to and do_remove:
            S = os.stat(srcpath)
            if S.st_nlink < 2:
                warning("not removing %r, unsufficient hard links (%s)",
                        srcpath, S.st_nlink)
            else:
                pfx_call(os.remove, srcpath)
        return linked_to