Определите путь (и) файла относительно каталога, включая символические ссылки - PullRequest
0 голосов
/ 23 февраля 2019

У меня есть каталог с тысячами потомков (не менее 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 голосов
/ 25 февраля 2019

Это, как и многие другие вещи, сложнее, чем может показаться на поверхности.

Каждый объект в файловой системе указывает на inode, который описывает содержимое файла.Сущности - это то, что вы видите - файлы, каталоги, сокеты, блочные устройства, символьные устройства и т. Д. *

Доступ к содержимому одного " файла " можно получить через одно или несколькопути - каждый из этих путей называется « hard link ».Жесткие ссылки могут указывать только на файлы в одной и той же файловой системе, они не могут пересекать границу файловой системы.

Для пути также можно указать " символьную ссылку ", которая можетуказывает на другой путь - этот путь не должен существовать, он может быть другой символической ссылкой, он может находиться в другой файловой системе или может указывать на исходный путь, создавая бесконечный цикл.

Этоневозможно найти все ссылки (символические или жесткие), которые указывают на конкретную сущность, без сканирования всего дерева.


Прежде чем мы перейдем к этому ... некоторые комментарии:

  1. Смотрите конец для некоторых ориентиров.Я не уверен, что это серьезная проблема, хотя по общему признанию, эта файловая система находится в 6-дисковом массиве ZFS, на i7, поэтому использование системы с более низкой спецификацией займет больше времени ...
  2. Учитывая, что это невозможно без вызова stat() для каждого файла в какой-то момент, вы будете бороться за придумывание лучшего решения, которое не является значительно более сложным (например, поддержаниеиндексная база данных со всеми возникающими проблемами)

Как уже упоминалось, мы должны сканировать (индексировать) все дерево.Я знаю, что это не то, что вы хотите сделать, но без этого невозможно ...

Для этого вам нужно собрать inode , а не имена файлов, и просмотреть их после факта... здесь может быть некоторая оптимизация, но я постарался упростить определение приоритетов.

Следующая структура создаст для нас эту структуру:

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

I 'Мы создали дерево примеров, которое выглядит следующим образом:

$ 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 .

#!/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)

Я запустил это в своей системе из любопытства.Это 6x дисковый пул ZFS RAID-Z2 на i7-7700K с большим количеством данных для игры.По общему признанию, это будет работать несколько медленнее в системах с более низкими характеристиками ...

Некоторые тесты для рассмотрения:

  • Набор данных из ~ 3,1 тыс. Файлов и ссылок в ~ 850 каталогах.Это выполняется менее чем за 3,5 секунды, ~ 80 мс при последующих запусках
  • Набор данных ~ 30 тыс. Файлов и ссылок в каталогах ~ 2,2 тыс.Это выполняется менее чем за 30 секунд, ~ 300 мс при последующих запусках
  • Набор данных ~ 73,5 тыс. Файлов и ссылок в ~ 8 тыс. Каталогов.Это выполняется примерно за 60 секунд, ~ 800 мс при последующих запусках

При использовании простых математических вычислений это примерно 1140 stat() вызовов в секунду с пустым кешем или ~ 90k stat() вызовов в секунду послекеш заполнен - ​​я не думаю, что stat() так медленно, как вы думаете!

0 голосов
/ 25 февраля 2019

Мой первый инстинкт - чтобы операционная система или какая-либо служба сообщали вам, когда дерево файловой системы изменилось, вместо того, чтобы вы искали эти изменения.По сути, не изобретайте велосипед заново.

Возможно:

Для Windows: 5 инструментов для отслеживания изменений папок

0 голосов
/ 25 февраля 2019

Симлинки не допускают ярлыков.Вы должны знать обо всех соответствующих записях 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 только в двух из тысячи каталогов, вы можете выбрать повторное сканирование только этой пары поддеревьев.

...