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

核心特性
-
可视化操作界面,无需命令行;
-
自动检测/创建 GitHub 仓库(不存在则创建,存在则直接使用);
-
支持单文件/整文件夹上传,保留文件夹原有目录结构;
-
实时上传进度显示 + 详细日志输出,便于排查问题;
-
支持文件覆盖(自动获取文件 SHA 值实现更新);
-
自定义仓库内上传根路径(如
uploads/)。
二、环境准备
1. 依赖安装
工具基于 Python 开发,需先安装以下依赖包:
python
pip install requests PyQt5
2. GitHub 个人访问令牌(PAT)准备
上传文件需要 GitHub 权限,需创建带 repo 权限的经典令牌:
-
打开 GitHub 设置:https://github.com/settings/tokens
-
点击「Generate new token (classic)」;
-
勾选
repo权限(仓库相关操作); -
设置令牌有效期(建议选「No expiration」,或按需设置);
-
生成令牌后立即复制保存(仅显示一次,丢失需重新创建)。
三、工具使用教程
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_())
总结
-
该工具核心是通过 GitHub API 实现仓库自动创建和文件上传,结合 PyQt5 实现可视化操作;
-
使用前需准备带
repo权限的 GitHub PAT 令牌,确保依赖包安装完整; -
支持单文件/文件夹上传,保留目录结构,实时输出日志和进度,便于排查问题;
-
可通过
pyinstaller打包为 exe 文件,提升易用性,也可扩展私有仓库、断点续传等功能。