由于 C 盘空间不足,我有把默认存放在 C 盘的目录通过符号链接转移出去的习惯(不改默认配置主要是多个程序都要改麻烦)。于是写了一个扫描和建立链接的工具以防万一
file.py
主要创建 File 类用于获取文件类型、是否为符号链接或快捷方式,并取得其真实地址
"""
获取文件信息
:param p: 文件路径
:return: 文件信息
"""
self.path = p
# 判断文件 or 文件夹
self.is_directory = os.path.isdir(p)
self.is_file = os.path.isfile(p)
self.is_exist = self.is_file or self.is_directory
if self.is_exist:
s = os.stat(p)
# 文件基本信息
self.hard_link_count = s.st_nlink
self.size = s.st_size
# 判断链接
self.is_windows_link = self.is_file and os.path.basename(p).endswith('.lnk')
if self.is_windows_link:
# 获取 lnk 目标
shell = win32com.client.Dispatch('WScript.Shell')
shortcut = shell.CreateShortCut(p)
real_path = str(shortcut.Targetpath)
target = File(real_path)
self.real_path = target.real_path
self.is_symbol_link = False
# 快捷方式可能无效或为网站等
self.is_exist = target.is_exist
else:
# 检查符号链接
attr = find_first_file(p)
self.is_symbol_link = not (not attr.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) \
and IsReparseTagNameSurrogate(attr.dwReserved0)
self.real_path = os.path.realpath(p)
else:
self.real_path = None
self.is_symbol_link = False
self.is_windows_link = False
self.hard_link_count = 0
self.size = 0
self.basename = os.path.basename(self.real_path) if self.is_exist else None
componenet/link_manager.py
文件主要实现查找目录中的所有符号链接及其真实地址
def _select_dir(self):
"""
选择一个目录,遍历所有文件并将符号链接及其源文件插入列表
:return: None
"""
# 检查是否有正在进行的任务
if self.is_working:
messagebox.showerror('任务进行中', '当前存在正在进行的任务,无法进行新任务')
return
# 选择目录
directory = filedialog.askdirectory()
if directory is None or directory == "" or not os.path.isdir(directory):
return
self.lst_log.delete(0, 'end')
self.lst_log.insert('end', f'loading {directory}')
self.lst_log.insert('end', '...')
last_idx = self.lst_log.size() - 1
# 插入文件到记录
def insert_to_data(f: File):
if f.real_path in self.data:
self.data[f.real_path].append(f)
else:
self.data[f.real_path] = [f]
self.fresh()
# 遍历目录,递归查找符号链接
# 返回 True 表示查询中断
def walk_dir(file_dir: File) -> bool:
if self.working_stop:
self.is_working = False
self.working_stop = False
return True
self.lst_log.delete(last_idx, last_idx)
self.lst_log.insert(last_idx, f'...{file_dir.path}')
# 目录本身是符号链接
if file_dir.is_symbol_link:
insert_to_data(file_dir)
return False
# 遍历目录内容
try:
for entry in os.scandir(file_dir.real_path):
if self.working_stop:
self.is_working = False
self.working_stop = False
return True
self.lst_log.delete(last_idx, last_idx)
self.lst_log.insert(last_idx, f'...{file_dir.path}')
file = File(entry.path)
if file.is_symbol_link:
insert_to_data(file)
elif file.is_directory:
if walk_dir(file):
return True
return False
except PermissionError:
self.lst_log.insert('end', f' -{file_dir.real_path} 权限不足')
return False
self.is_working = True
self.working_stop = False
td = Thread(target=lambda: walk_dir(File(directory)))
td.start()
componenet/link_create.py
可以一次为一组文件创建符号链接,也可以将文件或目录转移到新位置后在原位置创建符号链接。在执行前会检查是否有重名文件、如果要移动也会检查是否被占用。在这里需要管理员权限
"""
创建符号链接
:return: None
"""
lst_proc = self._process
lst_proc.delete(0, 'end')
# 检查文件名是否冲突
file_map_by_name = {} # type: dict[str, list[File]]
for file in self._src.files:
if file.basename in file_map_by_name:
file_map_by_name[file.basename].append(file)
else:
file_map_by_name[file.basename] = [file]
err_msg = ''
for name in file_map_by_name:
arr = file_map_by_name[name]
if len(arr) > 1:
err_msg += f'文件重复:{name}'
for file in arr:
err_msg += f'\n{file.real_path}'
if len(err_msg) > 0:
messagebox.showerror('文件重复', err_msg)
return
lst_proc.insert('end', '文件重复性检查完成')
# 检查文件是否已存在
for file in self._dst.files:
for pp in os.scandir(file.real_path):
name = os.path.basename(pp)
if name in file_map_by_name:
if len(err_msg) > 0:
err_msg += '\n'
err_msg += f'{file.real_path}: {name} 已存在'
continue
if len(err_msg) > 0:
messagebox.showerror('文件已存在', err_msg)
return
lst_proc.insert('end', '文件存在性检查完成')
# 检查管理员权限
winapi.require_admin()
while not winapi.is_admin():
if messagebox.askretrycancel('权限不足', '需要管理员权限运行'):
winapi.require_admin()
else:
return
# 创建软连接
count = len(self._src.files)
for index, file in enumerate(self._src.files):
for dp in self._dst.files:
winapi.make_symbol_in(file.real_path, dp.real_path, file.basename, file.is_directory)
lst_proc.insert('end', f'{index + 1}/{count} {file.real_path} <- {dp.real_path}/{file.basename}')
winapi.py
主要都是涉及到与 Windows 系统相关的函数,多会使用系统的库函数,用于获取文件属性、创建符号链接、检查文件占用、管理员权限等
def find_first_file(p: str) -> WIN32_FIND_DATAW:
"""
查找文件(夹)的文件属性
:param p: 文件路径
:return: 文件属性,详见 https://learn.microsoft.com/zh-cn/windows/win32/fileio/file-attribute-constants
"""
find_result = wintypes.WIN32_FIND_DATAW()
find_handle = kernel32.FindFirstFileW(p, ctypes.byref(find_result))
kernel32.FindClose(find_handle)
return find_result
def make_symbol_link(src: str, dst: str, is_directory: bool):
"""
创建符号链接
:param src: 源文件/目录
:param dst: 目标文件/目录
:param is_directory: 是否为目录
:return: None
"""
# return kernel32.CreateSymbolicLinkW(src, dst, 0x1 if is_directory else 0x0)
print(f"link {src} <= {dst}")
win32file.CreateSymbolicLink(dst, src, 1 if is_directory else 0)
def is_admin() -> bool:
"""
检查是否为管理员权限
:return: 是否以管理员权限执行
"""
try:
return shell32.IsUserAnAdmin()
except:
return False
def require_admin():
"""
请求管理员权限
:return: None
"""
if not is_admin():
shell32.ShellExecuteW(None, 'runas', sys.executable, __file__, None, 1)
def is_file_used(p):
"""
检查文件是否被占用
:param p: 文件
:return: 是否被占用
"""
handle = None
try:
handle = win32file.CreateFile(p, win32file.GENERIC_READ,
0, None, win32file.OPEN_EXISTING, win32file.FILE_ATTRIBUTE_NORMAL, None)
return int(handle) == win32file.INVALID_HANDLE_VALUE
except:
return True
finally:
try:
win32file.CloseHandle(handle)
except:
pass
def is_directory_open(p):
"""
检查目录是否被占用
:param p: 目录
:return: 是否被占用
"""
try:
os.listdir(p)
return False
except:
return True
目前已知的问题主要是:
- 使用 Listbox 作为信息输出感觉还是不太方便
- 扫描出的符号链接需要多点一下才能切换过去
- 导出的文件重新导入还没有测试
以及,在 Windows 11 的情况下,尝试了多种方式也没能实现将文件拖拽进程序中的操作,windnd 和 pydnd 好像都不行了?
完整的源代码位于Github,或者从论坛附件下载,附件下载的解压密码 52pojie