lq2007 发表于 2023-7-29 23:46

Python+tkinter+pywin32 实现的用于创建和管理磁盘中符号链接的工具

本帖最后由 lq2007 于 2023-7-29 23:47 编辑

由于 C 盘空间不足,我有把默认存放在 C 盘的目录通过符号链接转移出去的习惯(不改默认配置主要是多个程序都要改麻烦)。于是写了一个扫描和建立链接的工具以防万一

`file.py` 主要创建 File 类用于获取文件类型、是否为符号链接或快捷方式,并取得其真实地址

```python
"""
获取文件信息
: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` 文件主要实现查找目录中的所有符号链接及其真实地址

```python
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.append(f)
      else:
            self.data =
      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` 可以一次为一组文件创建符号链接,也可以将文件或目录转移到新位置后在原位置创建符号链接。在执行前会检查是否有重名文件、如果要移动也会检查是否被占用。在这里需要管理员权限

```python
"""
创建符号链接
:return: None
"""
lst_proc = self._process
lst_proc.delete(0, 'end')
# 检查文件名是否冲突
file_map_by_name = {}# type: dict]
for file in self._src.files:
    if file.basename in file_map_by_name:
      file_map_by_name.append(file)
    else:
      file_map_by_name =
err_msg = ''
for name in file_map_by_name:
    arr = file_map_by_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 系统相关的函数,多会使用系统的库函数,用于获取文件属性、创建符号链接、检查文件占用、管理员权限等

```python
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
```

目前已知的问题主要是:
1. 使用 Listbox 作为信息输出感觉还是不太方便
2. 扫描出的符号链接需要多点一下才能切换过去
3. 导出的文件重新导入还没有测试

以及,在 Windows 11 的情况下,尝试了多种方式也没能实现将文件拖拽进程序中的操作,windnd 和 pydnd 好像都不行了?

完整的源代码位于(https://github.com/luiqn2007/device_manager),或者从论坛附件下载,附件下载的解压密码 52pojie

xiatongxue 发表于 2023-7-30 14:01

看下 学习ing....

angxi6 发表于 2023-7-30 14:30

学到了!!

数码小叶 发表于 2023-7-30 15:13

看下 学习ing....

258239234 发表于 2023-7-30 15:55

很专业,高手!

cageforawalk666 发表于 2023-7-31 15:16

有学到新知识!
页: [1]
查看完整版本: Python+tkinter+pywin32 实现的用于创建和管理磁盘中符号链接的工具