代码之家  ›  专栏  ›  技术社区  ›  Thom Smith

确定文件相对于目录的路径,包括符号链接

  •  0
  • Thom Smith  · 技术社区  · 7 年前

    我有一个目录,上面有数千个后代(至少1000个,可能不超过20000个)。给定一个文件路径(保证存在),我想知道该文件可以在该目录中的何处找到——包括通过符号链接。

    例如,给定:

    • 目录路径是 /base
    • 真正的文件路径是 /elsewhere/myfile .
    • /基地 是到的符号链接 /realbase
    • /realbase/foo 是到的符号链接 /elsewhere .
    • /realbase/bar/baz 是到的符号链接 /别处/myfile .

    我想找到路 /base/foo/myfile /base/bar/baz .

    我可以通过递归检查 /基地 ,但这会很慢。我希望有一个更优雅的解决方案。


    动机

    这是一个崇高的文本插件。当用户保存一个文件时,我们要检测它是否在Sublime配置目录中。特别是,即使文件是从config目录中符号链接的,并且用户在其物理路径(例如在他们的Dropbox目录中)编辑文件,我们也希望这样做。可能还有其他应用程序。

    Sublime可以在Linux、Windows和Mac操作系统上运行,所以理想的解决方案应该是这样。

    0 回复  |  直到 7 年前
        1
  •  1
  •   Attie    7 年前

    这和很多事情一样,比表面看起来更复杂。

    文件系统中的每个实体都指向 inode ,它描述文件的内容。实体就是你看到的东西-文件、目录、套接字、块设备、字符设备等等。。。

    一个“的内容” 文件 “可以通过一个或多个路径访问-这些路径中的每一个都称为” 硬链接 ". 硬链接只能指向同一文件系统上的文件,它们不能跨越文件系统的边界。

    也可以通过路径来处理“ 符号链接

    如果不扫描整个树,就不可能定位指向特定实体的所有链接(符号链接或硬链接)。


    在我们进入这之前。。。一些评论:

    1. 请看最后的一些基准。我不相信这是一个重要的问题,尽管无可否认,这个文件系统在一个6磁盘的ZFS阵列上,在i7上,所以使用较低规格的系统需要更长的时间。。。
    2. 鉴于这是 不可能的 不打电话 stat()

    如前所述,我们 必须 扫描(索引)整棵树。我知道这不是你想做的,但不这样做是不可能的。。。

    要做到这一点,你需要收集 ,而不是文件名,并在事后复查。。。这里可能有一些优化,但我尽量保持简单,以优先考虑理解。

    def get_map(scan_root):
        # this dict will have device IDs at the first level (major / minor) ...
        # ... and inodes IDs at the second level
        # each inode will have the following keys:
        #   - 'type'     the entity's type - i.e: dir, file, socket, etc...
        #   - 'links'    a list of all found hard links to the inode
        #   - 'symlinks' a list of all found symlinks to the inode
        # e.g: entities[2049][4756]['links'][0]     path to a hard link for inode 4756
        #      entities[2049][4756]['symlinks'][0]  path to a symlink that points at an entity with inode 4756
        entity_map = {}
    
        for root, dirs, files in os.walk(scan_root):
            root = '.' + root[len(scan_root):]
            for path in [ os.path.join(root, _) for _ in files ]:
                try:
                    p_stat = os.stat(path)
                except OSError as e:
                    if e.errno == 2:
                        print('Broken symlink [%s]... skipping' % ( path ))
                        continue
                    if e.errno == 40:
                        print('Too many levels of symbolic links [%s]... skipping' % ( path ))
                        continue
                    raise
    
                p_dev = p_stat.st_dev
                p_ino = p_stat.st_ino
    
                if p_dev not in entity_map:
                    entity_map[p_dev] = {}
                e_dev = entity_map[p_dev]
    
                if p_ino not in e_dev:
                    e_dev[p_ino] = {
                        'type': get_type(p_stat.st_mode),
                        'links': [],
                        'symlinks': [],
                    }
                e_ino = e_dev[p_ino]
    
                if os.lstat(path).st_ino == p_ino:
                    e_ino['links'].append(path)
                else:
                    e_ino['symlinks'].append(path)
    
        return entity_map
    

    我制作了一个示例树,如下所示:

    $ tree --inodes
    .
    ├── [  67687]  4 -> 5
    ├── [  67676]  5 -> 4
    ├── [  67675]  6 -> dead
    ├── [  67676]  a
    │   └── [  67679]  1
    ├── [  67677]  b
    │   └── [  67679]  2 -> ../a/1
    ├── [  67678]  c
    │   └── [  67679]  3
    └── [  67687]  d
        └── [  67688]  4
    
    4 directories, 7 files
    

    此函数的输出为:

    $ places
    Broken symlink [./6]... skipping
    Too many levels of symbolic links [./5]... skipping
    Too many levels of symbolic links [./4]... skipping
    {201: {67679: {'links': ['./a/1', './c/3'],
                   'symlinks': ['./b/2'],
                   'type': 'file'},
           67688: {'links': ['./d/4'], 'symlinks': [], 'type': 'file'}}}
    

    如果我们对 ./c/3 ./a/1 ...

    通过随后搜索我们感兴趣的路径,我们可以在此树中找到所有其他引用:

    def filter_map(entity_map, filename):
        for dev, inodes in entity_map.items():
            for inode, info in inodes.items():
                if filename in info['links'] or filename in info['symlinks']:
                    return info
    
    $ places ./a/1
    Broken symlink [./6]... skipping
    Too many levels of symbolic links [./5]... skipping
    Too many levels of symbolic links [./4]... skipping
    {'links': ['./a/1', './c/3'], 'symlinks': ['./b/2'], 'type': 'file'}
    

    此演示的完整源代码如下。请注意,我使用了相对路径来保持简单,但最好将其更新为使用绝对路径。另外,任何指向树外部的符号链接当前都没有对应的 link ... 这是给读者的练习。

    也可以在填充树时收集数据(如果这是与您的过程一起工作的话)。。。你可以用 inotify 为了处理好这件事-甚至有一个 python module

    #!/usr/bin/env python3
    
    import os, sys, stat
    from pprint import pprint
    
    def get_type(mode):
        if stat.S_ISDIR(mode):
            return 'directory'
        if stat.S_ISCHR(mode):
            return 'character'
        if stat.S_ISBLK(mode):
            return 'block'
        if stat.S_ISREG(mode):
            return 'file'
        if stat.S_ISFIFO(mode):
            return 'fifo'
        if stat.S_ISLNK(mode):
            return 'symlink'
        if stat.S_ISSOCK(mode):
            return 'socket'
        return 'unknown'
    
    def get_map(scan_root):
        # this dict will have device IDs at the first level (major / minor) ...
        # ... and inodes IDs at the second level
        # each inode will have the following keys:
        #   - 'type'     the entity's type - i.e: dir, file, socket, etc...
        #   - 'links'    a list of all found hard links to the inode
        #   - 'symlinks' a list of all found symlinks to the inode
        # e.g: entities[2049][4756]['links'][0]     path to a hard link for inode 4756
        #      entities[2049][4756]['symlinks'][0]  path to a symlink that points at an entity with inode 4756
        entity_map = {}
    
        for root, dirs, files in os.walk(scan_root):
            root = '.' + root[len(scan_root):]
            for path in [ os.path.join(root, _) for _ in files ]:
                try:
                    p_stat = os.stat(path)
                except OSError as e:
                    if e.errno == 2:
                        print('Broken symlink [%s]... skipping' % ( path ))
                        continue
                    if e.errno == 40:
                        print('Too many levels of symbolic links [%s]... skipping' % ( path ))
                        continue
                    raise
    
                p_dev = p_stat.st_dev
                p_ino = p_stat.st_ino
    
                if p_dev not in entity_map:
                    entity_map[p_dev] = {}
                e_dev = entity_map[p_dev]
    
                if p_ino not in e_dev:
                    e_dev[p_ino] = {
                        'type': get_type(p_stat.st_mode),
                        'links': [],
                        'symlinks': [],
                    }
                e_ino = e_dev[p_ino]
    
                if os.lstat(path).st_ino == p_ino:
                    e_ino['links'].append(path)
                else:
                    e_ino['symlinks'].append(path)
    
        return entity_map
    
    def filter_map(entity_map, filename):
        for dev, inodes in entity_map.items():
            for inode, info in inodes.items():
                if filename in info['links'] or filename in info['symlinks']:
                    return info
    
    entity_map = get_map(os.getcwd())
    
    if len(sys.argv) == 2:
        entity_info = filter_map(entity_map, sys.argv[1])
        pprint(entity_info)
    else:
        pprint(entity_map)
    

    出于好奇,我在我的系统上运行了这个。它是i7-7700K上的一个6x磁盘的ZFS RAID-Z2池,有大量的数据可供使用。诚然,在低规格的系统上运行会慢一些。。。

    需要考虑的一些基准:

    • 包含~850个目录中~3.1k文件和链接的数据集。
    • 包含~2.2k目录中~30k个文件和链接的数据集。
    • 包含~73.5k文件和~8k目录中链接的数据集。 这大约需要60秒,后续运行约800毫秒

    用简单的数学计算,大约是1140 统计() 使用空缓存每秒调用数,或~90k 统计() 缓存填满后每秒的调用数-我不这么认为 统计() 就像你想的那样慢!

        2
  •  0
  •   J_H    7 年前

    符号链接不允许使用快捷方式。您必须了解所有可能指向感兴趣文件的相关FS条目。它对应于创建一个空目录,然后监听该目录下的所有文件创建事件,或者扫描当前位于该目录下的所有文件。运行以下命令。

    #! /usr/bin/env python
    
    from pathlib import Path
    import collections
    import os
    import pprint
    import stat
    
    
    class LinkFinder:
    
        def __init__(self):
            self.target_to_orig = collections.defaultdict(set)
    
        def scan(self, folder='/tmp'):
            for fspec, target in self._get_links(folder):
                self.target_to_orig[target].add(fspec)
    
        def _get_links(self, folder):
            for root, dirs, files in os.walk(Path(folder).resolve()):
                for file in files:
                    fspec = os.path.join(root, file)
                    if stat.S_ISLNK(os.lstat(fspec).st_mode):
                        target = os.path.abspath(os.readlink(fspec))
                        yield fspec, target
    
    
    if __name__ == '__main__':
        lf = LinkFinder()
        for folder in '/base /realbase'.split():
            lf.scan(folder)
        pprint.pprint(lf.target_to_orig)
    

    符号链接目标可以是文件或目录,因此要在给定的filespec上正确使用映射,必须反复截断它,询问映射中是否出现父目录或祖先目录。

    悬挂的符号链接不是专门处理的,它们只是允许悬挂。

    您可以选择序列化映射,可能是按排序的顺序。如果您反复重新扫描一个大目录,就有机会在运行期间记住目录mod时间,并避免重新扫描该目录中的文件。不幸的是,如果 他们 最近发生了变化。 您的子树可能显示出足够的结构,以避免递归超过K级,或者避免降到名称与某些正则表达式匹配的目录中。

    如果大多数FS更改是由少数程序(如包管理器或构建系统)生成的,那么让这些程序记录它们的操作可以获得性能上的胜利。也就是说,如果你在每个午夜做一个完整的扫描,然后你运行 make 在一千个目录中,只有两个目录可以选择重新扫描这对子树。

        3
  •  0
  •   shrewmouse    7 年前

    我的第一反应是让操作系统或某些服务在文件系统树发生更改时通知您,而不是您查找更改。基本上不要重新发明轮子。

    也许 吧:

    特定于Windows: 5 tools to monitor folder changes