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

时间:2019-02-22 21:57:37

标签: python python-3.x file symlink python-3.3

我的目录中有成千上万个后代(至少1000个,可能不超过20,000个)。给定一个文件路径(保证存在),我想知道该文件可以在该目录中的哪里找到-包括通过符号链接。

例如,给定:

  • 目录路径为/base
  • 真实文件路径为/elsewhere/myfile
  • /base是指向/realbase
  • 的符号链接
  • /realbase/foo是指向/elsewhere的符号链接。
  • /realbase/bar/baz是指向/elsewhere/myfile的符号链接。

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

我可以通过递归检查/base中的每个符号链接来做到这一点,但这会非常慢。我希望有一个更优雅的解决方案。


动机

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

Sublime可在Linux,Windows和Mac OS上运行,因此理想情况下解决方案也应如此。

3 个答案:

答案 0 :(得分:1)

与许多事物一样,它比表面上显示的还要复杂。

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

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

路径也可以寻址“ 符号链接”,它可以指向另一条路径-该路径不必存在,它可以是另一条符号链接,也可以在另一个文件系统上,或者它可以指向原始路径并产生无限循环。

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


在我们开始之前...一些评论:

  1. 请参阅结尾以获取一些基准。我不认为这是一个重大问题,尽管可以肯定的是,这个文件系统是在i7上的6磁盘ZFS阵列上,所以使用较低规格的系统将需要更长的时间...
  2. 鉴于这是 不可能 ,而在某个时候没有在每个文件上调用stat(),您将很难找到一个更好的解决方案不会更加复杂(例如维护索引数据库,并引入所有问题)

如前所述,我们必须扫描(索引)整棵树。我知道这不是您想要执行的操作,但是如果不执行此操作则不可能...

要执行此操作,您需要收集 inodes ,而不是文件名,然后在事后对其进行审查...此处可能存在一些优化,但是我尝试简化优先级排序理解。

以下函数将为我们生成此结构:

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池,其中包含大量数据。诚然,这将在较低规格的系统上运行速度较慢...

要考虑的一些基准:

  • 约3.1k个文件和约850个目录中的链接的数据集。 运行时间少于3.5秒,后续运行时间约为80ms
  • 约30k个文件和约2.2k目录中的链接的数据集。 此操作不到30秒,在后续运行中大约需要300ms
  • 约73.5k文件和约8k目录中的链接的数据集。 这大约需要60秒,随后的运行大约需要800ms

使用简单的数学运算,大约有1140个stat()调用,其中有一个空的缓存,或者大约90k stat()个调用,一旦缓存已满-我不认为{{1 }}就像您认为的那样慢!

答案 1 :(得分:0)

符号链接不允许使用快捷方式。您必须了解所有可能指向感兴趣文件的相关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)

您最终获得了从所有符号链接的文件规范到一组别名的映射,通过这些别名可以访问该文件规范。

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

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

您可能选择以序列化的顺序序列化映射。如果您反复重新扫描一个大目录,则有机会记住各次运行的目录修改时间,并避免重新扫描该目录中的文件。不幸的是,如果 them 中有任何最近更改,您仍然必须递归到其后代目录。 您的子树可能具有足够的结构,可以使您避免递归超过K个层次,或者避免进入名称与某些正则表达式匹配的目录。

如果大多数FS更改是由少数几个程序(例如程序包管理器或构建系统)产生的,则使这些程序记录其操作可能会赢得性能。也就是说,如果您在每个午夜进行一次全面扫描,然后在千个目录中的两个目录中仅运行make,则可以选择仅重新扫描该对子树。

答案 2 :(得分:0)

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

也许:

特定于Windows:5 tools to monitor folder changes