@tencent-ai/codebuddy-code 2.106.0 → 2.106.1-dev.4c12da5.202606131025
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/codebuddy-headless.js +309 -92
- package/dist/codebuddy.js +332 -115
- package/dist/web-ui/assets/index-1KNobXKH.js +859 -0
- package/dist/web-ui/index.html +1 -1
- package/dist/web-ui/sw.js +1 -1
- package/package.json +3 -2
- package/product.cloudhosted.json +2 -2
- package/product.internal.json +2 -2
- package/product.ioa.json +2 -2
- package/product.json +2 -2
- package/product.selfhosted.json +2 -2
- package/vendor/genie-trash/darwin-arm64 +0 -0
- package/vendor/genie-trash/darwin-x64 +0 -0
- package/vendor/genie-trash/linux-arm64 +0 -0
- package/vendor/genie-trash/linux-x64 +0 -0
- package/vendor/genie-trash/win32-x64.exe +0 -0
- package/vendor/shim/genie-safe-delete.cjs +589 -0
- package/vendor/shim/safe-bin/rm +18 -0
- package/vendor/shim/safe-bin/rmdir +18 -0
- package/vendor/shim/safe-bin/safe-delete-bash-env.sh +8 -0
- package/vendor/shim/safe-bin/safe-delete-common.sh +433 -0
- package/vendor/shim/safe-bin/unlink +18 -0
- package/vendor/shim/sitecustomize.py +471 -0
- package/dist/web-ui/assets/index-_HamWecR.js +0 -860
- package/lib/node/index.js +0 -30
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""
|
|
2
|
+
genie-safe-delete — Python 运行时安全删除 shim
|
|
3
|
+
|
|
4
|
+
通过 PYTHONPATH 中的 sitecustomize.py 自动加载,
|
|
5
|
+
拦截 os.remove / os.rmdir / shutil.rmtree / pathlib 的删除 API,
|
|
6
|
+
把文件移入操作系统的回收站而不是真删。
|
|
7
|
+
|
|
8
|
+
实现:优先调用 genie-trash native binary;不可用时回退到平台内联实现
|
|
9
|
+
(macOS: ctypes FSMoveObjectToTrashSync,Windows: ctypes SHFileOperationW,
|
|
10
|
+
Linux: freedesktop Trash spec 纯 Python 实现)。
|
|
11
|
+
|
|
12
|
+
设计要点见 apps/workbuddy-desktop/docs/safe-delete-runtime-trash.md
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import errno
|
|
16
|
+
import os
|
|
17
|
+
import stat
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# 触发条件
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
_SESSION_ID = (
|
|
25
|
+
os.environ.get("CODEBUDDY_SESSION_ID")
|
|
26
|
+
or os.environ.get("CLAUDE_SESSION_ID")
|
|
27
|
+
)
|
|
28
|
+
_IN_SANDBOX = os.environ.get("CODEBUDDY_SAFE_DELETE_SANDBOX") == "1"
|
|
29
|
+
|
|
30
|
+
if _SESSION_ID:
|
|
31
|
+
# __file__ 在 sitecustomize 加载时指向本文件路径,dirname 即 vendor/shim/
|
|
32
|
+
_SHIM_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
33
|
+
|
|
34
|
+
# sitecustomize 是 Python 保留文件名,Python 启动时会自动 import
|
|
35
|
+
# sys.path 上第一个匹配的模块。把 shim 目录前置到 PYTHONPATH 后,
|
|
36
|
+
# 用户环境原有的 sitecustomize(conda / virtualenv / 企业配置等)会被覆盖。
|
|
37
|
+
# 因此在注入 shim 前,先临时从 sys.modules 和 sys.path 中移除自身,
|
|
38
|
+
# 尝试加载原始的 sitecustomize,再恢复自身。
|
|
39
|
+
_self = sys.modules.pop("sitecustomize", None)
|
|
40
|
+
if _SHIM_DIR in sys.path:
|
|
41
|
+
sys.path.remove(_SHIM_DIR)
|
|
42
|
+
try:
|
|
43
|
+
__import__("sitecustomize")
|
|
44
|
+
except ImportError as e:
|
|
45
|
+
# 只有模块本身不存在时才静默(e.name == "sitecustomize");
|
|
46
|
+
# 若模块存在但内部导入失败(e.name 为其他名),输出 traceback 到 stderr
|
|
47
|
+
if e.name is not None and e.name != "sitecustomize":
|
|
48
|
+
import traceback
|
|
49
|
+
traceback.print_exc(file=sys.stderr)
|
|
50
|
+
if _SHIM_DIR not in sys.path:
|
|
51
|
+
sys.path.insert(0, _SHIM_DIR)
|
|
52
|
+
if _self is not None:
|
|
53
|
+
sys.modules["sitecustomize"] = _self
|
|
54
|
+
|
|
55
|
+
# -----------------------------------------------------------------------
|
|
56
|
+
# 平台内联 trash 实现(genie-trash binary 不可用时的降级路径)
|
|
57
|
+
# 仅使用标准库,无第三方依赖。
|
|
58
|
+
# -----------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
if sys.platform == "darwin":
|
|
61
|
+
# macOS: ctypes → CoreServices.FSMoveObjectToTrashSync
|
|
62
|
+
# API 在 macOS 10.8 标记 deprecated,但至今仍可用;不依赖 Finder,headless 安全。
|
|
63
|
+
from ctypes import cdll, byref, Structure, c_char, c_char_p
|
|
64
|
+
from ctypes.util import find_library as _find_library
|
|
65
|
+
|
|
66
|
+
_Foundation = cdll.LoadLibrary(_find_library("Foundation"))
|
|
67
|
+
_CoreServices = cdll.LoadLibrary(_find_library("CoreServices"))
|
|
68
|
+
_GetOSStatusComment = _Foundation.GetMacOSStatusCommentString
|
|
69
|
+
_GetOSStatusComment.restype = c_char_p
|
|
70
|
+
_FSPathMakeRefWithOptions = _CoreServices.FSPathMakeRefWithOptions
|
|
71
|
+
_FSMoveObjectToTrashSync = _CoreServices.FSMoveObjectToTrashSync
|
|
72
|
+
|
|
73
|
+
class _FSRef(Structure):
|
|
74
|
+
_fields_ = [("hidden", c_char * 80)]
|
|
75
|
+
|
|
76
|
+
def _platform_trash(abs_path):
|
|
77
|
+
path_b = abs_path.encode("utf-8") if isinstance(abs_path, str) else abs_path
|
|
78
|
+
fp = _FSRef()
|
|
79
|
+
rc = _FSPathMakeRefWithOptions(path_b, 0x01, byref(fp), None)
|
|
80
|
+
if rc:
|
|
81
|
+
raise OSError(_GetOSStatusComment(rc).decode("utf-8", errors="replace"))
|
|
82
|
+
rc = _FSMoveObjectToTrashSync(byref(fp), None, 0)
|
|
83
|
+
if rc:
|
|
84
|
+
raise OSError(_GetOSStatusComment(rc).decode("utf-8", errors="replace"))
|
|
85
|
+
|
|
86
|
+
elif sys.platform == "win32":
|
|
87
|
+
# Windows: ctypes → shell32.SHFileOperationW
|
|
88
|
+
# API 在 Vista 后标记 deprecated(推荐 IFileOperation),但仍广泛可用。
|
|
89
|
+
from ctypes import (
|
|
90
|
+
windll, Structure, byref, c_uint,
|
|
91
|
+
create_unicode_buffer, addressof,
|
|
92
|
+
GetLastError, FormatError,
|
|
93
|
+
)
|
|
94
|
+
from ctypes.wintypes import HWND, UINT, BOOL, LPCWSTR
|
|
95
|
+
|
|
96
|
+
_kernel32 = windll.kernel32
|
|
97
|
+
_shell32 = windll.shell32
|
|
98
|
+
_FO_DELETE = 3
|
|
99
|
+
_FOF_FLAGS = 64 | 16 | 1024 | 4 # ALLOWUNDO | NOCONFIRMATION | NOERRORUI | SILENT
|
|
100
|
+
|
|
101
|
+
class _SHFILEOPSTRUCTW(Structure):
|
|
102
|
+
_fields_ = [
|
|
103
|
+
("hwnd", HWND), ("wFunc", UINT), ("pFrom", LPCWSTR),
|
|
104
|
+
("pTo", LPCWSTR), ("fFlags", c_uint), ("fAnyOperationsAborted", BOOL),
|
|
105
|
+
("hNameMappings", c_uint), ("lpszProgressTitle", LPCWSTR),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
def _get_short_path(path):
|
|
109
|
+
"""调用 GetShortPathNameW 处理超长路径和特殊字符。"""
|
|
110
|
+
prefix = "\\\\?\\"
|
|
111
|
+
long_path = (prefix + "UNC" + path[1:]) if path.startswith("\\\\") else (prefix + path)
|
|
112
|
+
n = _kernel32.GetShortPathNameW(long_path, None, 0)
|
|
113
|
+
if not n:
|
|
114
|
+
err = GetLastError()
|
|
115
|
+
raise OSError(err, FormatError(err), path)
|
|
116
|
+
buf = create_unicode_buffer(n)
|
|
117
|
+
_kernel32.GetShortPathNameW(long_path, buf, n)
|
|
118
|
+
s = buf.value
|
|
119
|
+
# 还原 UNC 前缀:\\?\UNC\server → \\server
|
|
120
|
+
if s.startswith("\\\\?\\UNC"):
|
|
121
|
+
return "\\" + s[7:]
|
|
122
|
+
# 普通路径:去掉 \\?\
|
|
123
|
+
return s[4:] if s.startswith(prefix) else s
|
|
124
|
+
|
|
125
|
+
def _platform_trash(abs_path):
|
|
126
|
+
path = _get_short_path(os.path.abspath(abs_path))
|
|
127
|
+
# pFrom 需要双 null 终止的字符串(SHFileOperationW 规范)
|
|
128
|
+
buf = create_unicode_buffer(path, len(path) + 2)
|
|
129
|
+
fileop = _SHFILEOPSTRUCTW()
|
|
130
|
+
fileop.wFunc = _FO_DELETE
|
|
131
|
+
fileop.pFrom = LPCWSTR(addressof(buf))
|
|
132
|
+
fileop.fFlags = _FOF_FLAGS
|
|
133
|
+
result = _shell32.SHFileOperationW(byref(fileop))
|
|
134
|
+
if result:
|
|
135
|
+
raise OSError("SHFileOperationW 失败: 0x%x" % result)
|
|
136
|
+
|
|
137
|
+
else:
|
|
138
|
+
# Linux / 其他 POSIX: freedesktop Trash spec(XDG Base Directory)
|
|
139
|
+
from datetime import datetime as _datetime
|
|
140
|
+
import shutil as _shutil
|
|
141
|
+
|
|
142
|
+
def _trashinfo_encode(s):
|
|
143
|
+
"""仅编码非 ASCII 和控制字符,与 Node.js/Bash 版本保持一致。"""
|
|
144
|
+
parts = []
|
|
145
|
+
for ch in s:
|
|
146
|
+
if '\x20' <= ch <= '\x7e':
|
|
147
|
+
parts.append(ch)
|
|
148
|
+
else:
|
|
149
|
+
parts.append(''.join('%{:02X}'.format(b) for b in ch.encode('utf-8')))
|
|
150
|
+
return ''.join(parts)
|
|
151
|
+
|
|
152
|
+
_uid = os.getuid()
|
|
153
|
+
_xdg_data = os.fsencode(
|
|
154
|
+
os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
|
|
155
|
+
)
|
|
156
|
+
_home_trash = os.path.join(_xdg_data, b"Trash")
|
|
157
|
+
|
|
158
|
+
def _find_ext_volume_trash(volume_root):
|
|
159
|
+
"""freedesktop spec §1: 优先 .Trash/$uid(需 sticky bit),否则 .Trash-$uid。"""
|
|
160
|
+
uid_b = str(_uid).encode()
|
|
161
|
+
trash_base = os.path.join(volume_root, b".Trash")
|
|
162
|
+
if (
|
|
163
|
+
os.path.isdir(trash_base)
|
|
164
|
+
and not os.path.islink(trash_base)
|
|
165
|
+
and bool(os.lstat(trash_base).st_mode & 0o1000) # sticky bit
|
|
166
|
+
):
|
|
167
|
+
trash_dir = os.path.join(trash_base, uid_b)
|
|
168
|
+
os.makedirs(trash_dir, 0o700, exist_ok=True)
|
|
169
|
+
return trash_dir
|
|
170
|
+
trash_dir = os.path.join(volume_root, b".Trash-" + uid_b)
|
|
171
|
+
os.makedirs(trash_dir, 0o700, exist_ok=True)
|
|
172
|
+
return trash_dir
|
|
173
|
+
|
|
174
|
+
def _trash_move(path_b, dest_trash, topdir):
|
|
175
|
+
"""将文件移动到 dest_trash,并写入对应的 .trashinfo。"""
|
|
176
|
+
files_dir = os.path.join(dest_trash, b"files")
|
|
177
|
+
info_dir = os.path.join(dest_trash, b"info")
|
|
178
|
+
os.makedirs(files_dir, 0o700, exist_ok=True)
|
|
179
|
+
os.makedirs(info_dir, 0o700, exist_ok=True)
|
|
180
|
+
|
|
181
|
+
filename = os.path.basename(path_b)
|
|
182
|
+
base, ext = os.path.splitext(filename)
|
|
183
|
+
dest_name = filename
|
|
184
|
+
counter = 0
|
|
185
|
+
while os.path.exists(os.path.join(files_dir, dest_name)) or \
|
|
186
|
+
os.path.exists(os.path.join(info_dir, dest_name + b".trashinfo")):
|
|
187
|
+
counter += 1
|
|
188
|
+
dest_name = base + b"." + str(counter).encode() + ext
|
|
189
|
+
|
|
190
|
+
real_path = os.path.realpath(path_b)
|
|
191
|
+
real_top = os.path.realpath(topdir)
|
|
192
|
+
if real_path.startswith(real_top):
|
|
193
|
+
info_path = _trashinfo_encode(os.fsdecode(os.path.relpath(path_b, topdir)))
|
|
194
|
+
else:
|
|
195
|
+
info_path = _trashinfo_encode(os.fsdecode(os.path.abspath(path_b)))
|
|
196
|
+
|
|
197
|
+
info_content = "[Trash Info]\nPath=%s\nDeletionDate=%s\n" % (
|
|
198
|
+
info_path, _datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
|
199
|
+
)
|
|
200
|
+
info_file = os.path.join(info_dir, dest_name + b".trashinfo")
|
|
201
|
+
fd = os.open(info_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
202
|
+
try:
|
|
203
|
+
with os.fdopen(fd, "w") as f:
|
|
204
|
+
f.write(info_content)
|
|
205
|
+
except Exception:
|
|
206
|
+
try:
|
|
207
|
+
_orig_unlink(info_file)
|
|
208
|
+
except OSError:
|
|
209
|
+
pass
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
dest_file = os.path.join(files_dir, dest_name)
|
|
213
|
+
try:
|
|
214
|
+
os.rename(path_b, dest_file)
|
|
215
|
+
except OSError as e:
|
|
216
|
+
if e.errno == errno.EXDEV:
|
|
217
|
+
# _shutil.move 内部调用 shutil.rmtree,后者已被补丁为
|
|
218
|
+
# _safe_shutil_rmtree —— 这会重入 _try_trash 形成循环。
|
|
219
|
+
# 临时将 shutil.rmtree 恢复为原始版本后再调用。
|
|
220
|
+
_saved_rmtree = shutil.rmtree
|
|
221
|
+
shutil.rmtree = _orig_shutil_rmtree
|
|
222
|
+
try:
|
|
223
|
+
_shutil.move(os.fsdecode(path_b), os.fsdecode(dest_file))
|
|
224
|
+
except Exception:
|
|
225
|
+
try:
|
|
226
|
+
_orig_unlink(info_file)
|
|
227
|
+
except OSError:
|
|
228
|
+
pass
|
|
229
|
+
raise
|
|
230
|
+
finally:
|
|
231
|
+
shutil.rmtree = _saved_rmtree
|
|
232
|
+
else:
|
|
233
|
+
try:
|
|
234
|
+
_orig_unlink(info_file)
|
|
235
|
+
except OSError:
|
|
236
|
+
pass
|
|
237
|
+
raise
|
|
238
|
+
|
|
239
|
+
def _platform_trash(abs_path):
|
|
240
|
+
path_b = os.fsencode(abs_path)
|
|
241
|
+
path_dev = os.lstat(path_b).st_dev
|
|
242
|
+
home_dev = os.lstat(os.path.expanduser(b"~")).st_dev
|
|
243
|
+
if path_dev == home_dev:
|
|
244
|
+
_trash_move(path_b, _home_trash, _xdg_data)
|
|
245
|
+
else:
|
|
246
|
+
mount = os.path.realpath(path_b)
|
|
247
|
+
while not os.path.ismount(mount):
|
|
248
|
+
mount = os.path.split(mount)[0]
|
|
249
|
+
dest_trash = _find_ext_volume_trash(mount)
|
|
250
|
+
try:
|
|
251
|
+
_trash_move(path_b, dest_trash, mount)
|
|
252
|
+
except OSError as e:
|
|
253
|
+
if e.errno == errno.EXDEV:
|
|
254
|
+
_trash_move(path_b, _home_trash, _xdg_data)
|
|
255
|
+
else:
|
|
256
|
+
raise
|
|
257
|
+
|
|
258
|
+
# -----------------------------------------------------------------------
|
|
259
|
+
# 全局状态
|
|
260
|
+
# -----------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
# 保存原始引用(patch 前)
|
|
263
|
+
import shutil
|
|
264
|
+
import subprocess
|
|
265
|
+
import pathlib
|
|
266
|
+
import platform
|
|
267
|
+
|
|
268
|
+
_orig_remove = os.remove
|
|
269
|
+
_orig_unlink = os.unlink
|
|
270
|
+
_orig_rmdir = os.rmdir
|
|
271
|
+
_orig_shutil_rmtree = shutil.rmtree
|
|
272
|
+
_orig_path_unlink = pathlib.Path.unlink
|
|
273
|
+
_orig_path_rmdir = pathlib.Path.rmdir
|
|
274
|
+
|
|
275
|
+
# -----------------------------------------------------------------------
|
|
276
|
+
# genie-trash native binary 支持(优先路径,不可用自动降级)
|
|
277
|
+
# 优先使用 GENIE_TRASH_DIR 环境变量(由 buildSafeDeleteEnv() 注入),
|
|
278
|
+
# 解决打包后 extraResources 与 shim 不共父目录的路径错位问题。
|
|
279
|
+
# -----------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
def _get_trash_bin():
|
|
282
|
+
system = "win32" if sys.platform == "win32" else platform.system().lower()
|
|
283
|
+
machine = platform.machine().lower()
|
|
284
|
+
if machine in ("arm64", "aarch64"):
|
|
285
|
+
arch = "arm64"
|
|
286
|
+
elif machine in ("x86_64", "amd64"):
|
|
287
|
+
arch = "x64"
|
|
288
|
+
else:
|
|
289
|
+
arch = machine
|
|
290
|
+
ext = ".exe" if sys.platform == "win32" else ""
|
|
291
|
+
file_name = "%s-%s%s" % (system, arch, ext)
|
|
292
|
+
trash_dir = os.environ.get("GENIE_TRASH_DIR")
|
|
293
|
+
if trash_dir:
|
|
294
|
+
return os.path.join(trash_dir, file_name)
|
|
295
|
+
shim_dir = os.path.dirname(os.path.abspath(__file__))
|
|
296
|
+
return os.path.join(shim_dir, "..", "genie-trash", file_name)
|
|
297
|
+
|
|
298
|
+
_trash_bin_available = None # None=未探测, True=可用, False=不可用
|
|
299
|
+
|
|
300
|
+
def _try_trash_via_binary(abs_path):
|
|
301
|
+
global _trash_bin_available
|
|
302
|
+
if _trash_bin_available is False:
|
|
303
|
+
return False
|
|
304
|
+
try:
|
|
305
|
+
result = subprocess.run(
|
|
306
|
+
[_get_trash_bin(), abs_path],
|
|
307
|
+
capture_output=True, text=True, timeout=5
|
|
308
|
+
)
|
|
309
|
+
if result.returncode == 0:
|
|
310
|
+
_trash_bin_available = True
|
|
311
|
+
return True
|
|
312
|
+
# binary 已启动但对该路径失败:抛错,不降级真删
|
|
313
|
+
raise OSError(
|
|
314
|
+
"[safe-delete] 操作失败: %s"
|
|
315
|
+
% (result.stderr.strip() or "exit %d" % result.returncode)
|
|
316
|
+
)
|
|
317
|
+
except FileNotFoundError:
|
|
318
|
+
_trash_bin_available = False # binary 不存在,进程生命周期内不会凭空出现
|
|
319
|
+
return False
|
|
320
|
+
except PermissionError:
|
|
321
|
+
# 不缓存:权限问题可能是临时状态(如 chmod +x 后恢复)
|
|
322
|
+
return False
|
|
323
|
+
except subprocess.TimeoutExpired:
|
|
324
|
+
return False # 超时是临时问题,不永久禁用 native binary
|
|
325
|
+
except OSError:
|
|
326
|
+
raise
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
_trash_bin_available = False
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
# -----------------------------------------------------------------------
|
|
333
|
+
# 核心:尝试移入回收站。优先 binary,降级到平台内联实现。
|
|
334
|
+
# 返回 True=已移入回收站;抛错=fail-closed,调用方不得真删。
|
|
335
|
+
# -----------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
def _try_trash(abs_path):
|
|
338
|
+
if sys.platform == "win32" and _IN_SANDBOX:
|
|
339
|
+
message = (
|
|
340
|
+
"[safe-delete][SAFE_DELETE_FAIL_CLOSED] "
|
|
341
|
+
"target=%s reason=windows-sandbox-recycle-bin-unavailable" % abs_path
|
|
342
|
+
)
|
|
343
|
+
try:
|
|
344
|
+
sys.stderr.write(message + "\n")
|
|
345
|
+
sys.stderr.flush()
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
raise OSError(message)
|
|
349
|
+
if _try_trash_via_binary(abs_path):
|
|
350
|
+
return True
|
|
351
|
+
_platform_trash(abs_path)
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
def _safe_lstat(abs_path):
|
|
355
|
+
try:
|
|
356
|
+
return os.lstat(abs_path)
|
|
357
|
+
except OSError as e:
|
|
358
|
+
if e.errno == errno.ENOENT:
|
|
359
|
+
return None
|
|
360
|
+
# EACCES / ELOOP 等错误:让调用方回退原生
|
|
361
|
+
return False # 与 None 区分开
|
|
362
|
+
|
|
363
|
+
def _is_dir_empty(abs_path):
|
|
364
|
+
try:
|
|
365
|
+
return len(os.listdir(abs_path)) == 0
|
|
366
|
+
except OSError:
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
# -----------------------------------------------------------------------
|
|
370
|
+
# 包装:os.remove / os.unlink (文件,不能是目录)
|
|
371
|
+
# -----------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
def _safe_remove(path, *, dir_fd=None):
|
|
374
|
+
# dir_fd 不为 None → 走原生(无法解析到 abs_path)
|
|
375
|
+
if dir_fd is not None:
|
|
376
|
+
return _orig_remove(path, dir_fd=dir_fd)
|
|
377
|
+
abs_path = os.path.abspath(os.fspath(path))
|
|
378
|
+
st = _safe_lstat(abs_path)
|
|
379
|
+
if not st:
|
|
380
|
+
return _orig_remove(path) # ENOENT / stat 错误 → 让原生抛
|
|
381
|
+
if stat.S_ISDIR(st.st_mode):
|
|
382
|
+
return _orig_remove(path) # 让原生抛 EISDIR / EPERM
|
|
383
|
+
_try_trash(abs_path)
|
|
384
|
+
|
|
385
|
+
# -----------------------------------------------------------------------
|
|
386
|
+
# 包装:os.rmdir(必须是空目录)
|
|
387
|
+
# -----------------------------------------------------------------------
|
|
388
|
+
|
|
389
|
+
def _safe_rmdir(path, *, dir_fd=None):
|
|
390
|
+
if dir_fd is not None:
|
|
391
|
+
return _orig_rmdir(path, dir_fd=dir_fd)
|
|
392
|
+
abs_path = os.path.abspath(os.fspath(path))
|
|
393
|
+
st = _safe_lstat(abs_path)
|
|
394
|
+
if not st:
|
|
395
|
+
return _orig_rmdir(path) # ENOENT / stat 错误
|
|
396
|
+
if not stat.S_ISDIR(st.st_mode):
|
|
397
|
+
return _orig_rmdir(path) # ENOTDIR
|
|
398
|
+
if not _is_dir_empty(abs_path):
|
|
399
|
+
return _orig_rmdir(path) # ENOTEMPTY
|
|
400
|
+
_try_trash(abs_path)
|
|
401
|
+
|
|
402
|
+
# -----------------------------------------------------------------------
|
|
403
|
+
# 包装:shutil.rmtree(递归删目录)
|
|
404
|
+
# -----------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
def _safe_shutil_rmtree(path, ignore_errors=False, onerror=None, **kwargs):
|
|
407
|
+
# Python 3.12+ 引入 onexc,3.14+ 还有 dir_fd——一并 forward 给原生
|
|
408
|
+
try:
|
|
409
|
+
abs_path = os.path.abspath(os.fspath(path))
|
|
410
|
+
except TypeError:
|
|
411
|
+
return _orig_shutil_rmtree(
|
|
412
|
+
path, ignore_errors=ignore_errors, onerror=onerror, **kwargs
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
st = _safe_lstat(abs_path)
|
|
416
|
+
if not st:
|
|
417
|
+
return _orig_shutil_rmtree(
|
|
418
|
+
path, ignore_errors=ignore_errors, onerror=onerror, **kwargs
|
|
419
|
+
)
|
|
420
|
+
if not stat.S_ISDIR(st.st_mode):
|
|
421
|
+
# 不是目录 → 让原生处理(rmtree 会抛 NotADirectoryError)
|
|
422
|
+
return _orig_shutil_rmtree(
|
|
423
|
+
path, ignore_errors=ignore_errors, onerror=onerror, **kwargs
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
_try_trash(abs_path)
|
|
428
|
+
except Exception as e:
|
|
429
|
+
if ignore_errors:
|
|
430
|
+
return
|
|
431
|
+
if onerror is not None:
|
|
432
|
+
onerror(_safe_shutil_rmtree, path, sys.exc_info())
|
|
433
|
+
return
|
|
434
|
+
raise
|
|
435
|
+
|
|
436
|
+
# -----------------------------------------------------------------------
|
|
437
|
+
# 包装:pathlib.Path.unlink / rmdir
|
|
438
|
+
# -----------------------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
def _safe_path_unlink(self, missing_ok=False):
|
|
441
|
+
abs_path = os.path.abspath(str(self))
|
|
442
|
+
st = _safe_lstat(abs_path)
|
|
443
|
+
if not st:
|
|
444
|
+
if st is None and missing_ok:
|
|
445
|
+
return None
|
|
446
|
+
return _orig_path_unlink(self, missing_ok=missing_ok) # 让原生抛
|
|
447
|
+
if stat.S_ISDIR(st.st_mode):
|
|
448
|
+
return _orig_path_unlink(self, missing_ok=missing_ok) # 抛 IsADirectoryError
|
|
449
|
+
_try_trash(abs_path)
|
|
450
|
+
|
|
451
|
+
def _safe_path_rmdir(self):
|
|
452
|
+
abs_path = os.path.abspath(str(self))
|
|
453
|
+
st = _safe_lstat(abs_path)
|
|
454
|
+
if not st:
|
|
455
|
+
return _orig_path_rmdir(self) # ENOENT / stat 错误 → 让原生抛
|
|
456
|
+
if not stat.S_ISDIR(st.st_mode):
|
|
457
|
+
return _orig_path_rmdir(self) # 抛 NotADirectoryError
|
|
458
|
+
if not _is_dir_empty(abs_path):
|
|
459
|
+
return _orig_path_rmdir(self) # 抛 OSError(ENOTEMPTY)
|
|
460
|
+
_try_trash(abs_path)
|
|
461
|
+
|
|
462
|
+
# -----------------------------------------------------------------------
|
|
463
|
+
# 安装 patch
|
|
464
|
+
# -----------------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
os.remove = _safe_remove
|
|
467
|
+
os.unlink = _safe_remove # CPython 里 os.unlink is os.remove,显式 patch 兼容其它实现
|
|
468
|
+
os.rmdir = _safe_rmdir
|
|
469
|
+
shutil.rmtree = _safe_shutil_rmtree
|
|
470
|
+
pathlib.Path.unlink = _safe_path_unlink
|
|
471
|
+
pathlib.Path.rmdir = _safe_path_rmdir
|