tl; dr
Chame a is_path_exists_or_creatable()
função definida abaixo.
Estritamente Python 3. É assim que funcionamos.
Um conto de duas perguntas
A questão de "Como faço para testar a validade do nome do caminho e, para nomes de caminho válidos, a existência ou capacidade de escrita desses caminhos?" são claramente duas questões distintas. Ambos são interessantes, e nenhum recebeu uma resposta genuinamente satisfatória aqui ... ou, bem, em qualquer lugar que eu pudesse pesquisar.
a resposta de vikki provavelmente é a que mais chega, mas tem as desvantagens notáveis de:
- Abertura desnecessária ( ... e falha ao fechar de forma confiável ) identificadores de arquivo.
- Gravar desnecessariamente ( ... e depois não fechar ou excluir de forma confiável ) arquivos de 0 bytes.
- Ignorar erros específicos do SO, diferenciando entre nomes de caminho inválidos não ignoráveis e problemas de sistema de arquivos ignoráveis. Sem surpresa, isso é crítico no Windows. ( Veja abaixo. )
- Ignorar as condições de corrida resultantes de processos externos (re) movendo simultaneamente os diretórios pais do nome do caminho a ser testado. ( Veja abaixo. )
- Ignorando tempos limite de conexão resultantes deste nome de caminho residindo em sistemas de arquivos obsoletos, lentos ou temporariamente inacessíveis. Isso poderia expor os serviços voltados ao público a possíveis ataques dirigidos por DoS . ( Veja abaixo. )
Vamos consertar tudo isso.
Pergunta # 0: O que é a validade do nome do caminho novamente?
Antes de jogar nossos frágeis trajes de carne nos moshpits crivados de dor, provavelmente deveríamos definir o que queremos dizer com "validade do nome do caminho". O que define a validade, exatamente?
Por "validade de nome de caminho", queremos dizer a correção sintática de um nome de caminho com respeito ao sistema de arquivos raiz do sistema atual - independentemente de esse caminho ou seus diretórios pais existirem fisicamente. Um nome de caminho está sintaticamente correto sob esta definição se estiver em conformidade com todos os requisitos sintáticos do sistema de arquivos raiz.
Por "sistema de arquivos raiz", queremos dizer:
- Em sistemas compatíveis com POSIX, o sistema de arquivos é montado no diretório raiz (
/
).
- No Windows, o sistema de arquivos é montado na
%HOMEDRIVE%
letra da unidade com sufixo de dois pontos contendo a instalação atual do Windows (normalmente, mas não necessariamente C:
).
O significado de "correção sintática", por sua vez, depende do tipo de sistema de arquivos raiz. Para ext4
(e para a maioria, mas não todos os sistemas de arquivos compatíveis com POSIX), um nome de caminho está sintaticamente correto se e somente se esse nome de caminho:
- Não contém bytes nulos (ou seja,
\x00
em Python). Este é um requisito difícil para todos os sistemas de arquivos compatíveis com POSIX.
- Não contém componentes de caminho com mais de 255 bytes (por exemplo,
'a'*256
em Python). Um componente do caminho é uma subsequência mais longo de um caminho não contendo qualquer /
caracter (por exemplo, bergtatt
, ind
, i
, e fjeldkamrene
em nome do caminho /bergtatt/ind/i/fjeldkamrene
).
Correção sintática. Sistema de arquivos raiz. É isso aí.
Pergunta no. 1: Como agora devemos fazer a validade do nome do caminho?
Validar nomes de caminho em Python é surpreendentemente não intuitivo. Estou totalmente de acordo com o Fake Name aqui: o os.path
pacote oficial deve fornecer uma solução pronta para o uso para isso. Por razões desconhecidas (e provavelmente incomuns), isso não acontece. Felizmente, desenrolando sua própria solução ad-hoc não é que arrasador ...
OK, na verdade é. É cabeludo; é desagradável; provavelmente gargalha enquanto borbulha e ri enquanto brilha. Mas o que você vai fazer? Nuthin '.
Em breve desceremos ao abismo radioativo do código de baixo nível. Mas primeiro, vamos falar de uma loja de alto nível. O padrão os.stat()
e as os.lstat()
funções levantam as seguintes exceções quando passam nomes de caminho inválidos:
- Para nomes de caminhos que residem em diretórios não existentes, instâncias de
FileNotFoundError
.
- Para nomes de caminhos que residem em diretórios existentes:
- No Windows, as instâncias de
WindowsError
cujo winerror
atributo é 123
(ou seja, ERROR_INVALID_NAME
).
- Em todos os outros sistemas operacionais:
- Para nomes de caminho contendo bytes nulos (ou seja,
'\x00'
), instâncias de TypeError
.
- Para nomes de caminho contendo componentes de caminho com mais de 255 bytes, instâncias de
OSError
cujo errcode
atributo é:
- Sob SunOS e a família * BSD de sistemas operacionais
errno.ERANGE
,. (Isso parece ser um bug no nível do sistema operacional, também conhecido como "interpretação seletiva" do padrão POSIX.)
- Em todos os outros sistemas operacionais
errno.ENAMETOOLONG
,.
Crucialmente, isso implica que apenas nomes de caminhos residentes em diretórios existentes são validáveis. As funções os.stat()
e os.lstat()
levantam FileNotFoundError
exceções genéricas quando nomes de caminho passados residem em diretórios não existentes, independentemente de esses nomes de caminho serem inválidos ou não. A existência do diretório tem precedência sobre a invalidade do nome do caminho.
Isso significa que os nomes de caminhos que residem em diretórios não existentes não são validáveis? Sim - a menos que modifiquemos esses nomes de caminho para residir em diretórios existentes. No entanto, isso é ao menos viável com segurança? A modificação de um nome de caminho não deveria nos impedir de validar o nome do caminho original?
Para responder a esta pergunta, lembre-se de que os nomes de caminho sintaticamente corretos no ext4
sistema de arquivos não contêm componentes de caminho (A) contendo bytes nulos ou (B) com mais de 255 bytes de comprimento. Portanto, um ext4
nome de caminho é válido se e somente se todos os componentes do caminho nesse nome de caminho são válidos. Isso é verdade para a maioria dos sistemas de arquivos de interesse do mundo real .
Esse insight pedante realmente nos ajuda? Sim. Isso reduz o problema maior de validar o nome do caminho completo de uma só vez para o problema menor de apenas validar todos os componentes do caminho naquele nome de caminho. Qualquer nome de caminho arbitrário é validável (independentemente de esse nome de caminho residir em um diretório existente ou não) em uma plataforma cruzada, seguindo o seguinte algoritmo:
- Divida o nome do caminho em componentes do caminho (por exemplo, o nome do caminho
/troldskog/faren/vild
na lista ['', 'troldskog', 'faren', 'vild']
).
- Para cada um desses componentes:
- Junte-se ao nome do caminho de um diretório com existência garantida com aquele componente em um novo nome do caminho temporário (por exemplo,
/troldskog
).
- Passe esse caminho para
os.stat()
ou os.lstat()
. Se esse nome de caminho e, portanto, esse componente for inválido, essa chamada certamente levantará uma exceção expondo o tipo de invalidade em vez de uma FileNotFoundError
exceção genérica . Por quê? Porque esse nome de caminho reside em um diretório existente. (A lógica circular é circular.)
Existe um diretório garantido para existir? Sim, mas normalmente apenas um: o diretório superior do sistema de arquivos raiz (conforme definido acima).
Passar nomes de caminho que residam em qualquer outro diretório (e, portanto, não há garantia de existência) para os.stat()
ou os.lstat()
convida condições de corrida, mesmo se esse diretório tiver sido testado anteriormente. Por quê? Porque os processos externos não podem ser impedidos de remover simultaneamente aquele diretório depois que o teste foi executado, mas antes que o nome do caminho seja passado para os.stat()
ou os.lstat()
. Liberte os cães da loucura arrebatadora!
Também existe um benefício colateral substancial para a abordagem acima: segurança. (Não é que bom?) Especificamente:
Os aplicativos frontais que validam nomes de caminhos arbitrários de fontes não confiáveis simplesmente passando esses nomes de caminho para os.stat()
ou os.lstat()
são suscetíveis a ataques de negação de serviço (DoS) e outras travessuras de chapéu preto. Usuários mal-intencionados podem tentar validar repetidamente os nomes de caminho que residem em sistemas de arquivos conhecidos por serem obsoletos ou lentos (por exemplo, compartilhamentos NFS Samba); Nesse caso, a estatística de nomes de caminho de entrada cegamente pode falhar com o tempo limite de conexão ou consumir mais tempo e recursos do que sua capacidade insuficiente para suportar o desemprego.
A abordagem acima evita isso validando apenas os componentes do caminho de um nome de caminho em relação ao diretório raiz do sistema de arquivos raiz. (Se mesmo isso estiver desatualizado, lento ou inacessível, você terá problemas maiores do que a validação de nome de caminho.)
Perdido? Ótimo. Vamos começar. (Python 3 assumido. Consulte "What Is Fragile Hope for 300, leycec ?")
import errno, os
ERROR_INVALID_NAME = 123
'''
Windows-specific error code indicating an invalid pathname.
See Also
----------
https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
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.
'''
try:
if not isinstance(pathname, str) or not pathname:
return False
_, pathname = os.path.splitdrive(pathname)
root_dirname = os.environ.get('HOMEDRIVE', 'C:') \
if sys.platform == 'win32' else os.path.sep
assert os.path.isdir(root_dirname)
root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep
for pathname_part in pathname.split(os.path.sep):
try:
os.lstat(root_dirname + pathname_part)
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
except TypeError as exc:
return False
else:
return True
Feito. Não aperte os olhos para esse código. ( Morde. )
Pergunta # 2: Possivelmente existência de nome de caminho ou capacidade de criação inválida, hein?
Testar a existência ou capacidade de criação de nomes de caminhos possivelmente inválidos é, dada a solução acima, trivial. A pequena chave aqui é chamar a função definida anteriormente antes de testar o caminho aprovado:
def is_path_creatable(pathname: str) -> bool:
'''
`True` if the current user has sufficient permissions to create the passed
pathname; `False` otherwise.
'''
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:
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_creatable(pathname))
except OSError:
return False
Feito e feito. Exceto não exatamente.
Pergunta nº 3: Possibilidade de existência de nome de caminho inválido ou capacidade de gravação no Windows
Existe uma ressalva. Claro que sim.
Conforme admite a os.access()
documentação oficial :
Nota: As operações de E / S podem falhar mesmo quando os.access()
indica que elas teriam sucesso, particularmente para operações em sistemas de arquivos de rede que podem ter semântica de permissões além do modelo de bit de permissão POSIX usual.
Para surpresa de ninguém, o Windows é o suspeito de sempre aqui. Graças ao uso extensivo de Listas de Controle de Acesso (ACL) em sistemas de arquivos NTFS, o modelo simplista de bits de permissão POSIX mapeia mal para a realidade subjacente do Windows. Embora isso (indiscutivelmente) não seja culpa do Python, pode ser uma preocupação para aplicativos compatíveis com o Windows.
Se este for você, uma alternativa mais robusta é desejada. Se o caminho passado não existir, tentamos criar um arquivo temporário com garantia de exclusão imediata no diretório pai desse caminho - um teste de capacidade de criação mais portátil (se caro):
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.
'''
dirname = os.path.dirname(pathname) or os.getcwd()
try:
with tempfile.TemporaryFile(dir=dirname): pass
return True
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:
return is_pathname_valid(pathname) and (
os.path.exists(pathname) or is_path_sibling_creatable(pathname))
except OSError:
return False
Observe, entretanto, que mesmo isso pode não ser suficiente.
Graças ao User Access Control (UAC), o sempre inimitável Windows Vista e todas as suas iterações subsequentes mentem descaradamente sobre as permissões relativas aos diretórios do sistema. Quando usuários não administradores tentam criar arquivos em diretórios C:\Windows
ou canônicos C:\Windows\system32
, o UAC permite superficialmente que o usuário faça isso enquanto, na verdade, isola todos os arquivos criados em uma "Loja Virtual" no perfil do usuário. (Quem poderia imaginar que enganar os usuários teria consequências prejudiciais a longo prazo?)
Isso é loucura. Este é o Windows.
Prove
Ousamos? É hora de testar os testes acima.
Visto que NULL é o único caractere proibido em nomes de caminho em sistemas de arquivos orientados para UNIX, vamos aproveitar isso para demonstrar a verdade nua e crua - ignorando travessuras não ignoráveis do Windows, que francamente me aborrecem e irritam em igual medida:
>>> 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
Além da sanidade. Além da dor. Você encontrará questões de portabilidade do Python.