Send2Trash est un module fort pratique qui permet de déplacer des fichiers dans la corbeille, quel que soit le système d'exploitation.

Il fonctionne parfaitement sur GNU/Linux et macOS. Cependant, comme nous l'avons appris à nos dépends sur Nuxeo Drive, Windows pose problème quand le nom du fichier ou le chemin complet du fichier est plus long que 260 caractères. Le fichier en question n'est donc pas du tout supprimé et on se retrouve avec une exception WindowsError. Quelques message d'erreurs que j'ai pu rencontrés :

WindowsError: [Error 111] ??? (seems related to deep tree)
Cause: short paths are disabled on Windows

WindowsError: [Error 121] The source or destination path exceeded or would exceed MAX_PATH.
Cause: short paths are disabled on Windows

WindowsError: [Error 124] The path in the source or destination or both was invalid.
Cause: dealing with different drive

WindowsError: [Error 206] The filename or extension is too long.
Cause: even the full short path is too long

Le coeur du soucis est que Send2Trash utilise l'API SHFileOperation pour déplacer le fichier dans la corbeille. Cette API est vieille et ne supporte pas du tout les chemins longs.


Solution 1: IFileOperation

Windows nous redirige vers l'API IFileOperation, le successeur de SHFileOperation à partir de Windows Vista. Mais pas moyen de mettre la main sur un exemple qui tient la route en Python ctypes. J'ai bien tenté ma chance, mais en vain. Cela dit, la porte reste ouverte à toute proposition ☺


Solution 2 : être astucieux

Windows a une API, GetShortPathNameW, pour déterminer le nom court d'un nom de fichier. Par exemple, le fichier aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa sera compressé en AAAAAAA~ ; on gagne quelques caractères.

L'idée est de compresser le chemin complet du fichier en utilisant cette fonction. Rien d'extraordinaire, mais ça permettra d'outrepasser la plupart des cas bloquants.

Voici une fonction qui ferait ce job :

def get_short_path_name(long_name):
    if not long_name.startswith('\\\\?\\'):
        long_name = '\\\\?\\' + long_name
    buf_size = GetShortPathNameW(long_name, None, 0)
    output = create_unicode_buffer(buf_size)
    GetShortPathNameW(long_name, output, buf_size)
    return output.value[4:]  # Remove '\\?\' for SHFileOperationW

La fonction est simple, elle prend un chemin en entrée, ajoute le suffixe des noms longs \\?\, calcule le nom court et renvoie le nom court sans le suffixe des noms longs. Ouf !

Un exemple concrêt  :

import os.path


long_path = os.path.join(
    'C:',
    'a' * 42,
    'b' * 42,
    'c' * 42,
    'd' * 42,
    'e' * 42,
    'f' * 42,
    '0' * 100 + '.txt'
)
get_short_path_name(long_path)
C:\AAAAAAA~\BBBBBBB~\CCCCCCC~\DDDDDDD~\EEEEEEE~\FFFFFFF~\0000000~.txt

La longueur du fichier original est de 365. Elle passe à 69 une fois compressée.

Le code complet, dans son contexte, est consultable sur la PR du patch.

Quelques limitations cependant :

  1. Si le chemin final compressé fait plus de 260 caractères => WindowsError ;
  2. Pas moyen de supprimer un dossier long se trouvant sur une autre partition que celle du script Python.

Sources :