legacy.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. # Copyright 2017 Virgil Dupras
  2. # This software is licensed under the "BSD" License as described in the "LICENSE" file,
  3. # which should be included with this package. The terms are also available at
  4. # http://www.hardcoded.net/licenses/bsd_license
  5. from __future__ import unicode_literals
  6. import os.path as op
  7. from ctypes import (
  8. windll,
  9. Structure,
  10. byref,
  11. c_uint,
  12. create_unicode_buffer,
  13. addressof,
  14. GetLastError,
  15. FormatError,
  16. )
  17. from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL
  18. from send2trash.util import preprocess_paths
  19. kernel32 = windll.kernel32
  20. GetShortPathNameW = kernel32.GetShortPathNameW
  21. shell32 = windll.shell32
  22. SHFileOperationW = shell32.SHFileOperationW
  23. class SHFILEOPSTRUCTW(Structure):
  24. _fields_ = [
  25. ("hwnd", HWND),
  26. ("wFunc", UINT),
  27. ("pFrom", LPCWSTR),
  28. ("pTo", LPCWSTR),
  29. ("fFlags", c_uint),
  30. ("fAnyOperationsAborted", BOOL),
  31. ("hNameMappings", c_uint),
  32. ("lpszProgressTitle", LPCWSTR),
  33. ]
  34. FO_MOVE = 1
  35. FO_COPY = 2
  36. FO_DELETE = 3
  37. FO_RENAME = 4
  38. FOF_MULTIDESTFILES = 1
  39. FOF_SILENT = 4
  40. FOF_NOCONFIRMATION = 16
  41. FOF_ALLOWUNDO = 64
  42. FOF_NOERRORUI = 1024
  43. def convert_sh_file_opt_result(result):
  44. # map overlapping values from SHFileOpterationW to approximate standard windows errors
  45. # ref https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationw#return-value
  46. # ref https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
  47. results = {
  48. 0x71: 0x50, # DE_SAMEFILE -> ERROR_FILE_EXISTS
  49. 0x72: 0x57, # DE_MANYSRC1DEST -> ERROR_INVALID_PARAMETER
  50. 0x73: 0x57, # DE_DIFFDIR -> ERROR_INVALID_PARAMETER
  51. 0x74: 0x57, # DE_ROOTDIR -> ERROR_INVALID_PARAMETER
  52. 0x75: 0x4C7, # DE_OPCANCELLED -> ERROR_CANCELLED
  53. 0x76: 0x57, # DE_DESTSUBTREE -> ERROR_INVALID_PARAMETER
  54. 0x78: 0x05, # DE_ACCESSDENIEDSRC -> ERROR_ACCESS_DENIED
  55. 0x79: 0x6F, # DE_PATHTOODEEP -> ERROR_BUFFER_OVERFLOW
  56. 0x7A: 0x57, # DE_MANYDEST -> ERROR_INVALID_PARAMETER
  57. 0x7C: 0xA1, # DE_INVALIDFILES -> ERROR_BAD_PATHNAME
  58. 0x7D: 0x57, # DE_DESTSAMETREE -> ERROR_INVALID_PARAMETER
  59. 0x7E: 0xB7, # DE_FLDDESTISFILE -> ERROR_ALREADY_EXISTS
  60. 0x80: 0xB7, # DE_FILEDESTISFLD -> ERROR_ALREADY_EXISTS
  61. 0x81: 0x6F, # DE_FILENAMETOOLONG -> ERROR_BUFFER_OVERFLOW
  62. 0x82: 0x13, # DE_DEST_IS_CDROM -> ERROR_WRITE_PROTECT
  63. 0x83: 0x13, # DE_DEST_IS_DVD -> ERROR_WRITE_PROTECT
  64. 0x84: 0x6F9, # DE_DEST_IS_CDRECORD -> ERROR_UNRECOGNIZED_MEDIA
  65. 0x85: 0xDF, # DE_FILE_TOO_LARGE -> ERROR_FILE_TOO_LARGE
  66. 0x86: 0x13, # DE_SRC_IS_CDROM -> ERROR_WRITE_PROTECT
  67. 0x87: 0x13, # DE_SRC_IS_DVD -> ERROR_WRITE_PROTECT
  68. 0x88: 0x6F9, # DE_SRC_IS_CDRECORD -> ERROR_UNRECOGNIZED_MEDIA
  69. 0xB7: 0x6F, # DE_ERROR_MAX -> ERROR_BUFFER_OVERFLOW
  70. 0x402: 0xA1, # UNKNOWN -> ERROR_BAD_PATHNAME
  71. 0x10000: 0x1D, # ERRORONDEST -> ERROR_WRITE_FAULT
  72. 0x10074: 0x57, # DE_ROOTDIR | ERRORONDEST -> ERROR_INVALID_PARAMETER
  73. }
  74. return results.get(result, result)
  75. def prefix_and_path(path):
  76. r"""Guess the long-path prefix based on the kind of *path*.
  77. Local paths (C:\folder\file.ext) and UNC names (\\server\folder\file.ext)
  78. are handled.
  79. Return a tuple of the long-path prefix and the prefixed path.
  80. """
  81. prefix, long_path = "\\\\?\\", path
  82. if not path.startswith(prefix):
  83. if path.startswith("\\\\"):
  84. # Likely a UNC name
  85. prefix = "\\\\?\\UNC"
  86. long_path = prefix + path[1:]
  87. else:
  88. # Likely a local path
  89. long_path = prefix + path
  90. elif path.startswith(prefix + "UNC\\"):
  91. # UNC name with long-path prefix
  92. prefix = "\\\\?\\UNC"
  93. return prefix, long_path
  94. def get_awaited_path_from_prefix(prefix, path):
  95. """Guess the correct path to pass to the SHFileOperationW() call.
  96. The long-path prefix must be removed, so we should take care of
  97. different long-path prefixes.
  98. """
  99. if prefix == "\\\\?\\UNC":
  100. # We need to prepend a backslash for UNC names, as it was removed
  101. # in prefix_and_path().
  102. return "\\" + path[len(prefix) :]
  103. return path[len(prefix) :]
  104. def get_short_path_name(long_name):
  105. prefix, long_path = prefix_and_path(long_name)
  106. buf_size = GetShortPathNameW(long_path, None, 0)
  107. # FIX: https://github.com/hsoft/send2trash/issues/31
  108. # If buffer size is zero, an error has occurred.
  109. if not buf_size:
  110. err_no = GetLastError()
  111. raise WindowsError(err_no, FormatError(err_no), long_path)
  112. output = create_unicode_buffer(buf_size)
  113. GetShortPathNameW(long_path, output, buf_size)
  114. return get_awaited_path_from_prefix(prefix, output.value)
  115. def send2trash(paths):
  116. paths = preprocess_paths(paths)
  117. if not paths:
  118. return
  119. # convert data type
  120. paths = [str(path, "mbcs") if not isinstance(path, str) else path for path in paths]
  121. # convert to full paths
  122. paths = [op.abspath(path) if not op.isabs(path) else path for path in paths]
  123. # get short path to handle path length issues
  124. paths = [get_short_path_name(path) for path in paths]
  125. fileop = SHFILEOPSTRUCTW()
  126. fileop.hwnd = 0
  127. fileop.wFunc = FO_DELETE
  128. # FIX: https://github.com/hsoft/send2trash/issues/17
  129. # Starting in python 3.6.3 it is no longer possible to use:
  130. # LPCWSTR(path + '\0') directly as embedded null characters are no longer
  131. # allowed in strings
  132. # Workaround
  133. # - create buffer of c_wchar[] (LPCWSTR is based on this type)
  134. # - buffer is two c_wchar characters longer (double null terminator)
  135. # - cast the address of the buffer to a LPCWSTR
  136. # NOTE: based on how python allocates memory for these types they should
  137. # always be zero, if this is ever not true we can go back to explicitly
  138. # setting the last two characters to null using buffer[index] = '\0'.
  139. # Additional note on another issue here, unicode_buffer expects length in
  140. # bytes essentially, so having multi-byte characters causes issues if just
  141. # passing pythons string length. Instead of dealing with this difference we
  142. # just create a buffer then a new one with an extra null. Since the non-length
  143. # specified version apparently stops after the first null, join with a space first.
  144. buffer = create_unicode_buffer(" ".join(paths))
  145. # convert to a single string of null terminated paths
  146. path_string = "\0".join(paths)
  147. buffer = create_unicode_buffer(path_string, len(buffer) + 1)
  148. fileop.pFrom = LPCWSTR(addressof(buffer))
  149. fileop.pTo = None
  150. fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT
  151. fileop.fAnyOperationsAborted = 0
  152. fileop.hNameMappings = 0
  153. fileop.lpszProgressTitle = None
  154. result = SHFileOperationW(byref(fileop))
  155. if result:
  156. error = convert_sh_file_opt_result(result)
  157. raise WindowsError(None, FormatError(error), paths, error)