pykitool 0.0.1__py3-none-any.whl
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.
- pykitool/__init__.py +0 -0
- pykitool/base/__init__.py +0 -0
- pykitool/base/cache.py +352 -0
- pykitool/base/enums.py +102 -0
- pykitool/base/exception.py +6 -0
- pykitool/base/response.py +82 -0
- pykitool/base/tlog.py +230 -0
- pykitool/sqliter/__init__.py +0 -0
- pykitool/sqliter/exception.py +30 -0
- pykitool/sqliter/middleware.py +84 -0
- pykitool/sqliter/plus.py +125 -0
- pykitool/sqliter/repo.py +188 -0
- pykitool/utils/__init__.py +0 -0
- pykitool/utils/cbfile.py +697 -0
- pykitool/utils/cbrequest.py +473 -0
- pykitool/utils/cbruntime.py +870 -0
- pykitool/utils/cbutils.py +518 -0
- pykitool-0.0.1.dist-info/METADATA +36 -0
- pykitool-0.0.1.dist-info/RECORD +21 -0
- pykitool-0.0.1.dist-info/WHEEL +5 -0
- pykitool-0.0.1.dist-info/top_level.txt +1 -0
pykitool/utils/cbfile.py
ADDED
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
import hashlib
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import time
|
|
10
|
+
import zipfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, BinaryIO, Dict, List, Optional, Tuple, Union
|
|
13
|
+
|
|
14
|
+
from loguru import logger
|
|
15
|
+
|
|
16
|
+
# ================================ 路径处理 ================================
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# 统一转换路径为 POSIX 格式
|
|
20
|
+
def ap(path: Optional[str]) -> str:
|
|
21
|
+
if path:
|
|
22
|
+
if path.startswith("http://") or path.startswith("https://"):
|
|
23
|
+
return path
|
|
24
|
+
else:
|
|
25
|
+
return Path(path).as_posix()
|
|
26
|
+
return ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# 获取文件名(完整)
|
|
30
|
+
def fullname(path: str) -> str:
|
|
31
|
+
return os.path.basename(path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# 获取文件名称
|
|
35
|
+
def filename(path: str) -> str:
|
|
36
|
+
_, filename, _ = name_and_format(path)
|
|
37
|
+
return filename
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# 获取文件后缀
|
|
41
|
+
def fileformat(path: str) -> str:
|
|
42
|
+
_, _, fileformat = name_and_format(path)
|
|
43
|
+
return fileformat
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# 获取文件名和后缀(返回完整文件名、文件名、扩展名)
|
|
47
|
+
def name_and_format(path: str) -> Tuple[str, str, str]:
|
|
48
|
+
path = ap(path)
|
|
49
|
+
fullname = os.path.basename(path)
|
|
50
|
+
filename, fileformat = os.path.splitext(fullname)
|
|
51
|
+
return fullname, filename, fileformat
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ================================ 文件读写 ================================
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# 读取文件内容(支持单个路径或多个路径)
|
|
58
|
+
def read(paths: Union[str, bytes, List[str], Tuple[str, ...]]) -> Optional[str]:
|
|
59
|
+
if isinstance(paths, (str, bytes)):
|
|
60
|
+
paths = [paths]
|
|
61
|
+
elif not isinstance(paths, (list, tuple)):
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
content_list: List[str] = []
|
|
65
|
+
for path in paths:
|
|
66
|
+
try:
|
|
67
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
68
|
+
content = file.read()
|
|
69
|
+
content_list.append(content)
|
|
70
|
+
logger.trace(f"Successfully read file: {path}")
|
|
71
|
+
except FileNotFoundError:
|
|
72
|
+
logger.error(f"File not found: {path}")
|
|
73
|
+
except PermissionError:
|
|
74
|
+
logger.error(f"Permission denied: {path}")
|
|
75
|
+
except IOError as e:
|
|
76
|
+
logger.error(f"IO error reading file {path}: {str(e)}")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"Error reading file {path}: {str(e)}")
|
|
79
|
+
|
|
80
|
+
if content_list:
|
|
81
|
+
return "\n\n".join(content_list)
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# 写入文件内容
|
|
86
|
+
def write(path: Union[str, Path], data: str) -> Optional[str]:
|
|
87
|
+
try:
|
|
88
|
+
path_str = str(path)
|
|
89
|
+
with open(path_str, "w", encoding="utf-8") as file:
|
|
90
|
+
file.write(data)
|
|
91
|
+
return path_str
|
|
92
|
+
except FileNotFoundError:
|
|
93
|
+
logger.error(f"File not found: {path}")
|
|
94
|
+
except PermissionError:
|
|
95
|
+
logger.error(f"Permission denied: {path}")
|
|
96
|
+
except IOError as e:
|
|
97
|
+
logger.error(f"IO error writing to file {path}: {str(e)}")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Error writing to file {path}: {str(e)}")
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ================================ 文件操作 ================================
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# 拷贝文件到目标位置
|
|
107
|
+
def cp(source_file: Union[str, Path], target: Union[str, Path]) -> Optional[str]:
|
|
108
|
+
source_file = str(source_file)
|
|
109
|
+
target = str(target)
|
|
110
|
+
file_name = os.path.basename(source_file)
|
|
111
|
+
|
|
112
|
+
if os.path.isdir(target):
|
|
113
|
+
target_file = os.path.join(target, file_name)
|
|
114
|
+
else:
|
|
115
|
+
target_file = target
|
|
116
|
+
|
|
117
|
+
target_dir = os.path.dirname(target_file)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
mk_folder(target_dir)
|
|
121
|
+
shutil.copy2(source_file, target_file)
|
|
122
|
+
logger.debug(f"File {source_file} copied to: {target_file}")
|
|
123
|
+
return target_file
|
|
124
|
+
except FileNotFoundError:
|
|
125
|
+
logger.error(f"Source file not found: {source_file}")
|
|
126
|
+
except PermissionError:
|
|
127
|
+
logger.error(f"Permission denied copying to: {target_file}")
|
|
128
|
+
except shutil.SameFileError:
|
|
129
|
+
logger.error(f"Source and target are the same file: {source_file}")
|
|
130
|
+
except OSError as e:
|
|
131
|
+
logger.error(f"OS error copying file: {str(e)}")
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error(f"Error copying file: {str(e)}")
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# 移动文件到目标位置
|
|
138
|
+
def mv(source_file: Union[str, Path], target: Union[str, Path]) -> Optional[str]:
|
|
139
|
+
source_file = str(source_file)
|
|
140
|
+
target = str(target)
|
|
141
|
+
file_name = os.path.basename(source_file)
|
|
142
|
+
|
|
143
|
+
if os.path.isdir(target):
|
|
144
|
+
target_file = os.path.join(target, file_name)
|
|
145
|
+
else:
|
|
146
|
+
target_file = target
|
|
147
|
+
|
|
148
|
+
target_dir = os.path.dirname(target_file)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
mk_folder(target_dir)
|
|
152
|
+
shutil.move(source_file, target_file)
|
|
153
|
+
logger.debug(f"File {source_file} moved to: {target_file}")
|
|
154
|
+
return target_file
|
|
155
|
+
except FileNotFoundError:
|
|
156
|
+
logger.error(f"Source file not found: {source_file}")
|
|
157
|
+
except PermissionError:
|
|
158
|
+
logger.error(f"Permission denied moving to: {target_file}")
|
|
159
|
+
except shutil.SameFileError:
|
|
160
|
+
logger.error(f"Source and target are the same file: {source_file}")
|
|
161
|
+
except OSError as e:
|
|
162
|
+
logger.error(f"OS error moving file: {str(e)}")
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.error(f"Error moving file: {str(e)}")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# 拷贝文件夹到目标位置
|
|
169
|
+
def cp_folder(source_folder: Union[str, Path], target_folder: Union[str, Path]) -> None:
|
|
170
|
+
try:
|
|
171
|
+
shutil.copytree(str(source_folder), str(target_folder), dirs_exist_ok=True)
|
|
172
|
+
logger.debug(f"Folder {source_folder} copied to: {target_folder}")
|
|
173
|
+
except shutil.Error as e:
|
|
174
|
+
logger.error(f"Error copying folder: {str(e)}")
|
|
175
|
+
raise
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# 新建文件夹(可选清空已有内容)
|
|
179
|
+
def mk_folder(path: Union[str, Path], is_clean: bool = False, exist_ok: bool = True) -> str:
|
|
180
|
+
output_path = Path(path)
|
|
181
|
+
if is_clean:
|
|
182
|
+
rm_folder(str(path))
|
|
183
|
+
output_path.mkdir(parents=True, exist_ok=exist_ok)
|
|
184
|
+
return str(output_path)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# 安全删除文件
|
|
188
|
+
def rm(path: Optional[str]) -> None:
|
|
189
|
+
try:
|
|
190
|
+
if not path:
|
|
191
|
+
logger.error("Invalid path: None or empty string")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# 防止删除系统根目录
|
|
195
|
+
unsafe_paths = ["/", "C:\\", "C:/", os.path.expanduser("~")]
|
|
196
|
+
if path in unsafe_paths:
|
|
197
|
+
logger.error("Unsafe delete attempt blocked: {}", path)
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if os.path.exists(path):
|
|
201
|
+
os.remove(path)
|
|
202
|
+
logger.trace("Successfully deleted file: {}", path)
|
|
203
|
+
else:
|
|
204
|
+
logger.warning("File does not exist: {}", path)
|
|
205
|
+
except PermissionError:
|
|
206
|
+
logger.error(f"Permission denied deleting file: {path}")
|
|
207
|
+
except OSError as e:
|
|
208
|
+
logger.error(f"OS error deleting file {path}: {str(e)}")
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Error deleting file {path}: {str(e)}")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# 安全删除文件夹
|
|
214
|
+
def rm_folder(path: Optional[str]) -> None:
|
|
215
|
+
folder_path: Optional[str] = None
|
|
216
|
+
try:
|
|
217
|
+
if not path:
|
|
218
|
+
logger.error("Invalid path: None or empty string")
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
path = os.path.abspath(path)
|
|
222
|
+
|
|
223
|
+
if os.path.isfile(path):
|
|
224
|
+
folder_path = os.path.dirname(path)
|
|
225
|
+
elif os.path.isdir(path):
|
|
226
|
+
folder_path = path
|
|
227
|
+
else:
|
|
228
|
+
logger.warning("Path does not exist: {}", path)
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
# 防止删除系统根目录
|
|
232
|
+
unsafe_paths = ["/", "C:\\", "C:/", os.path.expanduser("~")]
|
|
233
|
+
if folder_path in unsafe_paths:
|
|
234
|
+
logger.error("Unsafe delete attempt blocked: {}", folder_path)
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
if os.path.exists(folder_path):
|
|
238
|
+
shutil.rmtree(folder_path)
|
|
239
|
+
logger.debug("Successfully deleted folder: {}", folder_path)
|
|
240
|
+
else:
|
|
241
|
+
logger.warning("Folder does not exist: {}", folder_path)
|
|
242
|
+
except PermissionError:
|
|
243
|
+
logger.error(f"Permission denied deleting folder: {folder_path}")
|
|
244
|
+
except OSError as e:
|
|
245
|
+
logger.error(f"OS error deleting folder {folder_path}: {str(e)}")
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.error(f"Error deleting folder {folder_path}: {str(e)}")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# 覆盖文件(可选备份原文件)
|
|
251
|
+
def overwrite(source: Union[str, Path], target: Union[str, Path], backup: bool = True) -> None:
|
|
252
|
+
source = str(source)
|
|
253
|
+
target = str(target)
|
|
254
|
+
try:
|
|
255
|
+
if not os.path.exists(source):
|
|
256
|
+
raise FileNotFoundError(f"Source file does not exist: {source}")
|
|
257
|
+
if backup and os.path.exists(target):
|
|
258
|
+
name_part, ext = os.path.splitext(target)
|
|
259
|
+
timestamp = time.strftime("%Y%m%d%H%M%S")
|
|
260
|
+
shutil.move(target, f"{name_part}_{timestamp}{ext}")
|
|
261
|
+
shutil.move(source, target)
|
|
262
|
+
logger.debug(f"File overwritten: {target}")
|
|
263
|
+
except FileNotFoundError as e:
|
|
264
|
+
raise FileNotFoundError(f"Overwrite failed: {str(e)}") from e
|
|
265
|
+
except PermissionError as e:
|
|
266
|
+
raise PermissionError(f"Permission denied: {str(e)}") from e
|
|
267
|
+
except Exception as e:
|
|
268
|
+
raise RuntimeError(f"Failed to overwrite {target} with {source}: {str(e)}") from e
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ================================ 文件信息 ================================
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# 计算文件 MD5 值(优化块大小为 64KB 提升大文件性能)
|
|
275
|
+
def md5(file_obj: BinaryIO) -> str:
|
|
276
|
+
md5_hash = hashlib.md5()
|
|
277
|
+
# 使用 64KB 块大小,比 4KB 更适合大文件
|
|
278
|
+
for chunk in iter(lambda: file_obj.read(65536), b""):
|
|
279
|
+
md5_hash.update(chunk)
|
|
280
|
+
return md5_hash.hexdigest()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# 根据文件路径计算 MD5 值
|
|
284
|
+
def md5_file(path: Union[str, Path]) -> str:
|
|
285
|
+
with open(str(path), "rb") as f:
|
|
286
|
+
return md5(f)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# 获取文件大小(单位:MB)
|
|
290
|
+
def get_file_size_MB(file_path: Union[str, Path]) -> float:
|
|
291
|
+
return os.path.getsize(str(file_path)) / (1024 * 1024)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# 验证路径是否有效且存在
|
|
295
|
+
def is_valid_path(p: Any) -> bool:
|
|
296
|
+
return isinstance(p, (str, Path)) and str(p).strip() != "" and Path(p).exists()
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# 验证文件夹是否包含有效图片
|
|
300
|
+
def is_valid_image_folder(path: Union[str, Path], extensions: Tuple[str, ...] = (".png", ".jpg", ".jpeg")) -> bool:
|
|
301
|
+
p = Path(path)
|
|
302
|
+
if not p.exists() or not p.is_dir():
|
|
303
|
+
return False
|
|
304
|
+
images = list(p.glob("*"))
|
|
305
|
+
return any(img.suffix.lower() in extensions for img in images)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# 清理文件名中的非法字符
|
|
309
|
+
def clean_filename(text: Optional[str]) -> str:
|
|
310
|
+
if not text:
|
|
311
|
+
return "untitled"
|
|
312
|
+
text = re.sub(r'[\\/:*?"<>|]', "_", text.strip())
|
|
313
|
+
text = text.strip(".")
|
|
314
|
+
if not text:
|
|
315
|
+
return "untitled"
|
|
316
|
+
return text
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# ================================ 符号链接 ================================
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# 创建符号链接
|
|
323
|
+
def symlink(src: Union[str, Path], dest: Union[str, Path]) -> None:
|
|
324
|
+
src = os.path.abspath(str(src))
|
|
325
|
+
dest = os.path.abspath(str(dest))
|
|
326
|
+
system = platform.system()
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
if os.path.islink(dest):
|
|
330
|
+
current_target = os.readlink(dest)
|
|
331
|
+
if os.path.abspath(current_target) != src:
|
|
332
|
+
os.unlink(dest)
|
|
333
|
+
_create_symlink(src, dest, system)
|
|
334
|
+
logger.info(f"[Symlink overwritten] {dest} -> {src}")
|
|
335
|
+
else:
|
|
336
|
+
logger.info(f"[Symlink ok] {dest} -> {src}")
|
|
337
|
+
else:
|
|
338
|
+
if os.path.exists(dest):
|
|
339
|
+
if os.path.isdir(dest):
|
|
340
|
+
shutil.rmtree(dest)
|
|
341
|
+
else:
|
|
342
|
+
rm(dest)
|
|
343
|
+
_create_symlink(src, dest, system)
|
|
344
|
+
logger.info(f"[Symlink created] {dest} -> {src}")
|
|
345
|
+
except OSError as e:
|
|
346
|
+
logger.error(f"OS error creating symlink: {str(e)}")
|
|
347
|
+
except Exception as e:
|
|
348
|
+
logger.error(f"Error creating symlink: {str(e)}")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# 创建符号链接辅助函数(根据操作系统选择方式)
|
|
352
|
+
def _create_symlink(src: str, dest: str, system: str) -> None:
|
|
353
|
+
if system != "Windows":
|
|
354
|
+
os.symlink(src, dest)
|
|
355
|
+
else:
|
|
356
|
+
subprocess.check_call(["cmd", "/c", "mklink", "/J", dest, src], shell=True)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ================================ 目录遍历 ================================
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# 获取指定文件夹下所有文件(返回字典 {文件名: 路径})
|
|
363
|
+
def sub_files(path: Union[str, Path]) -> Dict[str, str]:
|
|
364
|
+
result: Dict[str, str] = {}
|
|
365
|
+
path = str(path)
|
|
366
|
+
try:
|
|
367
|
+
for entry in os.listdir(path):
|
|
368
|
+
full_path = os.path.join(path, entry)
|
|
369
|
+
if os.path.isfile(full_path):
|
|
370
|
+
key = os.path.splitext(entry)[0]
|
|
371
|
+
result[key] = full_path
|
|
372
|
+
except OSError as e:
|
|
373
|
+
logger.error(f"Error listing files in {path}: {str(e)}")
|
|
374
|
+
return result
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# 获取指定文件夹下子文件夹(可排除指定模式)
|
|
378
|
+
def sub_folders(path: Union[str, Path], exclude: str = "") -> List[str]:
|
|
379
|
+
patterns = [p.strip() for p in exclude.replace(";", ",").split(",") if p.strip()]
|
|
380
|
+
path = str(path)
|
|
381
|
+
try:
|
|
382
|
+
if not os.path.isdir(path):
|
|
383
|
+
return []
|
|
384
|
+
all_dirs = [name for name in os.listdir(path) if os.path.isdir(os.path.join(path, name))]
|
|
385
|
+
filtered_dirs = [name for name in all_dirs if not any(fnmatch.fnmatch(name, pattern) for pattern in patterns)]
|
|
386
|
+
return filtered_dirs
|
|
387
|
+
except OSError as e:
|
|
388
|
+
logger.error(f"Error listing folders in {path}: {str(e)}")
|
|
389
|
+
return []
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# 获取目录中指定索引前缀的文件
|
|
393
|
+
def directory_idx(path: Union[str, Path], idx: int = 0) -> Tuple[Optional[str], Optional[str]]:
|
|
394
|
+
path = str(path)
|
|
395
|
+
try:
|
|
396
|
+
for filename in os.listdir(path):
|
|
397
|
+
if filename.lower().startswith(f"{idx:02d}_"):
|
|
398
|
+
file_path = ap(os.path.join(path, filename))
|
|
399
|
+
if os.path.isfile(file_path):
|
|
400
|
+
return file_path, os.path.splitext(filename)[0]
|
|
401
|
+
except OSError as e:
|
|
402
|
+
logger.error(f"Error accessing directory {path}: {str(e)}")
|
|
403
|
+
return None, None
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# 从基础目录提取相对路径
|
|
407
|
+
def relative_path(path: Union[str, Path], base_dir: str = "webapp") -> Path:
|
|
408
|
+
path = Path(path)
|
|
409
|
+
parts = list(path.parts)
|
|
410
|
+
try:
|
|
411
|
+
base_index = parts.index(base_dir)
|
|
412
|
+
relative_parts = parts[base_index + 1 :]
|
|
413
|
+
return Path(*relative_parts) if relative_parts else Path(".")
|
|
414
|
+
except ValueError:
|
|
415
|
+
return path
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# ================================ 临时文件 ================================
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# 获取或创建临时目录
|
|
422
|
+
def tempdir(root_dir: str = "tools") -> str:
|
|
423
|
+
temp_dir = tempfile.gettempdir()
|
|
424
|
+
temp_dir_root = os.path.join(temp_dir, root_dir.lower())
|
|
425
|
+
mk_folder(temp_dir_root)
|
|
426
|
+
return temp_dir_root
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# 获取唯一临时文件夹路径
|
|
430
|
+
def temp_folder() -> str:
|
|
431
|
+
return ap(os.path.join(tempdir(), next(tempfile._get_candidate_names())))
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# 获取临时视频文件路径
|
|
435
|
+
def temp_video_mp4(prefix: str = "", suffix: str = "", ext: str = "mp4") -> str:
|
|
436
|
+
prefix = prefix or next(tempfile._get_candidate_names())
|
|
437
|
+
suffix_str = f"_{suffix}" if suffix else ""
|
|
438
|
+
return ap(os.path.join(tempdir(), f"{prefix}{suffix_str}.{ext}"))
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# 获取临时音频文件路径
|
|
442
|
+
def temp_audio_wav(prefix: str = "", suffix: str = "", ext: str = "wav") -> str:
|
|
443
|
+
prefix = prefix or next(tempfile._get_candidate_names())
|
|
444
|
+
suffix_str = f"_{suffix}" if suffix else ""
|
|
445
|
+
return ap(os.path.join(tempdir(), f"{prefix}{suffix_str}.{ext}"))
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# 创建临时处理目录并拷贝源文件
|
|
449
|
+
def temp_process_path(path: str, rebuild: bool = False, output: Optional[str] = None) -> Tuple[str, str]:
|
|
450
|
+
file_name = filename(path)
|
|
451
|
+
if rebuild and output:
|
|
452
|
+
rm_folder(output)
|
|
453
|
+
if output:
|
|
454
|
+
mk_folder(output)
|
|
455
|
+
temp_file_path = ap(os.path.join(output, os.path.basename(path)))
|
|
456
|
+
cp(path, temp_file_path)
|
|
457
|
+
logger.info("{} -> save as: {}", path, output)
|
|
458
|
+
return file_name, temp_file_path
|
|
459
|
+
return file_name, path
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# 拷贝文件到系统临时目录
|
|
463
|
+
def move_to_temp(path: Union[str, Path]) -> str:
|
|
464
|
+
path = str(path)
|
|
465
|
+
filename = os.path.basename(path)
|
|
466
|
+
temp_dir = tempfile.gettempdir()
|
|
467
|
+
temp_path = os.path.join(temp_dir, filename)
|
|
468
|
+
shutil.copy(path, temp_path)
|
|
469
|
+
return temp_path
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# ================================ 压缩解压 ================================
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# 压缩文件/文件夹到 zip 归档
|
|
476
|
+
def zip_files(paths: List[Union[str, Path]], output: Union[str, Path]) -> None:
|
|
477
|
+
output = str(output)
|
|
478
|
+
try:
|
|
479
|
+
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
480
|
+
for path in paths:
|
|
481
|
+
path = str(path)
|
|
482
|
+
if os.path.isfile(path):
|
|
483
|
+
zipf.write(path, arcname=os.path.basename(path))
|
|
484
|
+
elif os.path.isdir(path):
|
|
485
|
+
for root, _, files in os.walk(path):
|
|
486
|
+
for file in files:
|
|
487
|
+
full_path = os.path.join(root, file)
|
|
488
|
+
rel_path = os.path.relpath(full_path, start=os.path.dirname(path))
|
|
489
|
+
zipf.write(full_path, arcname=rel_path)
|
|
490
|
+
logger.debug(f"Files compressed to: {output}")
|
|
491
|
+
except zipfile.BadZipFile as e:
|
|
492
|
+
logger.error(f"Bad zip file error: {str(e)}")
|
|
493
|
+
raise
|
|
494
|
+
except OSError as e:
|
|
495
|
+
logger.error(f"OS error creating zip: {str(e)}")
|
|
496
|
+
raise
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# 解压 zip 归档到目录
|
|
500
|
+
def unzip(path: Union[str, Path], extract_dir: Union[str, Path], password: Optional[str] = None) -> None:
|
|
501
|
+
path = str(path)
|
|
502
|
+
extract_dir = str(extract_dir)
|
|
503
|
+
|
|
504
|
+
if not zipfile.is_zipfile(path):
|
|
505
|
+
raise ValueError(f"Invalid zip file: {path}")
|
|
506
|
+
|
|
507
|
+
mk_folder(extract_dir)
|
|
508
|
+
try:
|
|
509
|
+
with zipfile.ZipFile(path, "r") as zip_ref:
|
|
510
|
+
if password:
|
|
511
|
+
zip_ref.setpassword(password.encode("utf-8"))
|
|
512
|
+
zip_ref.extractall(extract_dir)
|
|
513
|
+
logger.debug(f"Files extracted to: {extract_dir}")
|
|
514
|
+
except zipfile.BadZipFile as e:
|
|
515
|
+
logger.error(f"Bad zip file: {str(e)}")
|
|
516
|
+
raise
|
|
517
|
+
except RuntimeError as e:
|
|
518
|
+
logger.error(f"Runtime error extracting zip (wrong password?): {str(e)}")
|
|
519
|
+
raise
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
# ================================ 调用示例 ================================
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
if __name__ == "__main__":
|
|
526
|
+
# 测试路径(请根据实际情况修改)
|
|
527
|
+
test_file = "D:/Projects/test/file.txt"
|
|
528
|
+
test_folder = "D:/Projects/test"
|
|
529
|
+
|
|
530
|
+
# ==================== 路径处理示例 ====================
|
|
531
|
+
|
|
532
|
+
# 路径转换为 POSIX 格式
|
|
533
|
+
# result = ap("D:\\Projects\\test\\file.txt")
|
|
534
|
+
# logger.info("POSIX path: {}", result)
|
|
535
|
+
|
|
536
|
+
# 获取文件名(不含后缀)
|
|
537
|
+
# result = filename("D:/Projects/test/video.mp4")
|
|
538
|
+
# logger.info("File name: {}", result)
|
|
539
|
+
|
|
540
|
+
# 获取完整文件名(含后缀)
|
|
541
|
+
# result = fullname("D:/Projects/test/video.mp4")
|
|
542
|
+
# logger.info("Full name: {}", result)
|
|
543
|
+
|
|
544
|
+
# 获取文件名和后缀
|
|
545
|
+
# full, filename, ext = name_and_suffix("D:/Projects/test/video.mp4")
|
|
546
|
+
# logger.info("Name and suffix: {} | {} | {}", full, filename, ext)
|
|
547
|
+
|
|
548
|
+
# ==================== 文件读写示例 ====================
|
|
549
|
+
|
|
550
|
+
# 读取文件内容
|
|
551
|
+
# content = read(test_file)
|
|
552
|
+
# logger.info("File content: {}", content)
|
|
553
|
+
|
|
554
|
+
# 写入文件内容
|
|
555
|
+
# result = write("D:/Projects/test/output.txt", "Hello World")
|
|
556
|
+
# logger.info("Write result: {}", result)
|
|
557
|
+
|
|
558
|
+
# ==================== 文件操作示例 ====================
|
|
559
|
+
|
|
560
|
+
# 拷贝文件
|
|
561
|
+
# result = cp("D:/Projects/test/source.txt", "D:/Projects/test/backup/")
|
|
562
|
+
# logger.info("Copy result: {}", result)
|
|
563
|
+
|
|
564
|
+
# 移动文件
|
|
565
|
+
# result = mv("D:/Projects/test/source.txt", "D:/Projects/test/archive/")
|
|
566
|
+
# logger.info("Move result: {}", result)
|
|
567
|
+
|
|
568
|
+
# 拷贝文件夹
|
|
569
|
+
# cp_folder("D:/Projects/test/src", "D:/Projects/test/backup")
|
|
570
|
+
# logger.info("Folder copied")
|
|
571
|
+
|
|
572
|
+
# 新建文件夹
|
|
573
|
+
# result = mk_folder("D:/Projects/test/new_folder", is_clean=True)
|
|
574
|
+
# logger.info("Folder created: {}", result)
|
|
575
|
+
|
|
576
|
+
# 删除文件
|
|
577
|
+
# rm("D:/Projects/test/temp.txt")
|
|
578
|
+
# logger.info("File deleted")
|
|
579
|
+
|
|
580
|
+
# 删除文件夹
|
|
581
|
+
# rm_folder("D:/Projects/test/temp_folder")
|
|
582
|
+
# logger.info("Folder deleted")
|
|
583
|
+
|
|
584
|
+
# 覆盖文件
|
|
585
|
+
# overwrite("D:/Projects/test/new.txt", "D:/Projects/test/old.txt", backup=True)
|
|
586
|
+
# logger.info("File overwritten")
|
|
587
|
+
|
|
588
|
+
# ==================== 文件信息示例 ====================
|
|
589
|
+
|
|
590
|
+
# 计算文件 MD5 值
|
|
591
|
+
# result = md5_file(test_file)
|
|
592
|
+
# logger.info("MD5: {}", result)
|
|
593
|
+
|
|
594
|
+
# 获取文件大小(MB)
|
|
595
|
+
# result = get_file_size_MB(test_file)
|
|
596
|
+
# logger.info("File size: {} MB", result)
|
|
597
|
+
|
|
598
|
+
# 验证路径是否有效
|
|
599
|
+
# result = is_valid_path(test_folder)
|
|
600
|
+
# logger.info("Is valid path: {}", result)
|
|
601
|
+
|
|
602
|
+
# 验证图片文件夹是否有效
|
|
603
|
+
# result = is_valid_image_folder("D:/Projects/images")
|
|
604
|
+
# logger.info("Is valid image folder: {}", result)
|
|
605
|
+
|
|
606
|
+
# 清理非法文件名字符
|
|
607
|
+
# result = clean_filename("test:file*name?.txt")
|
|
608
|
+
# logger.info("Cleaned filename: {}", result)
|
|
609
|
+
|
|
610
|
+
# ==================== 符号链接示例 ====================
|
|
611
|
+
|
|
612
|
+
# 创建符号链接
|
|
613
|
+
# symlink("D:/Projects/source", "D:/Projects/link")
|
|
614
|
+
# logger.info("Symlink created")
|
|
615
|
+
|
|
616
|
+
# ==================== 目录遍历示例 ====================
|
|
617
|
+
|
|
618
|
+
# 获取指定文件夹下所有文件
|
|
619
|
+
# result = sub_files(test_folder)
|
|
620
|
+
# logger.info("Sub files: {}", result)
|
|
621
|
+
|
|
622
|
+
# 获取指定文件夹下子文件夹
|
|
623
|
+
# result = sub_folders(test_folder, exclude="__pycache__,*.egg-info")
|
|
624
|
+
# logger.info("Sub folders: {}", result)
|
|
625
|
+
|
|
626
|
+
# 获取目录中指定索引前缀的文件
|
|
627
|
+
# file_path, file_name = directory_idx("D:/Projects/test/tts", 0)
|
|
628
|
+
# logger.info("Directory idx: {} | {}", file_path, file_name)
|
|
629
|
+
|
|
630
|
+
# 获取相对路径
|
|
631
|
+
# result = relative_path("D:/Projects/webapp/static/js/app.js", "webapp")
|
|
632
|
+
# logger.info("Relative path: {}", result)
|
|
633
|
+
|
|
634
|
+
# ==================== 临时文件示例 ====================
|
|
635
|
+
|
|
636
|
+
# 获取临时目录
|
|
637
|
+
# result = tempdir()
|
|
638
|
+
# logger.info("Temp directory: {}", result)
|
|
639
|
+
|
|
640
|
+
# 获取临时文件夹路径
|
|
641
|
+
# result = temp_folder()
|
|
642
|
+
# logger.info("Temp folder: {}", result)
|
|
643
|
+
|
|
644
|
+
# 获取临时视频文件路径
|
|
645
|
+
# result = temp_video_mp4(prefix="test", suffix="clip")
|
|
646
|
+
# logger.info("Temp video path: {}", result)
|
|
647
|
+
|
|
648
|
+
# 获取临时音频文件路径
|
|
649
|
+
# result = temp_audio_wav(prefix="test", suffix="segment")
|
|
650
|
+
# logger.info("Temp audio path: {}", result)
|
|
651
|
+
|
|
652
|
+
# 创建临时处理目录
|
|
653
|
+
# file_name, temp_path = temp_process_path("D:/video.mp4", output="D:/temp/process")
|
|
654
|
+
# logger.info("Temp process: {} | {}", file_name, temp_path)
|
|
655
|
+
|
|
656
|
+
# 移动文件到临时目录
|
|
657
|
+
# result = move_to_temp(test_file)
|
|
658
|
+
# logger.info("Moved to temp: {}", result)
|
|
659
|
+
|
|
660
|
+
# ==================== 压缩解压示例 ====================
|
|
661
|
+
|
|
662
|
+
# 压缩文件
|
|
663
|
+
# zip_files(["D:/Projects/test/file1.txt", "D:/Projects/test/folder"], "D:/output.zip")
|
|
664
|
+
# logger.info("Files zipped")
|
|
665
|
+
|
|
666
|
+
# 解压文件
|
|
667
|
+
# unzip("D:/output.zip", "D:/extracted")
|
|
668
|
+
# logger.info("Files unzipped")
|
|
669
|
+
|
|
670
|
+
# ==================== 下载示例 ====================
|
|
671
|
+
|
|
672
|
+
# 使用 yt-dlp 下载视频
|
|
673
|
+
# result = ytdlp_download(url="https://www.youtube.com/watch?v=xxxxx")
|
|
674
|
+
# logger.info("Downloaded video: {}", result)
|
|
675
|
+
|
|
676
|
+
# HTTP 下载文件
|
|
677
|
+
# result = http_download("https://example.com/file.zip", save_folder="D:/downloads")
|
|
678
|
+
# logger.info("Downloaded file: {}", result)
|
|
679
|
+
|
|
680
|
+
# ==================== SRT 字幕示例 ====================
|
|
681
|
+
|
|
682
|
+
# 加载 SRT 字幕文件
|
|
683
|
+
# segments = load_srt("D:/Projects/test/subtitle.srt")
|
|
684
|
+
# logger.info("Loaded segments: {}", len(segments))
|
|
685
|
+
|
|
686
|
+
# 写入 SRT 字幕文件
|
|
687
|
+
# data = [{"start": 0.0, "end": 2.5, "text": "Hello"}, {"start": 2.5, "end": 5.0, "text": "World"}]
|
|
688
|
+
# result = write_srt(data, "D:/Projects/test/output.srt")
|
|
689
|
+
# logger.info("SRT written: {}", result)
|
|
690
|
+
|
|
691
|
+
# 移除空白和重复字幕
|
|
692
|
+
# result = remove_blank_and_duplicate_srt("D:/Projects/test/subtitle.srt")
|
|
693
|
+
# logger.info("SRT cleaned: {}", result)
|
|
694
|
+
|
|
695
|
+
# 从 JSON 生成 SRT 字幕
|
|
696
|
+
# result = generate_srt("D:/Projects/test/data.json", "text", "D:/Projects/test/output.srt")
|
|
697
|
+
# logger.info("SRT generated: {}", result)
|