GitHub 文件/文件夹批量上传工具

一、工具介绍

基于 Python + PyQt5 开发的可视化桌面应用,核心功能是自动创建 GitHub 仓库 并支持单个文件整个文件夹的批量上传,无需手动操作 Git 命令或 GitHub 网页端,适合非技术人员或需要快速上传文件到 GitHub 的场景。

核心特性

  1. 可视化操作界面,无需命令行;

  2. 自动检测/创建 GitHub 仓库(不存在则创建,存在则直接使用);

  3. 支持单文件/整文件夹上传,保留文件夹原有目录结构;

  4. 实时上传进度显示 + 详细日志输出,便于排查问题;

  5. 支持文件覆盖(自动获取文件 SHA 值实现更新);

  6. 自定义仓库内上传根路径(如 uploads/)。

二、环境准备

1. 依赖安装

工具基于 Python 开发,需先安装以下依赖包:

python 复制代码
pip install requests PyQt5

2. GitHub 个人访问令牌(PAT)准备

上传文件需要 GitHub 权限,需创建带 repo 权限的经典令牌:

  1. 打开 GitHub 设置:https://github.com/settings/tokens

  2. 点击「Generate new token (classic)」;

  3. 勾选 repo 权限(仓库相关操作);

  4. 设置令牌有效期(建议选「No expiration」,或按需设置);

  5. 生成令牌后立即复制保存(仅显示一次,丢失需重新创建)。

三、工具使用教程

1. 运行程序

将代码保存为 github_uploader.py,执行以下命令启动:

python 复制代码
python github_uploader.py

启动后会看到可视化主界面,分为 5 个核心区域:

2. 操作步骤

步骤 1:选择上传模式
  • 「单个文件」:上传指定的单个文件(如 test.txt);

  • 「整个文件夹」:上传文件夹内所有文件(保留目录结构)。

步骤 2:选择本地路径
  • 点击「选择文件/选择文件夹」按钮,选择要上传的本地文件/文件夹;

  • 路径会自动填充到输入框中。

步骤 3:填写 GitHub 配置
配置项 说明
仓库所有者 你的 GitHub 用户名(区分大小写,如 octocat
仓库名称 要创建/上传的仓库名(如 file-upload-test,不存在则自动创建)
PAT 令牌 步骤 2 中创建的带 repo 权限的令牌(输入时隐藏,保障安全)
仓库内根路径 可选,文件上传到仓库的指定目录(默认 uploads/,如填 docs/ 则文件上传到 docs/ 下)
步骤 4:开始上传
  • 点击「开始上传」按钮,程序会:

    • 校验输入是否完整(路径、用户名、仓库名、令牌不能为空);

    • 检查仓库是否存在,不存在则自动创建;

    • 扫描本地文件,批量上传(实时输出日志 + 进度);

  • 「清空日志」按钮可清空当前日志区域内容,便于重新上传。

3. 上传结果说明

  • 上传成功:弹窗提示「上传完成!所有文件均成功上传到 GitHub」,日志显示每个文件的上传状态;

  • 部分失败:弹窗提示「上传完成,但有步骤失败」,需查看日志中的错误信息(如令牌权限不足、文件路径错误等)。

四、核心代码解析

1. 核心类说明

类名 作用
UploadThread 上传线程类(继承 QThread),避免 UI 卡顿,包含仓库创建、文件扫描、文件上传逻辑
GitHubUploader 主界面类(继承 QWidget),负责界面布局、用户交互、线程调用

2. 关键功能实现

(1)自动创建仓库
python 复制代码
def create_github_repo(self):
    # 检查仓库是否存在
    check_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}"
    check_response = requests.get(check_url, headers=headers)
    # 不存在则创建
    if check_response.status_code != 200:
        create_url = "https://api.github.com/user/repos"
        create_data = {"name": self.repo_name, "private": False, "auto_init": True}
        requests.post(create_url, json=create_data, headers=headers)
  • 通过 GitHub API 先检查仓库是否存在,状态码 200 表示存在;

  • 不存在则调用创建仓库 API,auto_init=True 自动初始化仓库(避免空仓库无法上传)。

(2)文件上传核心逻辑
python 复制代码
def upload_single_file(self, local_file_path, github_file_path):
    # 读取文件并 Base64 编码(GitHub API 要求)
    with open(local_file_path, "rb") as f:
        file_content = base64.b64encode(f.read()).decode("utf-8")
    # 构建上传请求(PUT 方法,支持覆盖)
    api_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}/contents/{github_file_path}"
    data = {"message": f"Upload file: {os.path.basename(local_file_path)}", "content": file_content}
    # 若文件已存在,添加 SHA 值实现覆盖
    if response.status_code == 200:
        data["sha"] = response.json()["sha"]
    requests.put(api_url, json=data, headers=headers)
  • GitHub API 要求文件内容为 Base64 编码;

  • 使用 PUT 方法上传,已存在的文件需传入 SHA 值实现覆盖。

(3)多线程处理
python 复制代码
# 线程信号定义(用于线程与 UI 通信)
log_signal = pyqtSignal(str)  # 日志更新
progress_signal = pyqtSignal(int)  # 进度更新
finish_signal = pyqtSignal(bool)  # 上传完成
  • 上传逻辑放在独立线程中,避免 UI 冻结;

  • 通过 pyqtSignal 实现线程向主界面传递日志、进度等信息。

五、常见问题排查

1. 仓库创建失败

  • 原因 1:PAT 令牌无 repo 权限 → 重新创建令牌并勾选 repo

  • 原因 2:仓库名已被占用 → 更换仓库名;

  • 原因 3:网络问题 → 检查网络连接,或更换 GitHub 镜像源。

2. 文件上传失败

  • 原因 1:本地文件路径错误 → 确认文件/文件夹存在;

  • 原因 2:令牌过期/错误 → 检查令牌是否正确,或重新生成;

  • 原因 3:文件过大 → GitHub API 单文件上传限制 100MB,超大文件需用 Git LFS(工具暂不支持)。

3. UI 无响应

  • 原因:上传超大文件夹时,线程未及时释放资源 → 等待日志输出,或拆分文件夹分批上传。

七、完整代码(可直接复制)

python 复制代码
import sys
import requests
import base64
import os
from pathlib import Path
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout,
    QLabel, QLineEdit, QPushButton, QFileDialog,
    QTextEdit, QGroupBox, QMessageBox, QRadioButton
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal


# 上传线程(支持自动创建仓库 + 文件/文件夹上传)
class UploadThread(QThread):
    log_signal = pyqtSignal(str)  # 日志信号
    progress_signal = pyqtSignal(int)  # 进度信号
    finish_signal = pyqtSignal(bool)  # 完成信号

    def __init__(self, target_path, repo_owner, repo_name, github_token, github_root_path, is_folder):
        super().__init__()
        self.target_path = target_path  # 本地文件/文件夹路径
        self.repo_owner = repo_owner  # GitHub仓库所有者
        self.repo_name = repo_name  # GitHub仓库名
        self.github_token = github_token  # PAT令牌
        self.github_root_path = github_root_path  # 仓库内根路径
        self.is_folder = is_folder  # 是否是文件夹上传
        self.total_files = 0  # 总文件数
        self.uploaded_files = 0  # 已上传文件数

    # 自动创建GitHub仓库(不存在则创建)
    def create_github_repo(self):
        try:
            # 1. 先检查仓库是否已存在
            check_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}"
            headers = {
                "Authorization": f"token {self.github_token}",
                "Accept": "application/vnd.github.v3+json"
            }
            check_response = requests.get(check_url, headers=headers)

            # 仓库已存在
            if check_response.status_code == 200:
                self.log_signal.emit(f"仓库 {self.repo_owner}/{self.repo_name} 已存在,无需创建")
                return True

            # 仓库不存在,尝试创建
            self.log_signal.emit(f"仓库 {self.repo_owner}/{self.repo_name} 不存在,正在自动创建...")
            create_url = "https://api.github.com/user/repos"
            create_data = {
                "name": self.repo_name,
                "private": False,  # 创建公共仓库(如需私有,改为True)
                "auto_init": True,  # 自动初始化(创建README,避免空仓库)
                "description": "Auto-created by GitHub Upload Tool"
            }
            create_response = requests.post(create_url, json=create_data, headers=headers)

            if create_response.status_code == 201:
                self.log_signal.emit(f"仓库 {self.repo_name} 创建成功!")
                return True
            else:
                error_detail = create_response.json() if create_response.headers.get(
                    'Content-Type') == 'application/json' else create_response.text
                self.log_signal.emit(f"仓库创建失败: {error_detail}")
                return False
        except Exception as e:
            self.log_signal.emit(f"仓库检查/创建出错: {str(e)}")
            return False

    # 递归获取文件夹内所有文件
    def get_all_files(self, folder_path):
        file_list = []
        for root, dirs, files in os.walk(folder_path):
            for file in files:
                file_path = os.path.join(root, file)
                # 计算相对路径(保持文件夹结构)
                rel_path = os.path.relpath(file_path, folder_path)
                file_list.append((file_path, rel_path))
        self.total_files = len(file_list)
        return file_list

    # 单个文件上传逻辑
    def upload_single_file(self, local_file_path, github_file_path):
        try:
            # 读取并编码文件内容
            with open(local_file_path, "rb") as f:
                file_content = f.read()
            encoded_content = base64.b64encode(file_content).decode("utf-8")

            # 构建API请求URL
            api_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}/contents/{github_file_path}"
            headers = {
                "Authorization": f"token {self.github_token}",
                "Accept": "application/vnd.github.v3+json"
            }

            # 检查文件是否已存在(获取SHA)
            response = requests.get(api_url, headers=headers)
            data = {
                "message": f"Upload file: {os.path.basename(local_file_path)}",
                "content": encoded_content
            }
            if response.status_code == 200:
                data["sha"] = response.json()["sha"]

            # 发送上传请求
            response = requests.put(api_url, json=data, headers=headers)
            if response.status_code in [200, 201]:
                self.uploaded_files += 1
                progress = int(self.uploaded_files / self.total_files * 100) if self.total_files > 0 else 100
                self.progress_signal.emit(progress)
                return True, f"上传成功: {github_file_path}"
            else:
                error_detail = response.json() if response.headers.get(
                    'Content-Type') == 'application/json' else response.text
                return False, f"上传失败: {github_file_path} (状态码: {response.status_code})\n   错误详情: {error_detail}"
        except Exception as e:
            return False, f"文件出错: {local_file_path} - {str(e)}"

    def run(self):
        try:
            # 第一步:检查/创建仓库(核心新增逻辑)
            self.log_signal.emit("开始检查仓库状态...")
            if not self.create_github_repo():
                self.finish_signal.emit(False)
                return

            # 第二步:扫描文件
            self.log_signal.emit("开始扫描本地文件...")

            # 区分文件/文件夹处理
            if self.is_folder:
                # 文件夹上传:递归获取所有文件
                if not os.path.isdir(self.target_path):
                    raise NotADirectoryError(f"指定路径不是文件夹: {self.target_path}")

                file_list = self.get_all_files(self.target_path)
                if self.total_files == 0:
                    self.log_signal.emit("文件夹内未找到任何文件")
                    self.finish_signal.emit(True)
                    return

                self.log_signal.emit(f"共检测到 {self.total_files} 个文件,开始批量上传...")

                # 逐个上传文件
                for local_file, rel_path in file_list:
                    # 拼接仓库内完整路径(根路径 + 相对路径)
                    github_file_path = os.path.join(self.github_root_path, rel_path).replace("\\", "/")
                    success, msg = self.upload_single_file(local_file, github_file_path)
                    self.log_signal.emit(msg)

                    # 如果上传失败,记录但继续上传其他文件
                    if not success:
                        self.log_signal.emit(f"继续上传其他文件...")

                self.log_signal.emit(f"\n上传完成!总计 {self.total_files} 个文件,成功上传 {self.uploaded_files} 个")
                self.finish_signal.emit(self.uploaded_files == self.total_files)

            else:
                # 单个文件上传
                self.total_files = 1
                self.progress_signal.emit(0)
                github_file_path = os.path.join(self.github_root_path, os.path.basename(self.target_path)).replace("\\",
                                                                                                                   "/")
                success, msg = self.upload_single_file(self.target_path, github_file_path)
                self.log_signal.emit(msg)
                self.progress_signal.emit(100)
                self.finish_signal.emit(success)

        except Exception as e:
            self.log_signal.emit(f"整体流程出错: {str(e)}")
            self.finish_signal.emit(False)


# 主界面窗口
class GitHubUploader(QWidget):
    def __init__(self):
        super().__init__()
        self.upload_thread = None
        self.is_folder_mode = False  # 默认是文件模式
        self.init_ui()

    def init_ui(self):
        # 窗口基本设置
        self.setWindowTitle("GitHub 文件/文件夹上传工具")
        self.setGeometry(100, 100, 800, 600)

        # 整体布局
        main_layout = QVBoxLayout()
        main_layout.setSpacing(15)
        main_layout.setContentsMargins(20, 20, 20, 20)

        # 1. 选择模式(文件/文件夹)
        mode_group = QGroupBox("上传模式")
        mode_layout = QHBoxLayout()

        self.file_radio = QRadioButton("单个文件")
        self.folder_radio = QRadioButton("整个文件夹")
        self.file_radio.setChecked(True)  # 默认选中文件模式

        # 绑定模式切换事件
        self.file_radio.clicked.connect(self.switch_mode)
        self.folder_radio.clicked.connect(self.switch_mode)

        mode_layout.addWidget(self.file_radio)
        mode_layout.addWidget(self.folder_radio)
        mode_group.setLayout(mode_layout)
        main_layout.addWidget(mode_group)

        # 2. 路径选择区域
        path_group = QGroupBox("路径选择")
        path_layout = QHBoxLayout()
        self.path_edit = QLineEdit()
        self.path_edit.setPlaceholderText("请选择要上传的文件/文件夹路径")
        self.path_btn = QPushButton("选择文件")
        self.path_btn.clicked.connect(self.select_path)
        path_layout.addWidget(self.path_edit)
        path_layout.addWidget(self.path_btn)
        path_group.setLayout(path_layout)
        main_layout.addWidget(path_group)

        # 3. GitHub配置区域
        github_group = QGroupBox("GitHub 配置")
        github_layout = QVBoxLayout()

        # 仓库所有者
        owner_layout = QHBoxLayout()
        owner_layout.addWidget(QLabel("仓库所有者:"))
        self.repo_owner_edit = QLineEdit()
        self.repo_owner_edit.setPlaceholderText("必填:你的GitHub用户名(区分大小写)")
        owner_layout.addWidget(self.repo_owner_edit)
        github_layout.addLayout(owner_layout)

        # 仓库名称
        repo_layout = QHBoxLayout()
        repo_layout.addWidget(QLabel("仓库名称:"))
        self.repo_name_edit = QLineEdit()
        self.repo_name_edit.setPlaceholderText("必填:要创建/上传的仓库名(如test-upload)")
        repo_layout.addWidget(self.repo_name_edit)
        github_layout.addLayout(repo_layout)

        # 访问令牌
        token_layout = QHBoxLayout()
        token_layout.addWidget(QLabel("PAT令牌:"))
        self.token_edit = QLineEdit()
        self.token_edit.setPlaceholderText("必填:带repo权限的GitHub经典令牌")
        self.token_edit.setEchoMode(QLineEdit.Password)
        token_layout.addWidget(self.token_edit)
        github_layout.addLayout(token_layout)

        # 仓库内根路径
        root_layout = QHBoxLayout()
        root_layout.addWidget(QLabel("仓库内根路径:"))
        self.github_root_edit = QLineEdit()
        self.github_root_edit.setPlaceholderText("可选:如uploads/(默认自动填充)")
        self.github_root_edit.setText("uploads/")  # 默认根路径
        root_layout.addWidget(self.github_root_edit)
        github_layout.addLayout(root_layout)

        github_group.setLayout(github_layout)
        main_layout.addWidget(github_group)

        # 4. 操作按钮 + 进度提示
        op_layout = QHBoxLayout()
        self.upload_btn = QPushButton("开始上传")
        self.upload_btn.clicked.connect(self.start_upload)
        self.clear_btn = QPushButton("清空日志")
        self.clear_btn.clicked.connect(self.clear_log)
        self.progress_label = QLabel("进度: 0%")
        op_layout.addWidget(self.upload_btn)
        op_layout.addWidget(self.clear_btn)
        op_layout.addStretch()
        op_layout.addWidget(self.progress_label)
        main_layout.addLayout(op_layout)

        # 5. 日志输出区域
        log_group = QGroupBox("上传日志")
        log_layout = QVBoxLayout()
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)
        log_layout.addWidget(self.log_text)
        log_group.setLayout(log_layout)
        main_layout.addWidget(log_group)

        self.setLayout(main_layout)

    # 切换上传模式(文件/文件夹)
    def switch_mode(self):
        self.is_folder_mode = self.folder_radio.isChecked()
        if self.is_folder_mode:
            self.path_btn.setText("选择文件夹")
            self.path_edit.setPlaceholderText("请选择要上传的文件夹路径")
        else:
            self.path_btn.setText("选择文件")
            self.path_edit.setPlaceholderText("请选择要上传的文件路径")

    # 选择文件/文件夹路径
    def select_path(self):
        if self.is_folder_mode:
            # 选择文件夹
            folder_path = QFileDialog.getExistingDirectory(self, "选择文件夹")
            if folder_path:
                self.path_edit.setText(folder_path)
        else:
            # 选择文件
            file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "所有文件 (*.*)")
            if file_path:
                self.path_edit.setText(file_path)

    # 清空日志
    def clear_log(self):
        self.log_text.clear()
        self.progress_label.setText("进度: 0%")

    # 开始上传
    def start_upload(self):
        # 1. 校验输入
        target_path = self.path_edit.text().strip()
        repo_owner = self.repo_owner_edit.text().strip()
        repo_name = self.repo_name_edit.text().strip()
        token = self.token_edit.text().strip()
        github_root_path = self.github_root_edit.text().strip()

        if not target_path:
            tip = "请先选择要上传的文件夹!" if self.is_folder_mode else "请先选择要上传的文件!"
            QMessageBox.warning(self, "提示", tip)
            return
        if not repo_owner or not repo_name or not token:
            QMessageBox.warning(self, "提示", "仓库所有者、仓库名称、PAT令牌均为必填项!")
            return

        # 2. 初始化状态
        self.upload_btn.setEnabled(False)
        self.clear_log()
        self.log_text.append("📋 开始执行上传流程...")

        # 3. 创建并启动上传线程
        self.upload_thread = UploadThread(
            target_path=target_path,
            repo_owner=repo_owner,
            repo_name=repo_name,
            github_token=token,
            github_root_path=github_root_path,
            is_folder=self.is_folder_mode
        )
        self.upload_thread.log_signal.connect(self.append_log)
        self.upload_thread.progress_signal.connect(self.update_progress)
        self.upload_thread.finish_signal.connect(self.upload_finish)
        self.upload_thread.start()

    # 追加日志
    def append_log(self, msg):
        self.log_text.append(msg)
        # 自动滚动到最新日志
        self.log_text.verticalScrollBar().setValue(self.log_text.verticalScrollBar().maximum())

    # 更新进度
    def update_progress(self, progress):
        self.progress_label.setText(f"进度: {progress}%")

    # 上传完成
    def upload_finish(self, is_success):
        self.upload_btn.setEnabled(True)
        if is_success:
            QMessageBox.information(self, "成功", "上传完成!所有文件均成功上传到GitHub")
        else:
            QMessageBox.warning(self, "部分失败", "上传完成,但有步骤失败,请查看日志详情!")


# 程序入口
if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = GitHubUploader()
    window.show()
    sys.exit(app.exec_())

总结

  1. 该工具核心是通过 GitHub API 实现仓库自动创建和文件上传,结合 PyQt5 实现可视化操作;

  2. 使用前需准备带 repo 权限的 GitHub PAT 令牌,确保依赖包安装完整;

  3. 支持单文件/文件夹上传,保留目录结构,实时输出日志和进度,便于排查问题;

  4. 可通过 pyinstaller 打包为 exe 文件,提升易用性,也可扩展私有仓库、断点续传等功能。

相关推荐
小鸡吃米…2 小时前
TensorFlow——Keras 框架
人工智能·python·tensorflow·keras
懒惰的bit2 小时前
Python入门学习记录
python·学习
米羊1212 小时前
Spring 框架漏洞
开发语言·python
二十雨辰2 小时前
[python]-闭包和装饰器
python
大尚来也2 小时前
Python 调用 Ollama 本地大模型 API 完全指南
开发语言·python
henry1010102 小时前
通过GitHub Page服务免费部署静态Web网站
前端·html·github·html5
qq_24218863322 小时前
Python 春节贺卡代码
开发语言·python
Lenyiin2 小时前
《LeetCode 顺序刷题》11 -20
java·c++·python·算法·leetcode·lenyiin
Jelena157795857922 小时前
淘宝图搜API接口技术深度解析:从架构设计到工程实践
python·api