Это, как и многие другие вещи, сложнее, чем может показаться на поверхности.
Каждый объект в файловой системе указывает на inode
, который описывает содержимое файла.Сущности - это то, что вы видите - файлы, каталоги, сокеты, блочные устройства, символьные устройства и т. Д. *
Доступ к содержимому одного " файла " можно получить через одно или несколькопути - каждый из этих путей называется « hard link ».Жесткие ссылки могут указывать только на файлы в одной и той же файловой системе, они не могут пересекать границу файловой системы.
Для пути также можно указать " символьную ссылку ", которая можетуказывает на другой путь - этот путь не должен существовать, он может быть другой символической ссылкой, он может находиться в другой файловой системе или может указывать на исходный путь, создавая бесконечный цикл.
Этоневозможно найти все ссылки (символические или жесткие), которые указывают на конкретную сущность, без сканирования всего дерева.
Прежде чем мы перейдем к этому ... некоторые комментарии:
- Смотрите конец для некоторых ориентиров.Я не уверен, что это серьезная проблема, хотя по общему признанию, эта файловая система находится в 6-дисковом массиве ZFS, на i7, поэтому использование системы с более низкой спецификацией займет больше времени ...
- Учитывая, что это невозможно без вызова
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()
так медленно, как вы думаете!