2 Commits
Author SHA1 Message Date
clear 9daa64f81a Release v1.0.5 adaptive fast service 2026-09-27 18:17:05 +08:00
clear 964b8a470a Redesign UI and harden fast transfers 2026-07-31 21:21:43 +08:00
10 changed files with 2689 additions and 293 deletions

No files matched your search

+1
View File
@@ -13,6 +13,7 @@ venv/
build/
dist/
release/
*.spec
*.log
*.tmp
+34 -7
View File
@@ -1,5 +1,9 @@
# PGU-NetFileSyncWin 项目维护说明
当前应用版本:`v1.0.5`(2026-09-27)
本版本变更:极速服务 V3、CPU 自适应并发、HTTP 长连接、服务版本显示与旧版升级、连接断开后的 user linger、下载重试稳定性和项目文档整理。
## 项目定位
PGU-NetFileSyncWin 是一个面向 Windows 的 PyQt5 远程文件管理与高速下载工具。它用于连接 PGU 设备、浏览远程文件,并将文件或文件夹下载到 Windows 本机。
@@ -20,8 +24,11 @@ PGU-NetFileSyncWin 是一个面向 Windows 的 PyQt5 远程文件管理与高速
- `assets/pgu_netfilesync_icon.png`:Qt 窗口图标。
- `docs/images/main-window.png`:README 和 USER 使用的主界面截图。
- `tests/test_delete_worker.py`:永久删除的路径安全、分批和失败回退测试。
- `tests/test_commercial_ui.py`:商业化主界面、DPI、扫描默认值和状态栏集成测试。
- `tests/test_fast_service_recovery.py`:极速服务版本识别、重启恢复和修复状态测试。
- `tests/test_password_storage.py`:DPAPI 密码加密、迁移和清除测试。
- `tests/test_root_access.py`:`/root` 二次密码验证和符号链接绕过测试。
- `tests/test_unicode_paths.py`:中文远程路径、中文本地路径和极速 Range 协议测试。
- `tests/test_update.py`:版本比较、Release 解析、配置合并和更新脚本测试。
- `README.md`:项目概览。
- `USER.md`:用户说明书。
@@ -107,25 +114,36 @@ remote_path=/home/clear/Data
- 发现 `5901`:显示为 `PGU4`,主界面端口填 `22`。
- 仅发现 `8080`:显示为 `PGU5`,主界面端口填 `22`。
扫描入口只保留在 `设备与连接设置` 弹窗中。选择设备后:
- PGU4 自动填写用户 `clear`、对应密码和 `/home/clear/Data`。
- PGU5 自动填写用户 `root`、对应密码和 `/nvme-data/pgu_data`。
- 密码始终使用 `QLineEdit.Password` 掩码显示。
## 极速通道
核心常量:
```python
FAST_HTTP_PORT = 5924
FAST_HTTP_WORKERS = 12
FAST_HTTP_BIG_FILE_WORKERS = 4
FAST_HTTP_WORKERS = 8
FAST_HTTP_BIG_FILE_WORKERS = 3
FAST_HTTP_TIMEOUT = 45
FAST_SPLIT_THRESHOLD = 64 * 1024 * 1024
FAST_HTTP_TOKEN = "PGU4_SYNC_FAST_V1"
FAST_HTTP_TOKEN = "PGU4_SYNC_FAST_V2"
FAST_SERVICE_VERSION = "3"
```
策略:
- 小文件按文件并发,最多 12 路。
- 连接时同时检测 Windows 本机与远端设备在线 CPU 线程数,总并发自动取两者较小值且不保存。
- 小文件按文件并发,最多 8 路,并复用每个下载线程的 HTTP 连接,优先保证吞吐稳定。
- 大文件超过 64MB 后使用 Range 分片。
- 单个大文件最多 4 路分片。
- 分片失败自动重试。
- 单个大文件最多 3 路分片,避免远端磁盘和HTTP线程争用。
- 分片失败在块内断点续传并自动重试,避免从块头重复下载。
- 极速HTTP使用45秒读取超时,避免短暂磁盘/网络抖动被过早判定失败。
- 失败汇总只在任务结束后提示。
- V3 按远端逻辑 CPU 数量建立并发槽位,上限为 `min(16, CPU线程数)` 且至少 2 路;同时扩大连接等待队列并启用 HTTP/1.1 长连接。路径仍使用 UTF-8 URL-safe Base64,支持中文目录、中文文件名和空格。
## 远端极速服务
@@ -148,6 +166,15 @@ FAST_HTTP_TOKEN = "PGU4_SYNC_FAST_V1"
crontab @reboot
```
极速服务脚本带持久版本标记,并在权限允许时启用 systemd user linger,避免 SSH 会话结束后服务退出。连接设备时:
- 当前 V3 服务正常:直接启用极速通道并刷新自启配置,GUI 显示 `极速 v3`。
- 检测到旧版本时显示实际版本,例如 `极速 v2 · 可升级`,按钮提供 `升级极速`。
- 当前 V3 已安装但未运行:自动清理残留进程并重启,不显示升级。
- 自动恢复失败:显示 `修复极速`。
- 仅脚本确实为旧版本时显示 `升级极速`。
- crontab 清理必须匹配 `pgu4_fast_http.py.*--port 5924`,避免带引号的旧命令累积。
关闭 Windows GUI 不会停止 PGU 设备上的极速服务。
## 自动更新
@@ -155,7 +182,7 @@ crontab @reboot
当前应用版本由 `APP_VERSION` 定义。打包后的 EXE 启动后会静默访问 Gitea `releases/latest`:
- 没有新版本、请求超时或接口异常时不提示。
- 主界面右上角提供 `检查更新` 按钮;手动检查会明确提示最新版或连接失败。
- 左侧底部提供 `检查更新` 按钮;手动检查会明确提示最新版或连接失败。
- 仅接受 `107.173.157.41:3000` 上名为 `PGU-NetFileSyncWin.exe` 的附件。
- 发现更高版本后询问用户,确认后显示下载进度。
- 新版 INI 只补充当前 INI 缺失的键,绝不覆盖已有连接配置。
+17 -3
View File
@@ -7,18 +7,32 @@ PGU-NetFileSyncWin 是一个 Windows 桌面文件同步工具,用于连接 PGU
## 主要功能
- 连接 PGU 设备并浏览远程文件。
- 支持局域网扫描,并区分 PGU4 与 PGU5 设备。
- 支持在连接设置中扫描局域网,并区分 PGU4 与 PGU5 设备。
- 扫描选择设备后自动填写对应账号、密码、SSH 端口和默认远程路径。
- 支持多选、框选、空白区域取消选择。
- 支持目录前进、后退、搜索和常用键盘快捷键。
- 支持拖拽已选文件到 Windows 资源管理器下载。
- 支持“下载到...”选择本地目录。
- 支持“下载”选择本地目录。
- 支持远端批量永久删除文件及非空目录,不经过回收站,并显示删除进度。
- 支持暂停、继续、取消和失败自动重试。
- 支持极速通道,提高大量小文件下载速度。
- 支持远端极速服务安装、升级、停止和开机自启。
- 支持中文远程目录、中文文件名和中文 Windows 目标目录。
- 支持远端极速服务安装、升级、修复、停止和开机自启。
- 重启 PGU 设备后会自动恢复当前版本极速服务,清理重复旧自启项。
- 启动后静默检查 Gitea 新版本,并支持自动升级和重启。
- 使用速度条显示实时速度,下载完成后显示用时和平均速度。
- 使用同目录 `PGU-NetFileSyncWin.ini` 保存连接配置,便于和 exe 一起复制使用。
## 当前版本
当前应用版本:`v1.0.5`
本版本重点修复极速下载稳定性:客户端会同时考虑 Windows 本机和 PGU 设备的 CPU 线程数;极速服务按远端 CPU 建立并发槽位,支持 HTTP 长连接、连接队列扩容、服务版本显示和旧版一键升级。
- 高速通道区域显示已安装的极速服务版本,例如 `极速 v3`。
- 检测到旧版时显示实际版本,并提供 `升级极速`。
- 大量小文件下载复用连接,减少速度波动和重复重试。
## 发布版
推荐使用发布页中的文件:
+39 -16
View File
@@ -47,16 +47,17 @@ IP、端口、用户名和远程路径可以直接用记事本修改。密码请
## 3. 连接 PGU 设备
1. 打开软件。
2. 检查 IP、端口、用户和密码是否正确。
3. 点击 `连接`。
4. 连接成功后,按钮会变成 `已连接`。
5. 文件列表会显示 PGU 设备中的远程目录。
2. 点击左侧的 `设备与连接设置`。
3. 检查 IP、端口、用户名和密码,点击 `保存并连接`。
4. 也可以直接点击左侧 `连接设备`,使用已经保存的连接信息。
5. 连接成功后,按钮会变成 `断开连接`。
6. 文件列表会显示 PGU 设备中的远程目录。
如果不知道 PGU 设备 IP,可以点击 `扫描`。
如果不知道 PGU 设备 IP,请进入 `设备与连接设置` 后点击 `扫描局域网设备`。
## 4. 扫描 PGU 设备
点击 `扫描` 后,会出现扫描窗口。
点击 `设备与连接设置`,再点击 `扫描局域网设备`。
操作步骤:
@@ -64,14 +65,19 @@ IP、端口、用户名和远程路径可以直接用记事本修改。密码请
2. 开始扫描。
3. 在结果列表中选择 PGU 设备。
4. 点击使用选中的 IP。
5. 回到主界面后点击 `连接`。
5. 返回连接设置后点击 `保存并连接`。
扫描会检查 `5901`、`5923` 和 `8080` 端口,并在结果中标明设备类型:
- 开放 `5901` 或 `5923`:显示为 `PGU4`。
- 开放 `8080`:显示为 `PGU5`。
选择设备后,软件会自动填写 IP 和对应连接端口。
选择设备后,软件会自动填写:
- PGU4:用户 `clear`,密码自动填写,默认路径 `/home/clear/Data`。
- PGU5:用户 `root`,密码自动填写,默认路径 `/nvme-data/pgu_data`。
密码始终使用掩码显示,保存后由 Windows DPAPI 加密。
## 5. 浏览远程文件
@@ -85,9 +91,21 @@ IP、端口、用户名和远程路径可以直接用记事本修改。密码请
常用操作:
- 双击文件夹:进入该文件夹。
- 点击 `上级`:返回上一层。
- 点击 `刷新`:重新读取当前目录。
- 点击向上箭头:返回上一层。
- 点击刷新图标:重新读取当前目录。
- 点击前进、后退图标:切换访问过的目录。
- 在远程路径输入框中输入路径后按回车:进入指定目录。
- 使用右上角搜索框:筛选当前目录。
常用快捷键:
- `Ctrl+F`:搜索。
- `F5`:刷新。
- `Alt+Left` / `Alt+Right`:后退、前进。
- `Alt+Up`:上级目录。
- `Enter`:打开选中的文件夹。
- `Delete`:永久删除选中项。
- `Esc`:清除搜索或选择。
访问 `/root` 或其子目录时,软件会要求再次输入当前 SSH 连接密码。密码错误或取消验证时不会进入该目录。离开 `/root` 后,再次进入需要重新验证。
@@ -106,7 +124,7 @@ IP、端口、用户名和远程路径可以直接用记事本修改。密码请
### 方法一:下载到指定目录
1. 在文件列表中选择文件或文件夹。
2. 点击 `下载到...`。
2. 点击 `下载`。
3. 选择本机目录。
4. 软件开始下载。
@@ -122,7 +140,7 @@ IP、端口、用户名和远程路径可以直接用记事本修改。密码请
## 8. 删除远程内容
选中一个或多个文件、目录后点击鼠标右键,选择 `删除选中项`。
选中一个或多个文件、目录后点击鼠标右键,选择 `永久删除选中项`。
软件会先弹窗要求二次确认。确认后,PGU 设备会在本机文件系统中批量永久删除选中内容,非空目录会连同内部文件一起删除,不会进入远程回收站。弹窗会显示选中项删除进度,完成后远程目录会自动刷新。
@@ -132,15 +150,20 @@ IP、端口、用户名和远程路径可以直接用记事本修改。密码请
## 9. 极速下载
底部按钮会显示当前极速服务状态:
左侧高速通道区域会显示当前极速服务状态:
- `极速 v3`:当前已安装并运行 V3 极速服务。
- `安装极速`:PGU 设备上还没有极速服务,点击后安装。
- `极速 v2 · 可升级`:检测到旧版本,点击 `升级极速` 后更新。
- `升级极速`:检测到旧服务,点击后升级。
- `修复极速`:当前版本已安装但自动启动失败,点击后修复。
- `停止极速`:极速服务正在运行,点击后停止。
一般情况下,连接 PGU 设备后,如果极速服务可用,软件会自动使用极速通道下载。
安装极速服务后,PGU 设备重启后也会尽量自动启动该服务。关闭 Windows 软件不会停止 PGU 设备上的极速服务。
安装极速服务后,PGU 设备重启时会通过 systemd 和单条 crontab 备用项自动启动。软件会清理重复旧自启命令;当前版本没有运行时会先自动恢复,不会误报为旧版本。
极速 V3 支持中文远程目录、中文文件名、空格以及中文 Windows 下载目录;同时按远端 CPU 数量限制服务并发,使用 HTTP 长连接减少大量小文件下载时的波动。
## 10. 下载过程
@@ -175,7 +198,7 @@ IP、端口、用户名和远程路径可以直接用记事本修改。密码请
- 没有新版本或网络不可用:不弹窗,正常使用。
- 发现新版本:弹窗询问是否立即升级。
- 确认升级:自动下载新版并重启软件。
- 也可以点击主界面右上角的 `检查更新` 手动检查。
- 也可以点击左侧底部的 `检查更新` 手动检查。
升级时会保留现有 IP、端口、账号、密码和远程路径。新版配置文件中新增的配置项可以自动补入,但已有配置值不会被覆盖。
@@ -199,7 +222,7 @@ Windows 可能缓存旧图标。请确认运行的是新版 exe。如果任务
### 想停止 PGU 设备上的极速服务
连接 PGU 设备后,点击底部 `停止极速`。
连接 PGU 设备后,点击左侧高速通道区域的 `停止极速`。
### 软件关闭后 PGU 设备上的极速服务还在
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 62 KiB

+2162 -265
View File
File diff suppressed because it is too large. Load diff
+173
View File
@@ -0,0 +1,173 @@
import os
import stat
import unittest
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QApplication, QStatusBar
from pgu4_sync import ConnectionDialog, MainWindow, RemoteEntry, adaptive_worker_count
class CommercialUiTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.app = QApplication.instance() or QApplication([])
def test_filtered_selection_maps_to_source_entry(self):
window = MainWindow()
self.addCleanup(window.close)
window.remote_model.set_entries(
[
RemoteEntry(
"alpha",
"/data/alpha",
0,
1,
stat.S_IFDIR | 0o755,
),
RemoteEntry(
"beta.txt",
"/data/beta.txt",
100,
1,
stat.S_IFREG | 0o644,
),
RemoteEntry(
"gamma.txt",
"/data/gamma.txt",
200,
1,
stat.S_IFREG | 0o644,
),
]
)
window.search_edit.setText("gamma")
self.assertEqual(1, window.remote_proxy.rowCount())
index = window.remote_proxy.index(0, 1)
window.remote_table.selectionModel().select(
index,
QItemSelectionModel.Select | QItemSelectionModel.Rows,
)
selected = window.selected_remote_entries()
self.assertEqual(["/data/gamma.txt"], [entry.path for entry in selected])
def test_disconnect_resets_browser_state(self):
window = MainWindow()
self.addCleanup(window.close)
window._connected = True
window.remote_model.set_entries(
[
RemoteEntry(
"file.txt",
"/data/file.txt",
100,
1,
stat.S_IFREG | 0o644,
)
]
)
window.disconnect_remote()
self.assertFalse(window._connected)
self.assertEqual(0, window.remote_model.rowCount())
self.assertEqual("尚未连接", window.item_count_label.text())
self.assertEqual("连接设备", window.connect_btn.text())
def test_adaptive_font_and_disconnected_controls(self):
window = MainWindow()
self.addCleanup(window.close)
self.assertGreaterEqual(window.font().pointSizeF(), 10.5)
self.assertFalse(window.remote_path_edit.isEnabled())
self.assertFalse(window.search_edit.isEnabled())
self.assertFalse(window.refresh_btn.isEnabled())
self.assertFalse(window.download_btn.isEnabled())
def test_path_history_truncates_forward_branch(self):
window = MainWindow()
self.addCleanup(window.close)
window._connected = True
window.record_history_path("/home")
window.record_history_path("/home/clear")
window.record_history_path("/home/clear/Data")
window._history_index = 1
window.record_history_path("/tmp")
self.assertEqual(["/home", "/home/clear", "/tmp"], window._path_history)
self.assertEqual(2, window._history_index)
def test_connection_password_cannot_be_revealed(self):
dialog = ConnectionDialog(
"192.168.1.248",
22,
"clear",
"secret-password",
)
self.addCleanup(dialog.close)
self.assertEqual(dialog.pass_edit.Password, dialog.pass_edit.echoMode())
self.assertFalse(hasattr(dialog, "show_password"))
def test_main_window_has_single_scan_entry_point(self):
window = MainWindow()
self.addCleanup(window.close)
self.assertFalse(hasattr(window, "scan_btn"))
self.assertEqual("PGU NetFileSync", window.windowTitle())
dialog = ConnectionDialog("", 22, "", "", window)
self.addCleanup(dialog.close)
self.assertTrue(dialog.scan_btn.isVisibleTo(dialog))
def test_status_messages_are_integrated_into_file_footer(self):
window = MainWindow()
self.addCleanup(window.close)
self.assertEqual([], window.findChildren(QStatusBar))
window.statusBar().showMessage("Unicode 状态:中文目录", 0)
self.assertEqual("Unicode 状态:中文目录", window.activity_label.text())
def test_scanned_pgu4_fills_connection_defaults(self):
dialog = ConnectionDialog("", 22, "", "")
self.addCleanup(dialog.close)
dialog.set_scanned_device("192.168.1.40", 5922, "PGU4")
self.assertEqual("192.168.1.40", dialog.host_edit.text())
self.assertEqual(5922, dialog.port_spin.value())
self.assertEqual("clear", dialog.user_edit.text())
self.assertEqual("Bone1644", dialog.pass_edit.text())
self.assertEqual("/home/clear/Data", dialog.selected_remote_path)
def test_scanned_pgu5_fills_connection_defaults(self):
dialog = ConnectionDialog("", 22, "", "")
self.addCleanup(dialog.close)
dialog.set_scanned_device("192.168.1.50", 22, "PGU5")
self.assertEqual("192.168.1.50", dialog.host_edit.text())
self.assertEqual(22, dialog.port_spin.value())
self.assertEqual("root", dialog.user_edit.text())
self.assertEqual("Clear8492..", dialog.pass_edit.text())
self.assertEqual("/nvme-data/pgu_data", dialog.selected_remote_path)
def test_worker_count_uses_smaller_local_or_remote_cpu_count(self):
self.assertEqual(8, adaptive_worker_count(32, 8))
self.assertEqual(6, adaptive_worker_count(6, 8))
self.assertEqual(32, adaptive_worker_count(32, 0))
def test_fast_service_version_is_visible_and_upgradeable(self):
window = MainWindow()
self.addCleanup(window.close)
window.set_fast_button_state("stop", "3")
self.assertEqual("极速 v3", window.fast_status_badge.text())
window.set_fast_button_state("upgrade", "2")
self.assertEqual("极速 v2 · 可升级", window.fast_status_badge.text())
+71
View File
@@ -0,0 +1,71 @@
import inspect
import unittest
from pgu4_sync import (
FAST_HTTP_SERVER_SCRIPT,
CheckFastServiceWorker,
FastServiceWorker,
)
class FastServiceRecoveryTests(unittest.TestCase):
def make_worker(self):
return CheckFastServiceWorker(None, "192.0.2.1", 5924, "token")
def test_current_service_is_auto_restarted_after_reboot(self):
worker = self.make_worker()
worker._healthcheck = lambda: False
worker._installed_service_state = lambda: "current"
worker._restart_current_service = lambda: True
available = []
legacy = []
repair = []
worker.available.connect(lambda *args: available.append(args))
worker.legacy.connect(legacy.append)
worker.repair.connect(repair.append)
worker.run()
self.assertEqual(1, len(available))
self.assertEqual([], legacy)
self.assertEqual([], repair)
def test_current_service_reports_repair_not_upgrade_when_restart_fails(self):
worker = self.make_worker()
worker._healthcheck = lambda: False
worker._installed_service_state = lambda: "current"
worker._restart_current_service = lambda: False
legacy = []
repair = []
worker.legacy.connect(legacy.append)
worker.repair.connect(repair.append)
worker.run()
self.assertEqual([], legacy)
self.assertEqual(1, len(repair))
def test_only_old_script_reports_upgrade(self):
worker = self.make_worker()
worker._healthcheck = lambda: False
worker._installed_service_state = lambda: "legacy"
legacy = []
repair = []
worker.legacy.connect(legacy.append)
worker.repair.connect(repair.append)
worker.run()
self.assertEqual(1, len(legacy))
self.assertEqual([], repair)
def test_service_script_has_persistent_version_marker(self):
self.assertIn('PGU_FAST_SERVICE_VERSION = "3"', FAST_HTTP_SERVER_SCRIPT)
self.assertIn('protocol_version = "HTTP/1.1"', FAST_HTTP_SERVER_SCRIPT)
self.assertIn("request_queue_size", FAST_HTTP_SERVER_SCRIPT)
self.assertIn("BoundedSemaphore", FAST_HTTP_SERVER_SCRIPT)
self.assertIn("X-PGU-Worker-Limit", FAST_HTTP_SERVER_SCRIPT)
source = inspect.getsource(FastServiceWorker)
self.assertIn("recv_exit_status", source)
self.assertIn("pgu4_fast_http.py.*--port", source)
self.assertIn("enable-linger", source)
+190
View File
@@ -0,0 +1,190 @@
import io
import http.client
import os
import socket
import subprocess
import sys
import tempfile
import threading
import time
import unittest
import urllib.parse
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt5.QtWidgets import QApplication
from pgu4_sync import (
DownloadJob,
DownloadSignals,
FAST_HTTP_SERVER_SCRIPT,
FileTask,
TransferController,
decode_fast_remote_path,
encode_fast_remote_path,
normalize_local_filename,
)
class RemoteBytes(io.BytesIO):
def set_pipelined(self, _enabled):
return None
class FakeSftp:
def __init__(self, expected_path, payload):
self.expected_path = expected_path
self.payload = payload
def open(self, path, mode):
if mode != "rb":
raise AssertionError(mode)
if path != self.expected_path:
raise AssertionError(path)
return RemoteBytes(self.payload)
def close(self):
return None
class FakeTransport:
def set_keepalive(self, _seconds):
return None
class FakeClient:
def __init__(self, sftp):
self.sftp = sftp
def get_transport(self):
return FakeTransport()
def open_sftp(self):
return self.sftp
def close(self):
return None
class FakeFactory:
def __init__(self, sftp):
self.sftp = sftp
def open_client(self):
return FakeClient(self.sftp)
class UnicodePathTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.app = QApplication.instance() or QApplication([])
def test_fast_remote_path_round_trip(self):
remote_path = "/home/clear/中文源目录/图像 01.jpg"
encoded = encode_fast_remote_path(remote_path)
self.assertTrue(encoded.isascii())
self.assertNotIn("中文", encoded)
self.assertEqual(remote_path, decode_fast_remote_path(encoded))
def test_local_filename_preserves_chinese_and_normalizes_unicode(self):
decomposed = "Cafe\u0301_中文数据"
normalized = normalize_local_filename(decomposed)
self.assertEqual("Café_中文数据", normalized)
def test_sftp_download_supports_chinese_source_and_target(self):
payload = "中文路径传输正常".encode("utf-8")
remote_path = "/home/clear/中文源目录/实验数据.bin"
sftp = FakeSftp(remote_path, payload)
signals = DownloadSignals()
controller = TransferController()
job = DownloadJob(
FakeFactory(sftp),
[],
Path("."),
1,
signals,
controller,
)
with tempfile.TemporaryDirectory(prefix="PGU-中文目标-") as temp_dir:
local_path = Path(temp_dir) / "目标文件夹" / "实验数据.bin"
local_path.parent.mkdir(parents=True)
local_path.write_bytes(b"\x00" * len(payload))
file_task = FileTask(remote_path, local_path, len(payload))
job._file_locks[local_path] = threading.Lock()
job._download_chunk_sftp_once(file_task, 0, len(payload))
self.assertEqual(payload, local_path.read_bytes())
def test_fast_http_service_reads_chinese_path_with_range(self):
payload = "极速中文路径".encode("utf-8")
token = "unicode-test-token"
with tempfile.TemporaryDirectory(prefix="PGU-极速中文-") as temp_dir:
root = Path(temp_dir)
source = root / "中文源文件夹" / "图像 数据.bin"
source.parent.mkdir(parents=True)
source.write_bytes(payload)
script = root / "fast_service.py"
script.write_text(FAST_HTTP_SERVER_SCRIPT, encoding="utf-8")
with socket.socket() as probe:
probe.bind(("127.0.0.1", 0))
port = probe.getsockname()[1]
process = subprocess.Popen(
[
sys.executable,
str(script),
"--host",
"127.0.0.1",
"--port",
str(port),
"--token",
token,
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
self.addCleanup(process.kill)
deadline = time.time() + 4
while time.time() < deadline:
try:
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=0.3)
conn.request(
"GET",
"/health?token=" + urllib.parse.quote(token, safe=""),
)
response = conn.getresponse()
if response.status == 200 and response.read() == b"ok-v3":
conn.close()
break
conn.close()
except OSError:
time.sleep(0.05)
else:
self.fail("fast HTTP test service did not start")
encoded_path = encode_fast_remote_path(str(source))
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=2)
request_path = (
"/file?token="
+ urllib.parse.quote(token, safe="")
+ "&path_b64="
+ urllib.parse.quote(encoded_path, safe="")
)
conn.request("GET", request_path, headers={"Range": "bytes=3-8"})
response = conn.getresponse()
body = response.read()
conn.close()
self.assertEqual(206, response.status)
self.assertEqual(payload[3:9], body)
process.terminate()
process.wait(timeout=2)
+2 -2
View File
@@ -35,10 +35,10 @@ class UpdateTests(unittest.TestCase):
self.assertEqual([True], finished)
def test_version_comparison(self):
self.assertTrue(is_newer_version("1.0.4", APP_VERSION))
self.assertTrue(is_newer_version("1.0.6", APP_VERSION))
self.assertTrue(is_newer_version("v1.1.0", "1.0.9"))
self.assertTrue(is_newer_version("1.0.10", "1.0.2"))
self.assertFalse(is_newer_version("v1.0.2", APP_VERSION))
self.assertFalse(is_newer_version("v1.0.4", APP_VERSION))
self.assertFalse(is_newer_version("1.0", "1.0.0"))
self.assertFalse(is_newer_version("invalid", APP_VERSION))