Как проверить, является ли каталог вспомогательной директорией другого каталога
Мне нравится писать систему шаблонов в Python, которая позволяет включать файлы.
например.
This is a template
You can safely include files with safe_include`othertemplate.rst`
Как вы знаете, файлы могут быть опасными. Например, если я использую систему шаблонов в веб-приложении, которое позволяет пользователям создавать свои собственные шаблоны, они могут сделать что-то вроде
I want your passwords: safe_include`/etc/password`
Поэтому поэтому я должен ограничить включение файлов в файлы, которые, например, находятся в определенном подкаталоге (например, /home/user/templates
)
Вопрос в следующем: как я могу проверить, находится ли /home/user/templates/includes/inc1.rst
в подкаталоге /home/user/templates
?
Будет ли работать следующий код и быть в безопасности?
import os.path
def in_directory(file, directory, allow_symlink = False):
#make both absolute
directory = os.path.abspath(directory)
file = os.path.abspath(file)
#check whether file is a symbolic link, if yes, return false if they are not allowed
if not allow_symlink and os.path.islink(file):
return False
#return true, if the common prefix of both is equal to directory
#e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b
return os.path.commonprefix([file, directory]) == directory
До тех пор, пока allow_symlink
является False, он должен быть безопасным, я думаю. Конечно, разрешение символических ссылок сделало бы его небезопасным, если пользователь сможет создавать такие ссылки.
ОБНОВЛЕНИЕ - Решение
Приведенный выше код не работает, если промежуточные каталоги являются символическими ссылками.
Чтобы предотвратить это, вы должны использовать realpath
вместо abspath
.
UPDATE: добавление каталога trailing/to для решения проблемы с помощью commonprefix(), который указал Reorx.
Это также делает ненужным allow_symlink
, поскольку символические ссылки расширяются до их реального адресата
import os.path
def in_directory(file, directory):
#make both absolute
directory = os.path.join(os.path.realpath(directory), '')
file = os.path.realpath(file)
#return true, if the common prefix of both is equal to directory
#e.g. /a/b/c/d.rst and directory is /a/b, the common prefix is /a/b
return os.path.commonprefix([file, directory]) == directory
Ответы
Ответ 1
os.path.realpath(path): возвращает канонический путь указанного имени файла, исключая любые символические ссылки, встречающиеся в пути (если они поддерживаются операционной системой).
Используйте его в имени каталога и подкаталога, затем проверьте, что последний начинается с прежнего.
Ответ 2
Модуль Python 3 pathlib
делает это простым с помощью атрибута Path.parents. Например:
from pathlib import Path
root = Path('/path/to/root')
child = root / 'some' / 'child' / 'dir'
other = Path('/some/other/path')
Тогда:
>>> root in child.parents
True
>>> other in child.parents
False
Ответ 3
def is_subdir(path, directory):
path = os.path.realpath(path)
directory = os.path.realpath(directory)
relative = os.path.relpath(path, directory)
return not relative.startswith(os.pardir + os.sep)
Ответ 4
Проблемы со многими из предложенных методов
Если вы собираетесь тестировать исходное происхождение каталога с помощью метода сравнения строк или os.path.commonprefix
, они подвержены ошибкам с одинаково названными путями или относительными путями. Например:
-
/path/to/files/myfile
будет отображаться как дочерний путь /path/to/file
с использованием многих методов.
-
/path/to/files/../../myfiles
не будет отображаться в качестве родительского элемента /path/myfiles/myfile
многими способами. На самом деле, это так.
предыдущий ответ Роб Деннис дает хороший способ сравнить происхождение пути, не сталкиваясь с этими проблемами. В Python 3.4 добавлен модуль pathlib
, который может выполнять эти операции с более сложным способом, необязательно, без ссылки на базовую ОС. jme описал в еще один предыдущий ответ, как использовать pathlib
с целью точного определения того, является ли один путь дочерним по отношению к другому. Если вы предпочитаете не использовать pathlib
(не уверен, почему, это довольно здорово), то Python 3.5 представил новый метод на основе ОС в os.path
, который позволяет выполнять проверки родительского родительского пути в аналогичных точках и ошибках, свободный способ с гораздо меньшим количеством кода.
Новое для Python 3.5
В Python 3.5 введена функция os.path.commonpath
. Это метод, специфичный для ОС, в котором работает код. Вы можете использовать commonpath
следующим образом, чтобы точно определить происхождение пути:
def path_is_parent(parent_path, child_path):
# Smooth out relative path names, note: if you are concerned about symbolic links, you should use os.path.realpath too
parent_path = os.path.abspath(parent_path)
child_path = os.path.abspath(child_path)
# Compare the common path of the parent and child path with the common path of just the parent path. Using the commonpath method on just the parent path will regularise the path name in the same way as the comparison that deals with both paths, removing any trailing path separator
return os.path.commonpath([parent_path]) == os.path.commonpath([parent_path, child_path])
Точный однострочный
Вы можете объединить всю партию в однострочный оператор if в Python 3.5. Он уродливый, он включает ненужные повторяющиеся вызовы на os.path.abspath
, и он определенно не поместится в руководящих принципах длины строки в 80 символов PEP 8, но если вам нравится такая штука, вот что:
if os.path.commonpath([os.path.abspath(parent_path_to_test)]) == os.path.commonpath([os.path.abspath(parent_path_to_test), os.path.abspath(child_path_to_test)]):
# Yes, the child path is under the parent path
Ответ 5
так, мне это нужно, и из-за критики о commonprefx я пошел по-другому:
def os_path_split_asunder(path, debug=False):
"""
http://stackoverflow.com/a/4580931/171094
"""
parts = []
while True:
newpath, tail = os.path.split(path)
if debug: print repr(path), (newpath, tail)
if newpath == path:
assert not tail
if path: parts.append(path)
break
parts.append(tail)
path = newpath
parts.reverse()
return parts
def is_subdirectory(potential_subdirectory, expected_parent_directory):
"""
Is the first argument a sub-directory of the second argument?
:param potential_subdirectory:
:param expected_parent_directory:
:return: True if the potential_subdirectory is a child of the expected parent directory
>>> is_subdirectory('/var/test2', '/var/test')
False
>>> is_subdirectory('/var/test', '/var/test2')
False
>>> is_subdirectory('var/test2', 'var/test')
False
>>> is_subdirectory('var/test', 'var/test2')
False
>>> is_subdirectory('/var/test/sub', '/var/test')
True
>>> is_subdirectory('/var/test', '/var/test/sub')
False
>>> is_subdirectory('var/test/sub', 'var/test')
True
>>> is_subdirectory('var/test', 'var/test')
True
>>> is_subdirectory('var/test', 'var/test/fake_sub/..')
True
>>> is_subdirectory('var/test/sub/sub2/sub3/../..', 'var/test')
True
>>> is_subdirectory('var/test/sub', 'var/test/fake_sub/..')
True
>>> is_subdirectory('var/test', 'var/test/sub')
False
"""
def _get_normalized_parts(path):
return os_path_split_asunder(os.path.realpath(os.path.abspath(os.path.normpath(path))))
# make absolute and handle symbolic links, split into components
sub_parts = _get_normalized_parts(potential_subdirectory)
parent_parts = _get_normalized_parts(expected_parent_directory)
if len(parent_parts) > len(sub_parts):
# a parent directory never has more path segments than its child
return False
# we expect the zip to end with the short path, which we know to be the parent
return all(part1==part2 for part1, part2 in zip(sub_parts, parent_parts))
Ответ 6
def is_in_directory(filepath, directory):
return os.path.realpath(filepath).startswith(
os.path.realpath(directory) + os.sep)
Ответ 7
Мне нравится "path in other_path.parents", упомянутый в другом ответе, потому что я большой поклонник pathlib, НО я считаю, что этот подход немного тяжелый (он создает один экземпляр Path для каждого родителя до корня пути). Также случай, когда путь == other_path терпит неудачу с этим подходом, тогда как os.commonpath будет успешным в этом случае.
Ниже представлен другой подход с собственным набором плюсов и минусов по сравнению с другими методами, идентифицированными в различных ответах:
try:
other_path.relative_to(path)
except ValueError:
...no common path...
else:
...common path...
который является немного более подробным, но может быть легко добавлен как функция в вашем приложении общего модуля утилит или даже добавить метод Path в момент запуска.
Ответ 8
Ответ Оливера, кажется, работает, но .resolve()
прежнему необходимо вызывать на обоих путях перед запуском .relative_to()
, если речь идет о потенциально относительных путях.
Ответ 9
Я бы проверил результат от commonprefix на имя файла, чтобы получить лучший ответ, примерно так:
def is_in_folder(filename, folder='/tmp/'):
# normalize both parameters
fn = os.path.normpath(filename)
fd = os.path.normpath(folder)
# get common prefix
commonprefix = os.path.commonprefix([fn, fd])
if commonprefix == fd:
# in case they have common prefix, check more:
sufix_part = fn.replace(fd, '')
sufix_part = sufix_part.lstrip('/')
new_file_name = os.path.join(fd, sufix_part)
if new_file_name == fn:
return True
pass
# for all other, it False
return False
Ответ 10
Основываясь на другом ответе здесь, с исправлением и с дружественным пользователем именем:
def isA_subdirOfB_orAisB(A, B):
"""It is assumed that A is a directory."""
relative = os.path.relpath(os.path.realpath(A),
os.path.realpath(B))
return not (relative == os.pardir
or relative.startswith(os.pardir + os.sep))