Ответ 1
TL;DR
Вызвать функцию is_path_exists_or_creatable()
, определенную ниже.
Строго Python 3. Вот как мы катимся.
Рассказ о двух вопросах
Вопрос "Как проверить правильность имени пути и, допустимые пути, существование или возможность записи этих путей?" очевидно, два отдельных вопроса. Оба интересны, и ни один из них не получил действительно удовлетворительного ответа здесь... или, ну, где угодно, что я мог бы grep.
vikki ответ, вероятно, будет самым близким, но имеет замечательные недостатки:
- Неправильно открывать (... и затем не удается достоверно закрыть) файлы.
- Неправильно писать (... и затем не удалять надежные файлы с закрытием или удалением) 0-байтов.
- Игнорирование ошибок, специфичных для ОС, для разграничения между недопустимыми недопустимыми именами путей и игнорируемыми файловыми системами. Неудивительно, что это критически важно для Windows. (См. Ниже.)
- Игнорирование условий гонки, возникающих в результате внешних процессов, одновременно (пере) перемещение родительских каталогов проверяемого пути. (См. Ниже.)
- Игнорирование тайм-аутов подключения, возникающих в результате этого пути, находящихся на устаревших, медленных или временно временно недоступных файловых системах. Это может предоставить публичные услуги потенциальным атакам
Под "корневой файловой системой" мы понимаем:
- В POSIX-совместимых системах файловая система установлена в корневой каталог (
/
).
%HOMEDRIVE%
, букву диска с двоеточием, содержащую текущую установку Windows (обычно, но необязательно C:
).
Не содержит нулевых байтов (т.е. \x00
в Python). Это сложное требование для всех файловых систем, совместимых с POSIX.
Не содержит компонентов пути длиной более 255 байт (например, 'a'*256
в Python). Компонент пути - это самая длинная подстрока пути, содержащая символ /
(например, bergtatt
, ind
, i
и fjeldkamrene
в пути /bergtatt/ind/i/fjeldkamrene
)).
Синтаксическая корректность. Корневая файловая система. Что это.
Вопрос №1: Как теперь мы будем делать допустимость имени пути?
Для путей, которые находятся в несуществующих каталогах, экземплярыFileNotFoundError
.
Для путей, которые находятся в существующих каталогах:
В Windows экземпляры WindowsError
, чей атрибут winerror
123
(т.е. ERROR_INVALID_NAME
).
Под всеми другими ОС:
Для путей, содержащих нулевые байты (т.е. '\x00'
), экземпляры TypeError
.
Для путей, содержащих компоненты пути длиной более 255 байт, экземпляры OSError
, чей атрибут errcode
:
В SunOS и в семействе ОС BSD errno.ERANGE
. (Кажется, это ошибка на уровне ОС, иначе называемая "выборочная интерпретация" стандарта POSIX.)
Под всеми другими ОС, errno.ENAMETOOLONG
.
Разделите это имя пути на компоненты пути (например, путь /troldskog/faren/vild
в список ['', 'troldskog', 'faren', 'vild']
).
Для каждого такого компонента:
Присоедините имя пути к каталогу, который, как предполагается, существует с этим компонентом, в новое временное имя пути (например, /troldskog
).
Передайте этот путь к os.stat()
или os.lstat()
. Если это имя пути и, следовательно, этот компонент недействительны, этот вызов гарантированно создает исключение, отображающее тип недействительности, а не общее исключение FileNotFoundError
. Зачем? Поскольку этот путь находится в существующем каталоге. (круговая логика является круговой.)
Забыли? Отлично.. (Предполагается Python 3. См. "Что такое хрупкая надежда для 300, ?" )
import errno, os
# Sadly, Python fails to provide the following magic number for us.
ERROR_INVALID_NAME = 123
'''
Windows-specific error code indicating an invalid pathname.
See Also
----------
https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx
Official listing of all such codes.
'''
def is_pathname_valid(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname for the current OS;
`False` otherwise.
'''
# If this pathname is either not a string or is but is empty, this pathname
# is invalid.
try:
if not isinstance(pathname, str) or not pathname:
return False
# Strip this pathname Windows-specific drive specifier (e.g., `C:\`)
# if any. Since Windows prohibits path components from containing `:`
# characters, failing to strip this `:`-suffixed prefix would
# erroneously invalidate all valid absolute Windows pathnames.
_, pathname = os.path.splitdrive(pathname)
# Directory guaranteed to exist. If the current OS is Windows, this is
# the drive to which Windows was installed (e.g., the "%HOMEDRIVE%"
# environment variable); else, the typical root directory.
root_dirname = os.environ.get('HOMEDRIVE', 'C:') \
if sys.platform == 'win32' else os.path.sep
assert os.path.isdir(root_dirname) # ...Murphy and her ironclad Law
# Append a path separator to this directory if needed.
root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep
# Test whether each path component split from this pathname is valid or
# not, ignoring non-existent and non-readable path components.
for pathname_part in pathname.split(os.path.sep):
try:
os.lstat(root_dirname + pathname_part)
# If an OS-specific exception is raised, its error code
# indicates whether this pathname is valid or not. Unless this
# is the case, this exception implies an ignorable kernel or
# filesystem complaint (e.g., path not found or inaccessible).
#
# Only the following exceptions indicate invalid pathnames:
#
# * Instances of the Windows-specific "WindowsError" class
# defining the "winerror" attribute whose value is
# "ERROR_INVALID_NAME". Under Windows, "winerror" is more
# fine-grained and hence useful than the generic "errno"
# attribute. When a too-long pathname is passed, for example,
# "errno" is "ENOENT" (i.e., no such file or directory) rather
# than "ENAMETOOLONG" (i.e., file name too long).
# * Instances of the cross-platform "OSError" class defining the
# generic "errno" attribute whose value is either:
# * Under most POSIX-compatible OSes, "ENAMETOOLONG".
# * Under some edge-case OSes (e.g., SunOS, *BSD), "ERANGE".
except OSError as exc:
if hasattr(exc, 'winerror'):
if exc.winerror == ERROR_INVALID_NAME:
return False
elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}:
return False
# If a "TypeError" exception was raised, it almost certainly has the
# error message "embedded NUL character" indicating an invalid pathname.
except TypeError as exc:
return False
# If no exception was raised, all path components and hence this
# pathname itself are valid. (Praise be to the curmudgeonly python.)
else:
return True
# If any other exception was raised, this is an unrelated fatal issue
# (e.g., a bug). Permit this exception to unwind the call stack.
#
# Did we mention this should be shipped with Python already?
Готово. Не жгите этого кода. (Он кусает.)
Вопрос №2: Возможно, неверное существование пути или творение, Eh?
def is_path_creatable(pathname: str) -> bool:
'''
`True` if the current user has sufficient permissions to create the passed
pathname; `False` otherwise.
'''
# Parent directory of the passed path. If empty, we substitute the current
# working directory (CWD) instead.
dirname = os.path.dirname(pathname) or os.getcwd()
return os.access(dirname, os.W_OK)
def is_path_exists_or_creatable(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname for the current OS _and_
either currently exists or is hypothetically creatable; `False` otherwise.
This function is guaranteed to _never_ raise exceptions.
'''
try:
# To prevent "os" module calls from raising undesirable exceptions on
# invalid pathnames, is_pathname_valid() is explicitly called first.
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_creatable(pathname))
# Report failure on non-fatal filesystem complaints (e.g., connection
# timeouts, permissions issues) implying this path to be inaccessible. All
# other exceptions are unrelated fatal issues and should not be caught here.
except OSError:
return False
Готово и сделано. За исключением не совсем.
Вопрос №3: Возможно, недопустимое существование пути или возможность записи в Windows
Существует оговорка. Конечно, есть.
import os, tempfile
def is_path_sibling_creatable(pathname: str) -> bool:
'''
`True` if the current user has sufficient permissions to create **siblings**
(i.e., arbitrary files in the parent directory) of the passed pathname;
`False` otherwise.
'''
# Parent directory of the passed path. If empty, we substitute the current
# working directory (CWD) instead.
dirname = os.path.dirname(pathname) or os.getcwd()
try:
# For safety, explicitly close and hence delete this temporary file
# immediately after creating it in the passed path parent directory.
with tempfile.TemporaryFile(dir=dirname): pass
return True
# While the exact type of exception raised by the above function depends on
# the current version of the Python interpreter, all such types subclass the
# following exception superclass.
except EnvironmentError:
return False
def is_path_exists_or_creatable_portable(pathname: str) -> bool:
'''
`True` if the passed pathname is a valid pathname on the current OS _and_
either currently exists or is hypothetically creatable in a cross-platform
manner optimized for POSIX-unfriendly filesystems; `False` otherwise.
This function is guaranteed to _never_ raise exceptions.
'''
try:
# To prevent "os" module calls from raising undesirable exceptions on
# invalid pathnames, is_pathname_valid() is explicitly called first.
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_sibling_creatable(pathname))
# Report failure on non-fatal filesystem complaints (e.g., connection
# timeouts, permissions issues) implying this path to be inaccessible. All
# other exceptions are unrelated fatal issues and should not be caught here.
except OSError:
return False
Обратите внимание, что даже этого может быть недостаточно.
Это сумасшествие. Это Windows.
Докажите это
Не так ли? Пришло время протестировать вышеуказанные тесты.
>>> print('"foo.bar" valid? ' + str(is_pathname_valid('foo.bar')))
"foo.bar" valid? True
>>> print('Null byte valid? ' + str(is_pathname_valid('\x00')))
Null byte valid? False
>>> print('Long path valid? ' + str(is_pathname_valid('a' * 256)))
Long path valid? False
>>> print('"/dev" exists or creatable? ' + str(is_path_exists_or_creatable('/dev')))
"/dev" exists or creatable? True
>>> print('"/dev/foo.bar" exists or creatable? ' + str(is_path_exists_or_creatable('/dev/foo.bar')))
"/dev/foo.bar" exists or creatable? False
>>> print('Null byte exists or creatable? ' + str(is_path_exists_or_creatable('\x00')))
Null byte exists or creatable? False
Вне здравомыслия. Помимо боли. Вы найдете проблемы с переносимостью Python.