文件夹批量迁移工具:设计与实现

50 阅读10分钟

文件夹批量迁移工具:设计与实现

在日常开发和文件管理中,我们经常需要在不同存储位置之间迁移项目文件夹。手动处理不仅效率低下,还容易出现遗漏或错误。为此,我设计了一款文件夹批量迁移工具,能够智能筛选可迁移的文件夹并执行迁移操作,同时生成详细的迁移记录。

工具设计思路

该工具的核心目标是安全、高效地迁移文件夹,主要解决以下问题:

  • 避免迁移过大的文件夹导致的性能问题
  • 识别并跳过包含大量小文件的文件夹(这类文件夹迁移效率极低)
  • 排除包含特定目录(如node_modules)的项目
  • 提供预览功能,让用户在实际执行前了解迁移计划
  • 生成详细的迁移记录,便于追踪和回溯

核心功能实现

配置参数设计

工具定义了一系列可配置参数,方便用户根据实际需求调整:

# 源根目录和目标根目录(实际使用时设置)
SOURCE_ROOT = ""  # 源根目录(要扫描的目录)
TARGET_ROOT = ""  # 目标根目录(要移动到的目录)

MAX_SIZE_MB = 600  # 允许移动的最大文件夹大小(MB)
LARGE_FILE_COUNT = 100  # 判定"大量文件"的阈值(文件数)
SMALL_FILE_AVG_SIZE_KB = 100  # 判定"小文件"的平均大小阈值(KB)
MOVE_RECORD_FILENAME = "move_record.json"  # 单个文件夹的移动记录文件名

这些参数可以根据实际的存储环境和迁移需求进行调整,平衡迁移效率和完整性。

关键工具函数

  1. 文件夹大小计算:准确计算文件夹总大小,用于判断是否符合迁移条件
def get_folder_total_size(folder_path: str) -> int:
    """计算文件夹的总大小(字节)"""
    total_size = 0
    for root, dirs, files in os.walk(folder_path):
        for file in files:
            file_path = os.path.join(root, file)
            total_size += os.path.getsize(file_path)  # 累加每个文件大小
    return total_size
  1. 文件特征分析:判断文件夹是否包含大量小文件,这类文件夹通常迁移效率低
def check_large_small_files(folder_path: str) -> bool:
    """检查文件夹是否包含"大量小文件"(文件数>阈值 且 平均大小<阈值)"""
    file_list = []
    for root, dirs, files in os.walk(folder_path):
        # 排除隐藏文件
        valid_files = [f for f in files if not f.startswith('.')]
        for file in valid_files:
            file_path = os.path.join(root, file)
            file_list.append(os.path.getsize(file_path))  # 收集所有文件大小
    
    if len(file_list) <= LARGE_FILE_COUNT:
        return False  # 文件数未达阈值,不是大量文件
    
    # 计算平均文件大小(KB)
    avg_size_kb = (sum(file_list) / len(file_list)) / 1024
    return avg_size_kb < SMALL_FILE_AVG_SIZE_KB  # 平均大小<阈值,判定为大量小文件
  1. 迁移适用性检查:综合评估文件夹是否适合迁移
def check_unsuitable_folders(folder_path: str) -> Dict[str, str]:
    """检查文件夹是否"不适合移动",返回不适合的原因(无则返回空字典)"""
    # 1. 检查是否包含 node_modules 文件夹
    for root, dirs, _ in os.walk(folder_path):
        if "node_modules" in dirs:
            return {"reason": f"包含 node_modules 文件夹(路径:{os.path.join(root, 'node_modules')})"}
    
    # 2. 检查是否包含大量小文件
    if check_large_small_files(folder_path):
        return {"reason": f"包含大量小文件(文件数> {LARGE_FILE_COUNT} 且 平均大小< {SMALL_FILE_AVG_SIZE_KB}KB)"}
    
    # 3. 检查文件夹大小是否超过阈值
    total_size_mb = get_folder_total_size(folder_path) / (1024 * 1024)
    if total_size_mb >= MAX_SIZE_MB:
        return {"reason": f"文件夹过大({total_size_mb:.2f}MB ≥ {MAX_SIZE_MB}MB)"}
    
    # 无不适宜原因
    return {}

主流程控制

主函数实现了整个迁移过程的控制逻辑,包括:

  • 命令行参数解析(支持预览模式和实际执行模式)
  • 源目录检查和目标目录创建
  • 文件夹遍历和筛选
  • 迁移操作执行
  • 迁移记录生成
def main():
    parser = argparse.ArgumentParser(description="移动源目录下的文件夹到目标目录")
    parser.add_argument('--do-move', action='store_true', default=False, 
                        help='是否真正执行移动操作,默认为False(仅显示要移动的内容但不实际移动)')
    args = parser.parse_args()
    
    dry_run = not args.do_move
    
    if dry_run:
        print("注意:当前为预览模式,不会实际移动任何文件夹。使用 --do-move 参数来真正执行移动。")
    
    # 检查源目录是否存在
    if not os.path.exists(SOURCE_ROOT):
        print(f"错误:源目录 {SOURCE_ROOT} 不存在,请检查路径!")
        return
    
    # 创建目标根目录(不存在则创建)
    os.makedirs(TARGET_ROOT, exist_ok=True)
    
    # 初始化汇总记录
    summary_record: List[Dict] = []
    
    # 遍历源目录下的所有一级子文件夹
    for folder_name in os.listdir(SOURCE_ROOT):
        source_folder = os.path.join(SOURCE_ROOT, folder_name)
        # 只处理文件夹(排除文件)
        if not os.path.isdir(source_folder):
            continue
        
        print(f"\n正在处理:{source_folder}")
        folder_record = {
            "folder_name": folder_name,
            "source_path": source_folder,
            "status": "未处理",
            "reason": "",
            "target_path": ""
        }
        
        # 检查是否适合移动
        unsuitable_info = check_unsuitable_folders(source_folder)
        if unsuitable_info:
            # 不适合移动:记录原因
            folder_record["status"] = "移动失败"
            folder_record["reason"] = unsuitable_info["reason"]
            print(f"→ 不适合移动:{unsuitable_info['reason']}")
            summary_record.append(folder_record)
            continue
        
        # 适合移动:执行移动操作
        target_folder = os.path.join(TARGET_ROOT, folder_name)
        # ... 移动逻辑实现 ...
        
        summary_record.append(folder_record)
    
    # 生成总汇总记录JSON,文件名包含时间戳
    # ... 记录生成逻辑 ...

使用方法

  1. 配置源目录和目标目录路径
  2. 根据需要调整其他参数(最大文件夹大小、文件数量阈值等)
  3. 先执行预览模式,检查迁移计划是否符合预期:
    python script.py
    
  4. 确认无误后,执行实际迁移:
    python script.py --do-move
    

工具特点

  1. 安全性:提供预览模式,避免误操作
  2. 智能筛选:自动识别不适合迁移的文件夹
  3. 可追溯性:详细记录每个文件夹的迁移状态和路径
  4. 灵活性:通过参数调整适应不同的迁移需求
  5. 健壮性:包含错误处理和异常情况处理

该工具特别适合需要定期整理和迁移大量项目文件夹的开发人员或系统管理员使用,能够显著提高工作效率并减少人为错误。

完整代码

import os
import shutil
import json
import argparse
from typing import Dict, List
from datetime import datetime

# -------------------------- 配置参数(可根据需求调整) --------------------------
SOURCE_ROOT = ""  # 源根目录(要扫描的目录)
TARGET_ROOT = ""  # 目标根目录(要移动到的目录)

MAX_SIZE_MB = 600  # 允许移动的最大文件夹大小(MB)
LARGE_FILE_COUNT = 100  # 判定"大量文件"的阈值(文件数)
SMALL_FILE_AVG_SIZE_KB = 100  # 判定"小文件"的平均大小阈值(KB)
MOVE_RECORD_FILENAME = "move_record.json"  # 单个文件夹的移动记录文件名

# -------------------------- 核心工具函数 --------------------------
def get_folder_total_size(folder_path: str) -> int:
    """
    计算文件夹的总大小(字节)
    :param folder_path: 文件夹路径
    :return: 总大小(字节)
    """
    total_size = 0
    for root, dirs, files in os.walk(folder_path):
        for file in files:
            file_path = os.path.join(root, file)
            total_size += os.path.getsize(file_path)  # 累加每个文件大小
    return total_size

def check_large_small_files(folder_path: str) -> bool:
    """
    检查文件夹是否包含"大量小文件"(文件数>阈值 且 平均大小<阈值)
    :param folder_path: 文件夹路径
    :return: True=包含大量小文件,False=不包含
    """
    file_list = []
    for root, dirs, files in os.walk(folder_path):
        # 排除隐藏文件
        valid_files = [f for f in files if not f.startswith('.')]
        for file in valid_files:
            file_path = os.path.join(root, file)
            file_list.append(os.path.getsize(file_path))  # 收集所有文件大小
    
    if len(file_list) <= LARGE_FILE_COUNT:
        return False  # 文件数未达阈值,不是大量文件
    
    # 计算平均文件大小(KB)
    avg_size_kb = (sum(file_list) / len(file_list)) / 1024
    return avg_size_kb < SMALL_FILE_AVG_SIZE_KB  # 平均大小<阈值,判定为大量小文件

def check_unsuitable_folders(folder_path: str) -> Dict[str, str]:
    """
    检查文件夹是否"不适合移动",返回不适合的原因(无则返回空字典)
    :param folder_path: 文件夹路径
    :return: {"reason": 原因描述} 或 {}
    """
    # 1. 检查是否包含 node_modules 文件夹
    for root, dirs, _ in os.walk(folder_path):
        if "node_modules" in dirs:
            return {"reason": f"包含 node_modules 文件夹(路径:{os.path.join(root, 'node_modules')})"}
    
    # 2. 检查是否包含大量小文件
    if check_large_small_files(folder_path):
        return {"reason": f"包含大量小文件(文件数> {LARGE_FILE_COUNT} 且 平均大小< {SMALL_FILE_AVG_SIZE_KB}KB)"}
    
    # 3. 检查文件夹大小是否超过阈值
    total_size_mb = get_folder_total_size(folder_path) / (1024 * 1024)
    if total_size_mb >= MAX_SIZE_MB:
        return {"reason": f"文件夹过大({total_size_mb:.2f}MB ≥ {MAX_SIZE_MB}MB)"}
    
    # 无不适宜原因
    return {}

# -------------------------- 主执行函数 --------------------------
def main():
    parser = argparse.ArgumentParser(description="移动源目录下的文件夹到目标目录")
    parser.add_argument('--do-move', action='store_true', default=False, 
                        help='是否真正执行移动操作,默认为False(仅显示要移动的内容但不实际移动)')
    args = parser.parse_args()
    
    dry_run = not args.do_move
    
    if dry_run:
        print("注意:当前为预览模式,不会实际移动任何文件夹。使用 --do-move 参数来真正执行移动。")
    
    # 1. 检查源目录是否存在
    if not os.path.exists(SOURCE_ROOT):
        print(f"错误:源目录 {SOURCE_ROOT} 不存在,请检查路径!")
        return
    
    # 2. 创建目标根目录(不存在则创建)
    os.makedirs(TARGET_ROOT, exist_ok=True)
    
    # 3. 初始化汇总记录
    summary_record: List[Dict] = []
    
    # 4. 遍历源目录下的所有一级子文件夹
    for folder_name in os.listdir(SOURCE_ROOT):
        source_folder = os.path.join(SOURCE_ROOT, folder_name)
        # 只处理文件夹(排除文件)
        if not os.path.isdir(source_folder):
            continue
        
        print(f"\n正在处理:{source_folder}")
        folder_record = {
            "folder_name": folder_name,
            "source_path": source_folder,
            "status": "未处理",
            "reason": "",
            "target_path": ""
        }
        
        # 5. 检查是否适合移动
        unsuitable_info = check_unsuitable_folders(source_folder)
        if unsuitable_info:
            # 不适合移动:记录原因
            folder_record["status"] = "移动失败"
            folder_record["reason"] = unsuitable_info["reason"]
            print(f"→ 不适合移动:{unsuitable_info['reason']}")
            summary_record.append(folder_record)
            continue
        
        # 6. 适合移动:执行移动操作
        target_folder = os.path.join(TARGET_ROOT, folder_name)
        if dry_run:
            # 在预览模式下,仅记录将要移动但不执行操作
            if os.path.exists(target_folder):
                folder_record["status"] = "目标已存在(未移动)"
                folder_record["reason"] = f"目标文件夹已存在:{target_folder}"
                print(f"→ 目标已存在:{target_folder},不会移动 {source_folder}")
            else:
                folder_record["status"] = "预览模式(未移动)"
                folder_record["target_path"] = target_folder
                print(f"→ [预览] 将会移动:{source_folder}{target_folder}")
        else:
            # 检查目标文件夹是否已存在
            if os.path.exists(target_folder):
                folder_record["status"] = "移动失败"
                folder_record["reason"] = f"目标文件夹已存在:{target_folder}"
                print(f"→ 目标已存在:{target_folder},不会移动 {source_folder}")
            else:
                # 实际执行移动操作
                try:
                    shutil.move(source_folder, target_folder)
                    
                    # 7. 在目标路径生成移动记录JSON
                    move_record = {
                        "moved_from": source_folder,
                        "moved_to": target_folder,
                        "move_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                    }
                    with open(os.path.join(target_folder, MOVE_RECORD_FILENAME), "w", encoding="utf-8") as f:
                        json.dump(move_record, f, ensure_ascii=False, indent=2)
                    
                    # 8. 更新汇总记录
                    folder_record["status"] = "移动成功"
                    folder_record["target_path"] = target_folder
                    print(f"→ 移动成功:{source_folder}{target_folder}")
                    print(f"→ 目标路径已生成记录:{os.path.join(target_folder, MOVE_RECORD_FILENAME)}")
                
                except Exception as e:
                    # 移动过程出错(如权限不足)
                    folder_record["status"] = "移动失败"
                    folder_record["reason"] = f"移动时出错:{str(e)}"
                    print(f"→ 移动失败:{str(e)}")
        
        summary_record.append(folder_record)
    
    # 9. 生成总汇总记录JSON,文件名包含时间戳
    try:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        summary_filename = f"summary_record_{timestamp}.json"
        with open(os.path.join(SOURCE_ROOT, summary_filename), "w", encoding="utf-8") as f:
            json.dump(summary_record, f, ensure_ascii=False, indent=2)
        
        print(f"\n处理完成!总汇总记录已保存至:{os.path.join(SOURCE_ROOT, summary_filename)}")
    except Exception as e:
        print(f"\n警告:无法保存汇总记录文件:{str(e)}")
        # 仍然显示记录在当前目录作为备用
        backup_filename = f"summary_record_{timestamp}.json" if 'timestamp' in locals() else f"summary_record_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
        try:
            with open(backup_filename, "w", encoding="utf-8") as f:
                json.dump(summary_record, f, ensure_ascii=False, indent=2)
            print(f"汇总记录已保存至当前目录:{backup_filename}")
        except Exception as backup_e:
            print(f"也无法保存到当前目录:{str(backup_e)}")
            print("汇总记录内容:")
            print(json.dumps(summary_record, ensure_ascii=False, indent=2))

# -------------------------- 启动脚本 --------------------------
if __name__ == "__main__":
    main()