# net-share **Repository Path**: HAYPO/net-share ## Basic Information - **Project Name**: net-share - **Description**: 基于 Python 的本地多协议文件共享服务,支持 HTTP / WebDAV / FTP / SFTP / DLNA 协议,提供 PySide6 图形界面,支持 Windows 一键打包为独立 exe。 - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-11-05 - **Last Updated**: 2026-11-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: Python, FTP, Http, PySide6 ## README # 内网文件共享 局域网里快速共享文件的小工具。**不复制文件**,只把路径加进分享列表,别人就能用浏览器、资源管理器、FTP 客户端或电视访问。 界面 PySide6,服务端 aiohttp(全异步);FTP 用 pyftpdlib,SFTP 用 asyncssh。五种协议都是**只读**。 ## 特性 - 自动读取网卡 IP,默认端口 `8080`,一键开启 / 停止 - 文件、文件夹拖进列表即可,随时移除(都不动本地文件) - **五种访问方式同时可用**:网页 / WebDAV / FTP / SFTP / DLNA,按需勾选 - 参数自动保存:地址、端口、列表、窗口大小、协议开关、界面主题,重开继续用 - **默认不自动开启分享**:打开程序后手动点「开启分享」;需要时再勾「启动时自动开启」 - 标题栏**主题下拉**切换浅色 / 深色;主题是外置模板文件(`netshare/themes/*.json` + `*.qss`),丢一份新主题到 `data/themes/` 即自动出现,粉色、黄色随便加,无需改代码或重新打包 - 深色下表格、日志、详情弹窗、tooltip 都做了配色适配 - 底部日志右侧四个按钮:**自动滚动**(可暂停,翻旧日志时不被新日志推走)、**跳到末尾**、**复制日志**、**清空日志**(只清窗口,磁盘日志保留) - 电视/路由器的无关 SSDP 探测按来源 IP 聚合,10 分钟只给一条提示 + 一条统计汇总(次数、ST 分布、推测设备类型),不刷屏 - 共享项失效(文件被移走、U 盘拔了)会标 `✕`,不会被悄悄删掉 - 点击列名排序(自然排序,`文件2` 在 `文件10` 前) - 支持大文件流式下载、断点续传(Range)、图片/PDF/视频在线预览 - **两层缓存**:内容层按对齐块缓存文件数据(默认 100 MB,0~4096 MB 可调,运行中改立即生效),五种协议共用,源文件一变自动失效;元数据层缓存目录扫描结果(网页 / WebDAV / FTP / SFTP / DLNA 共用,目录 mtime 一变就重扫) - **淘汰策略五选一**:LFU / LRU / LRU-K / SIEVE / 自动选择,界面「高级…」还能改准入次数、块大小、采样数、预读深度与元数据 TTL,全部即时生效 - **顺序预读**:识别到连续读大文件时提前读后面几块,投机块最多只占容量 1/4 且最先进淘汰,一遍过的大文件仍不会冲空缓存 - 右栏底部**内存指示器**:系统内存使用率 / 可用量 / 本进程 RSS / 缓存占用 / 命中率 / 合并回源 / 预读命中 / 目录缓存命中率,每秒刷新 - 全异步服务端,文件读取与目录扫描不占事件循环,缓存命中连线程池都不进;实测 10 / 30 / 50 人并发零失败 - 可打包成免安装单文件 exe,拷到别的 Windows 机器双击即用 ## 界面预览 桌面端:**左栏是连接配置,右栏是文件列表**,日志横跨底部、右侧带竖排工具按钮。选地址 → 勾协议 → 点「开启分享」,把文件或文件夹拖进右侧列表即可。两栏之间可以拖动分隔条调宽度;右栏较窄时「完整路径」列默认收起,在列表上右键可切回来。左栏滚动条只在内容放不下时出现。 ![主界面:分享中](docs/screenshots/main-window.png) 浏览器端:同一网络下的设备打开 `http://IP:8080`,默认跟随系统配色,也可用 `?theme=dark` 强制深色。 | 浅色主题 | 深色主题 | |---|---| | ![浅色主题](docs/screenshots/web-home-light.png) | ![深色主题](docs/screenshots/web-home-dark.png) | 文件夹可以逐层展开,图片 / PDF / 文本能在线预览: | 目录浏览 | 子目录与预览入口 | |---|---| | ![目录浏览](docs/screenshots/web-folder.png) | ![预览入口](docs/screenshots/web-preview.png) | 打包后的 exe(共享项失效时标 `✕`): ![打包后的 exe 窗口](docs/screenshots/exe-window.png) > 截图由 `tests/screenshot.py` / `tests/exe_shot.py` 生成,均为演示数据;本机用户名、主机名与内网 IP 已打码。重新生成配图:`python tools/mask_shots.py`。 ## 自定义主题 标题栏右上角的**主题下拉**列出所有可用主题,选择即切换,偏好写进 `config.json`。主题不写在代码里,而是模板文件: ``` netshare/themes/ light.json light.qss ← 内置浅色(也是其它主题的默认基底) dark.json dark.qss ← 内置深色,只写与浅色不同的覆盖规则 ``` 加一个粉色主题**不用改代码、不用重新打包**:在 exe 同级的 `data/themes/` 放两个文件(目录不存在就新建),重开程序,下拉里就多出「粉色」: ```json { "key": "pink", "label": "粉色", "order": 30, "dark": false, "base": "light", "qss": "pink.qss", "tooltip": {"background": "#4a2030", "foreground": "#ffeaf3", "border": "#a85070"}, "palette": {"Window": "#fff2f7", "WindowText": "#5c2138", "Base": "#ffffff"}, "colors": {"primary": "#e05c8a", "primary_hover": "#cf4f7c"} } ``` ```css /* data/themes/pink.qss —— 只写与浅色不同的部分 */ QMainWindow, QWidget#central { background: #fff2f7; } QFrame#card { border-color: #f5c9dc; } QPlainTextEdit#log { background: #fffafd; color: #7a3550; } ``` | 字段 | 含义 | |---|---| | `key` / `label` / `order` | 写进配置的值 / 下拉显示名 / 排序(小的在前) | | `dark` | 是否深色系,决定表格逐格前景色、协议说明富文本等硬编码颜色的明暗 | | `base` / `qss` | 继承哪套主题的 QSS(不写 `base` 则非 light 主题默认继承 `light`)/ 本主题样式文件名 | | `tooltip` | 同时喂给 QSS 和 `QToolTip.setPalette`,深色下不会出现白底白字 | | `palette` / `colors` | Qt 调色板角色 / 内联样式色板;**缺的键自动回退浅色**,只改几个色也能用 | 模板在每次切换主题时重新读盘,改完切走再切回来即可看到效果。同名 `key` 以 `data/themes/` 里的为准(可用来覆盖内置主题);也可用环境变量 `NETSHARE_THEME_DIR` 指定临时主题目录做预览。演示效果见 `tests/shot_theme_pink_demo.png`。 ## 五种协议 | 协议 | 地址 | 说明 | |---|---|---| | 网页 | `http://IP:8080` | 无需装任何软件,点击下载 | | WebDAV | `http://IP:8080/dav` | 映射成网络盘,像本地文件夹 | | FTP | `ftp://IP:2121` | 任意账号密码都能进(只读) | | SFTP | `sftp://IP:2222` | 默认 `netshare` / `netshare`,走 SSH 加密 | | DLNA | 电视自动发现「NetShare 媒体库」 | 只投放图片、音频、视频 | 各客户端的详细填法(nPlayer、FileZilla、WinSCP、电视盒子)见 **[docs/连接与排错.md](docs/连接与排错.md)**。 ## 快速开始 ```bash pip install -r requirements.txt python main.py ``` 也可以双击 `启动.bat`(会自动检查依赖)。 用起来就五步:选本机地址 → 勾协议 → 点「开启分享」 → 把文件拖进列表 → 把地址发给对方。 ## 打包 exe 双击 `打包.bat`,或: ```bash python -m pip install -r requirements-build.txt python build.py ``` 产物 `dist/NetShare.exe`(约 52 MB,单文件免安装)。想要启动快就用目录模式 `python build.py --onedir`。 目标机器不需要 Python、不需要管理员权限。配置和日志写在 **exe 旁边的 `data/`**,换个目录等于换一份干净配置。 ## 部署与并发 ### 目标机器不用装 Python `dist/NetShare.exe` 是 PyInstaller 单文件包,内嵌 CPython、PySide6、aiohttp、pyftpdlib、asyncssh 及各自的二进制依赖。**拷到另一台 Windows 10/11 x64 机器双击就能用**:不用装 Python、不用 pip、不用配环境变量、不用管理员权限。 - 首次启动会解压到临时目录,约 2~3 秒 - 配置和日志写在 **exe 同级的 `data/`**,整个目录拷走就是完整迁移;目录不可写时自动改到 `%LOCALAPPDATA%\NetShare\` - 首次监听端口时 Windows 会弹防火墙提示,选「允许访问」;错过了就点界面上的「⚠ 防火墙未放行」,那一步会申请一次 UAC 提权——**这是唯一需要管理员权限的动作,程序本身不需要** - 很老的 Windows 若提示缺 `VCRUNTIME140.dll`,装一次 VC++ 2015-2022 运行库即可,仍与 Python 无关 `tests/exe_test.py` 会刻意清空 `PYTHONPATH` / `PYTHONHOME` 等变量、把 `PATH` 缩到只剩系统目录再启动 exe,23 项功能(网页 / WebDAV / FTP / SFTP / DLNA / 主题模板加载)全部通过——这条保证有自动化验证兜底。 ### 10 个人同时访问 服务端基于 aiohttp 全异步:文件读取、目录扫描(`PROPFIND`、DLNA `Browse`)都放线程池,不阻塞事件循环;同时传输上限 256(`ShareServer.start(max_transfers=...)` 可调),五种协议共用一份只读 VFS,没有写锁竞争。 实测(本机回环,混合 64 KB / 2 MB / 20 MB 文件): | 并发 | 请求数 | 失败 | 吞吐 | P95 延迟 | |---|---|---|---|---| | 10 | 40 | 0 | 411.8 MB/s | 416 ms | | 30 | 120 | 0 | 453.6 MB/s | 1166 ms | | 50 | 200 | 0 | 423.0 MB/s | 2217 ms | 10 人混合协议压测(网页 / WebDAV PROPFIND / 自检页 / DLNA 描述 / DLNA Browse 同时打):150 个请求 0 失败。 ```bash python tests/stress_test.py 10 4 # 10 人同时下载 python tests/stress_test.py 50 4 # 50 人同时下载 python tests/stress_test.py 10 3 mixed # 10 人同时混用多协议 ``` > 真实局域网里瓶颈通常在带宽和磁盘而不是程序:千兆网约 110 MB/s,机械盘随机读更慢。10 人同时看片的实际上限取决于这两项,服务端本身的余量远大于此。 ### 两层缓存与超高并发 **内容层**按对齐块(默认 256 KB,「高级…」里可选 64 KB ~ 1 MB)保存热点文件内容,五种协议共用一份:网页 / WebDAV / DLNA 走 `_serve_file`,FTP / SFTP 走 `CachedFile`。缓存键带 `size + mtime_ns`,源文件被覆盖后旧块自动失效,响应头与内容取自同一次 stat。 **元数据层**缓存目录扫描结果(`iterdir` + 每条目 `stat`),网页目录页、WebDAV PROPFIND、DLNA Browse、FTP / SFTP 列目录共用同一份快照,默认存活 1.5 秒。有效性由三层兜住:目录 `mtime_ns` 一变立刻重扫、TTL 到点定时刷新、分享列表版本号一变整体作废。文件内容正确性不依赖这一层——`_serve_file` 每次请求都重新 stat,所以列表快照就算旧了也不会给错字节或错的 `Content-Length`。 #### 淘汰策略怎么选 | 策略 | 踢谁 | 适合 | |---|---|---| | LFU(默认) | 访问次数最少的(Morris 概率计数 + 分钟级衰减) | 大量小文件被反复访问:相册、公共目录多人轮着翻 | | LRU | 最久没被读到的 | 短期内反复拖动同一批视频;**不适合**一遍过的大文件,最容易把缓存冲空 | | LRU-K | 没读满 K 次的先踢,读满 K 次进保护区 | 电影和小文件混着用;K 就是「高级」里的准入次数 | | SIEVE | 从最老端扫,引用位为 1 就清零跳过、为 0 就淘汰 | 连接数多、想省 CPU:命中只打标记,不搬链表 | | 自动 | 看淘汰掉的块「准入后有没有被复用」:没复用多 → LRU-K(有扫描污染),复用多 → LFU(确有热点) | 不确定自己怎么用 | 策略只决定「内存满了踢谁」,不影响正确性,运行中随时可切,不用重启分享。界面右下角括号里显示的是**真正在跑**的策略(选「自动」时看到的会是 `LFU` / `LRU-K` 而不是 `AUTO`)。 真正保护 4K 电影的其实不是选哪个策略,而是**二次访问准入**:块第一次被读到只记热度、不占内存,第二次才真正缓存,所以 50 GB 视频顺序播一遍不会把缓存冲空。 #### 顺序预读 连续读到同一文件的第 2 块起,判断为顺序流,后台提前把后面几块读进来(默认 4 块,可设 0 关闭)。预读块有三个约束,避免它反过来把缓存冲空:单独限额只有容量的 1/4;标记为投机块,打分时最先被踢;一旦被真实请求读到就「转正」,正常参与淘汰。客户端一 seek 走就停止预读,不会越读越远。界面「预读 N/M 中用」是 M 块预读里真正被读到 N 块。 #### 合并回源(借鉴 nginx `proxy_cache_lock`) 多个客户端同时请求同一个还没缓存的块时,只让第一个请求真正读盘,其余请求等这一份结果,而不是各自读一遍。等不到(领读者失败 / 超过 5 秒)就自己读盘兜底,不会卡死;领读者被打断也会立刻放行等待者。目录快照同样如此,N 个人同一秒打开同一个相册只扫一次。实测 12 个客户端同时拉同一个 64 MB 文件(缓存未命中): | | 磁盘读 | P95 延迟 | |---|---|---| | 缓存关闭 | 768 MB(12 份) | 877 ms | | 缓存开启 | 64 MB(1 份,合并 2738 块) | 783 ms | 界面上「合并 N」就是被省掉的重复读块数。多人同时看同一部片子、下载器多线程分段拉同一个文件时最明显。 #### 高级参数 「缓存」行的**高级…** 按钮打开面板,改完点确定即时生效并写进 `data/config.json`:准入次数(1~16,同时是 LRU-K 的 K)、块大小(64/128/256/512/1024 KB,改动会清空缓存,因为块编号的含义变了)、淘汰采样数(对应 Redis `maxmemory-samples`)、LFU 计数系数与衰减速率、顺序预读块数、元数据缓存开关 / 存活时间 / 条目上限。所有边界与配置文件校验同一套,界面能选出来的值下次启动一定读得回来。「恢复默认」一键回到 `cache.py` 里的常量。 缓存关闭时容量输入框只灰显、**不禁用**:可以先填好数字再勾开关,不用两步解锁。 `tests/cache_load_test.py` 把同一组请求分别跑「缓存关闭 / 开启」,两轮都预热后对比。本机回环、1000 并发 × 4 轮(4000 请求/轮,混合 64 KB / 2 MB / 20 MB,缓存 64 MB,策略「自动」实际跑 LFU): | 指标 | 缓存关闭 | 缓存开启 | 变化 | |---|---|---|---| | 吞吐 | 161.7 MB/s | 187.4 MB/s | +15.9% | | QPS | 98.5 | 114.2 | +15.9% | | P50 延迟 | 1676 ms | 1356 ms | -19% | | P95 延迟 | 27.6 s | 22.2 s | -19.5% | | P99 延迟 | 35.1 s | 29.9 s | -14.7% | | 进程 CPU | 52.5 s | 41.0 s | -21.9% | | 读 IO 增量 | 6.4 GB | 0 B | 命中率 99.9% | 0 / 44 失败(1000 并发下少量连接被 listen backlog 拒掉,与缓存无关,两轮都留了 2% 余量),缓存占用 22.1 MB / 64 MB,节省读 7.8 GB,合并回源 17 块,预读 82 块全部被真实请求读到、0 块白读。指标不只吞吐:还统计 P50/P95/P99、进程 CPU、`ReadTransferCount` 增量、命中率、准入与淘汰数、预读有效率、元数据命中率与进程 RSS。 > 这是「同一批热点文件反复被访问」的理想场景。真实局域网里第一次访问仍要读盘,缓存也减不掉外部程序往同盘写文件产生的 IO——那种场景优先错峰、限速或换物理盘。 ```bash python tests/cache_test.py # 缓存 168 项:配置/准入/淘汰策略/预读/元数据/并发/一致性/单飞回源/Range/HTTP/目录页 python tests/cache_load_test.py # 默认 300 并发 × 6 轮 python tests/cache_load_test.py 1000 4 # 超高并发 A/B 对比 python tests/cache_load_test.py 500 8 lruk # 指定淘汰策略(lfu / lru / lruk / sieve / auto) ``` ## 列表右键菜单 在任意一行点右键:在资源管理器中打开 / 查看详细信息 / 复制完整路径 / 复制网页访问地址 / 移除共享。多选时统一处理。 ## 数据文件 ``` data/config.json 参数、分享列表、缓存设置,退出时自动保存 data/logs/netshare.log 运行日志,按天轮转,保留 7 天 ``` `config.json` 里的 `cache` 段可以直接手改,读回来时逐项校验,越界或写错一律回退默认: ```json "cache": { "enabled": true, "size_mb": 100, "policy": "lfu", "admission_hits": 2, "block_kb": 256, "sample_size": 16, "lfu_log_factor": 10.0, "lfu_decay": 1.0, "prefetch_blocks": 4, "meta_enabled": true, "meta_ttl_ms": 1500, "meta_entries": 512 } ``` `policy` 可选 `lfu` / `lru` / `lruk` / `sieve` / `auto`;`block_kb` 只能是 64 / 128 / 256 / 512 / 1024;`meta_ttl_ms` 设 0 等于关掉元数据缓存。 打包后位于 exe 同级目录;若程序目录不可写,自动改用 `%LOCALAPPDATA%\NetShare\`。删掉 `data/` 就恢复默认状态。 ## 端口 | 端口 | 用途 | |---|---| | 8080 | HTTP + WebDAV + DLNA(可改) | | 2121 | FTP 控制与数据(可改) | | 2222 | SFTP(可改) | | 50000-50100 | FTP 被动模式,防火墙需放行 | | 1900/UDP | DLNA 发现,固定不可改 | 首次监听时 Windows 会弹防火墙提示,选「允许访问」。若当时错过了,界面会提示「⚠ 防火墙未放行」,点一下即可修复。 ## 连不上怎么办 按顺序查,能覆盖绝大多数情况: 1. **手机浏览器打开 `http://IP:8080/`** —— 打不开就是网络层:防火墙、绑错网卡、不在同一网段 2. **打开 `http://IP:8080/__diag`** —— 自检页会逐项列出 OPTIONS / PROPFIND / GET 的结果,直接给结论 3. **看日志有没有对方的记录** —— 一条都没有说明包没进来;有记录但失败才是客户端配置问题 4. **界面上的「连不上?自检」** —— 一次性列出本机地址、端口监听、防火墙是否放行当前 exe `No route to host` 基本只有四种成因:客户端里存的是旧 IP、电脑网卡没拿到 IP、路由器开了 AP 隔离 / 访客网络、手机没连 WiFi。**iPhone 还要检查 `设置 → nPlayer → 本地网络` 是否打开**——iOS 14 起关掉这个开关会静默拦截所有内网连接,服务端日志一片空白。 完整排查流程、日志对照表、各客户端填法:[docs/连接与排错.md](docs/连接与排错.md) ## 安全说明 仅限内网使用。所有协议**只读**,不能上传、修改、删除、重命名。 - 网页与 WebDAV 默认匿名,需要时可在界面给 WebDAV 设密码 - FTP 等价匿名:任意账号密码均可进,只读 - SFTP 必须登录,默认 `netshare` / `netshare`,建议改掉 - 共享整块磁盘会让所有协议都能浏览整个分区,只共享需要的文件夹更稳妥 - 路径校验统一拦住 `..`、`%2e%2e`、反斜杠和指向外部的符号链接 ## 测试 ```bash python tests/smoke_test.py # 服务端 35 项:页面/下载/Range/中文名/越权/并发 python tests/gui_test.py # 界面 164 项:拖放/去重/排序/启停/右键菜单/自检/两栏/主题模板/端口判据/日志工具/缓存开关与高级面板 python tests/persist_test.py # 持久化 29 项:配置读写/重启恢复/失效标记 python tests/multiproto_test.py # 多协议 187 项:VFS/WebDAV/FTP/SFTP/DLNA/盘符根/SSDP 聚合 python tests/exe_test.py # 打包产物 23 项(需先打包,含主题模板加载检查) python tests/cache_test.py # 缓存 168 项:配置/准入/淘汰策略/预读/元数据/并发/一致性/单飞回源/Range/目录页 python tests/stress_test.py 150 4 # 并发压测:150 并发 × 4 次 python tests/stress_test.py 10 3 mixed # 10 人混合协议并发 python tests/cache_load_test.py 1000 4 # 超高并发缓存 A/B 对比(缓存开/关) ``` `multiproto_test.py` 用真实客户端(`ftplib`、`asyncssh`、PROPFIND、UDP M-SEARCH),不做 mock。 ## 目录结构 ``` main.py 程序入口 启动.bat / 打包.bat 双击启动 / 打包 build.py 打包逻辑 NetShare.spec PyInstaller 配置 netshare/ utils.py 网卡枚举、自然排序、MIME、格式化、内存/IO 指标 storage.py 配置与日志持久化 cache.py 只读块缓存(LFU/LRU/LRU-K/SIEVE/自动、准入、预读、钉住、一致性、统计)+ 目录元数据缓存 vfs.py 只读虚拟文件系统(五协议共用,目录扫描走元数据缓存) server.py HTTP:页面、目录浏览、下载、Range dav.py WebDAV(写方法一律 405) ftp_server.py FTP 服务 sftp_server.py SFTP 服务 dlna.py DLNA:SSDP 发现 + ContentDirectory gui.py PySide6 界面 theme.py 主题模板加载器(自动发现 / 继承 / 热加载) themes/ light、dark 两套 json + qss 模板 assets/ 图标、下拉箭头等资源 tests/ 各层测试与压测脚本 packaging/ exe 版本资源 tools/ 图标生成、截图脱敏 docs/ 连接与排错手册、界面截图 ``` ## 为什么没有 SMB / NFS Python 生态没有可用的 SMB2/3 服务端,NFS 需要自研 SunRPC + XDR,且 Windows 客户端要额外装组件。挂载场景用 WebDAV 已经能达到同样效果。