联通云盘批量上传 (WoPan bulk upload)
Upload files straight to 联通云盘 through its own API. This bypasses WebDAV,
which is typically 3-10x slower (a Cloudflare-hosted OpenList in front of
WoPan measured ~0.17 MB/s vs ~0.5 MB/s direct per connection).
When to use
- "把这些文件传到联通云盘" / "上传到沃盘" / "batch upload to Unicom Cloud Drive"
- A folder must land at a specific path inside 联通云盘 (e.g.
/OpenList/music) - OpenList/AList already mounts WoPan and WebDAV transfers are too slow
- The WoPan folder ID for a human-readable path must be resolved
Do not use this for other drives (夸克 / 百度 / 阿里) — only WoPan speaks this protocol.
Prerequisites
- An access token. Two ways to get one:
- From an existing OpenList/AList instance (preferred, needs no user action):
log into the OpenList API and read the storage config — see
references/protocol.md→ "Harvesting a token from OpenList".
This works whenever the user already has WoPan mounted there.
- By hand: log in at <https://pan.wo.cn/>, open devtools → Network,
find the request containingkey=PcLoginVerifyCode, and read
accessToken/refreshTokenfrom the response.
Note: logging into the web client afterwards may kick the driver offline;
the mobile app does not. pyaes(preferred) orcryptographyinstalled — used for the AES-128-CBC layer.
pip install pyaes. Installpypinyintoo if filenames may contain CJK.
Workflow
1. Resolve the target folder ID
WoPan addresses folders by opaque IDs, not paths. Walk the tree from the
personal root ("0"):
from wopan import WoPan
c = WoPan(access_token, refresh_token)
folder_id, created = c.resolve_path(["OpenList", "music", "Archive.org"])
# with create=True missing folders are made; returns the id of the deepest one
resolve_path returns (None, [...]) when a segment is missing and
create=False.
2. Normalise filenames
联通云盘 handles non-ASCII poorly and pinyin/ASCII names are usually wanted.
Run to_ascii() (in scripts/upload_batch.py) first: accents are folded
(Jörg → Jorg, Iñaki → Inaki) and CJK is transliterated via pypinyin.
3. Upload
python scripts/upload_batch.py \
--src "C:\OpenList\music" \
--target OpenList/music/Archive.org \
--storages path/to/storages.json # or $WOPAN_ACCESS_TOKEN
python scripts/upload_batch.py --src DIR --target A/B/C --create # mkdir as needed
python scripts/upload_batch.py --src DIR --target A/B/C --rename # ASCII/pinyin in place
WORKERS=1 python scripts/upload_batch.py ... # concurrency (capped at 2)
Credentials resolve in this order — first hit wins:
--storages PATH$WOPAN_STORAGES$WOPAN_ACCESS_TOKEN(+ optional$WOPAN_REFRESH_TOKEN)./storages.json
The script lists the destination folder first and skips names already
present, so re-running after a partial failure is safe — a verified re-run
against an already-complete folder prints uploading 0 file(s) and RESULT: PASS.
4. Verify
The script re-lists the cloud folder at the end and compares names and
sizes, both directions, then prints RESULT: PASS or FAIL. Treat FAIL
as failure. A non-empty extra on cloud= or SIZE MISMATCH block means the
folder drifted from the local source.
Concurrency: keep it at 1-2
The uplink to WoPan is fragile and slower than it looks. Measured on a real run
(50 lossless FLAC, ~585 MB): ~0.28-0.32 MB/s per connection, ~0.58 MB/s
aggregate at 2 workers, whole batch ≈ 20 min, 0 failures. Cap parallelism at
2; drop to 1 if transfers start failing. Concurrency is not the main failure
mode though — timeouts are, see below. Budget roughly total_MB / 0.58 seconds
and tell the user before starting a large batch.
Reporting back
After the final verify, it is worth emitting a small manifest (cloud listing
cross-checked against the local folder: counts, per-file size match, missing,
extra) plus a human-readable table of the batch. A "51 entries, missing=none"
line is far more convincing to the user than "done".
Common pitfalls
- Never pass a
(connect, read)timeout tuple to the upload POST.
This is the single biggest trap. In this environment requests/urllib3
applies the connect value as the socket write timeout, so any upload
that takes longer than that dies with
ConnectionError: ('Connection aborted.', TimeoutError('The write operation timed out'))
— and raising the read value changes nothing. The failure lands at exactly
the connect-timeout number of seconds, which is the tell.
Always use a scalar:timeout=3600. Verified: tuple(10,300)fails a
10 MB part at 10.4 s, scalar300completes it in 20.0 s. session.proxiescan be silently overridden by environment proxies.
requestsmergeshttp_proxy/https_proxyfrom the environment into the
request-level proxy dict, and request-level beats session-level. If a
sandbox injects a proxy, every API call fails with ProxyError whilecurl
works fine. The client setss.trust_env = False— keep that line.- The response
DATAfield is a quoted string in the raw wire format.
Go sees the quotes; a Pythonjsonparse has already stripped them, so
decide "plain JSON or encrypted blob?" by trying to parse, not by checking
for surrounding quotes. totalPartuses floor division, not ceil.totalPart = max(1, size // 8MiB)
and the final part absorbs the whole remainder, so a part may exceed 8 MiB.
Using ceil or a bounded final part size desynchronises the part count and the
server rejects the upload.- Token expiry surfaces as
RSP_CODE 9999. The client auto-refreshes once
via theapi-userAppRefreshTokenmethod when arefresh_tokenis available. - Access tokens are secrets. Never echo them into logs or the final reply.
Bundled resources
scripts/wopan.py— the client: AES layer, signed RPC dispatcher, chunked
upload,resolve_path,mkdir,move,delete.scripts/upload_batch.py— batch driver: token resolution, rename to ASCII,
skip existing, parallel upload, size-aware two-way verify.references/protocol.md— full wire protocol, endpoints, constants, and the
OpenList token-harvest recipe.
Verifying through OpenList (when the user has it mounted)
An upload confirmed by the WoPan API is not the same as the user seeing it.
If OpenList sits in front of the drive, confirm independently with a WebDAV
PROPFIND (Depth: 1) on the mount path — e.g. /dav/wopan/OpenList/music/Album
— and diff names and sizes against the local folder.
Gotcha: WebDAV hrefs come back percent-encoded, so %20 / %26 / %2C
must be unquote()d before comparing, or every file looks "missing" while the
listings actually agree. A naive check will report all names missing and all
names extra at the same time — that pairing is the tell.