Files
magpie/internal/davsync
yetone 7ab91961d2 davsync: an unchanged file on the server costs a request, not a download (Crispin on Discord: 是不是Magpie后台一直在自动同步,导致被坚果云给限制了) — every 3 minutes sync read the whole sealed file even when nothing had changed, and 坚果云's free plan caps monthly downloads and requests per 30 minutes. The sync state now keeps the server's version beside its hash (its ETag, or a Last-Modified at least a second older than the server's Date, so a write in that same second can't go unseen), from the read or from the ETag a write answers with (none said: the next sync reads in full, as before); the next read sends If-None-Match (or If-Modified-Since) and a 304 with nothing changed here ends the sync, while a change here merges with a copy of the server's file kept beside sync-state.json (sync-server.magpie-backup, 0600, checked against the hash; gone or different, the file is read again), so it isn't read twice. A server that ignores the condition sends the file and syncs exactly as today. S3 does the same with If-None-Match on GetObject, signed with the rest (the fake S3 checks every signature). A 429 or 503 (S3's SlowDown) now says the server is limiting how often magpie may reach it, and Run waits as long as Retry-After says (up to 6 h) or else doubles from 6 up to 30 minutes, back to 3 after a sync that goes through; log de-dup is kept, as the message doesn't carry the wait. The 403 paths (there, forbidden) only run when the server answers 403, so a working setup costs one conditional GET per cycle. TestSyncUnchangedIsNotRead (ETag, ETag not on PUT, Last-Modified: three unchanged cycles read 0 bytes, a server change is read once and merged, a change here is pushed without a read, a missing copy is read again), TestSyncRateLimited and TestS3Unchanged fail without the change (with the conditions dropped: 3 full reads of 1932 bytes, 'HTTP 429' with no backoff, unconditional S3 GETs); TestSyncIgnoringConditions guards servers without conditional reads. go vet ./internal/..., go build -tags nogui ./... (darwin, GOOS=linux), GOOS=windows go build ./..., go test -tags nogui ./internal/davsync/ ./internal/gui/ pass.
2026-10-02 11:08:14 +08:00
..
gateway: a request archive, off unless turned on, keeps each call's headers and bodies in the user's own S3 bucket and a recent call reads its own back (Jorben on Discord: 请求存档) — the Gateway page's recent calls only ever held the last bodies in memory, the first 256 KB of each, with no headers, gone at a restart. A switch over Recent calls ("Request archive", POST /api/settings/archive, settings' requestArchive) turns it on, refused with a reason while sync isn't set up with an s3:// bucket; with it on, each call the gateway serves gets an id (UTC HHMMSS-<16 hex>, said back in X-Magpie-Archive-Id) and, once it is recorded, its method, path and query, request headers and body and response status, headers and body go as one JSON file to <prefix>/magpie/archive/<YYYY-MM-DD>/<id>.json in the bucket sync keeps its backup in, signed with the same client (davsync.Bucket, no new SDK). Before anything leaves, Authorization, x-api-key, Cookie, Set-Cookie, any header or JSON field or query parameter named for a key, token, secret or password, and anything that looks like a key or token anywhere in the text (the redaction rules' patterns, in a stream's data lines too) become [REDACTED] — gone, no placeholder kept; the archive is plain JSON, readable in the bucket, not sealed with the passphrase. Uploads go one at a time on their own goroutine from a queue of 16 calls, each body capped at the 256 KB the call already keeps and marked truncated; a full queue drops the call with a log line, a failed upload is logged and shown under the switch until the next one goes, and no request waits on any of it. A recent call the archive kept shows its date and id under its bodies, and "Fetch from archive" (GET /api/archive?date=&id=, only a date and an id of that shape) shows the request's and response's headers and bodies as the bucket has them, or says it isn't there yet. The hook is in record() and the Call literal, so PR #239's request log lines are not touched; the archived call carries its usage, and with #239 its vendor request id. Only calls through serve() are archived, not Codex's backend, image or video calls. TestArchiveOn, TestArchiveOff, TestArchiveName, TestRequestArchiveToS3 (a real gateway request through the signature-checking fake S3), TestScrub and request-archive.test.cjs fail without the change; TestSettingsSaveKeepsWhatItDoesNotSend now sets requestArchive. A sandbox magpie web with a fake vendor and a fake S3 server uploaded nothing while off; on, it uploaded a signed archive with none of eight planted secrets in it, read it back through /api/archive and in the page, and with the S3 server down served the call in 0.1 s and said why the upload failed. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (Claude account tests and TestClaudeRunEndsWhenTheClientLeaves pass alone); request-archive, click-scroll, old-webkit, sse-body, gateway-fold pass.
2026-10-01 17:13:34 +08:00
archive: a request the archive kept is opened and downloaded from the Usage page, with its bodies whole (jorben, #447: 请求存档 reachable from usage history, full bodies, download) — the archive's name was only on the Gateway page's recent calls, gone once a call left the list or the gateway restarted, and the archive kept the first 256 KB of each body, the recent list's cut. The usage ledger's row now names the call's archive (archive in usage.jsonl, kept through the Requests page's packed rows), and its details on Usage → Requests show the Request archive panel, Fetch from archive and Download; with the archive on, a request it has no copy of says "Not archived". The bodies are kept whole up to requestArchiveMaxMB in settings.json (32 MB unless set, at most 1024): the response is teed to a temp file as it is sent, the request body written to one when the call ends, so the upload queue holds file handles, not bodies, and one upload at a time reads them; a body past the limit is kept to it and marked cut, with its whole size. What goes upstream is unchanged, the archive stays off unless turned on, headers and bodies are scrubbed as before, and the temp files are removed after the upload or when the queue is full. The page is sent a body to show only when it is 256 KB or less (a longer one is said by its size), an archive over 80 MB is only said to be; Download streams the object whole, to Downloads in the app (archive/export, -2, -3… for a name taken) and as the browser's download in magpie web (archive/file). TestArchiveWholeBodies, TestArchiveLimit, TestRowKeepsArchive and TestArchivePreviewAndDownload fail without the change; TestArchiveOffUsage, TestArchiveTooLargeToPreview, TestArchiveOldPreview keep the rest. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; request-archive-usage (new), request-archive, click-scroll, old-webkit, usage-ledger, usage-ledger-detail, usage-ledger-content, web-page, sse-body pass.
2026-10-02 00:57:51 +08:00
provider: a providers.json that can't be read is said where the providers are listed, never backed up or synced as none, and is replaced through a rename (PR #415's review) — #415 made edits refuse a file they couldn't read or decode, but every read for the user still took it as empty: magpie providers said "no providers yet", magpie models "no models yet", the Providers page opened the Add sheet as for a first use, and backup.Collect, so an export and a WebDAV/S3 sync, carried it as no providers, which another computer's Mirror would then take as every provider removed. magpie providers and magpie models now list what is left (the signed-in accounts) and end in the error, the TUI says it over its list, and the Providers page shows a "Your providers file can't be read" box (无法读取供应商配置文件) over the list with magpie's reason and no Add sheet; start-up, the gateway and the catalog still go on with it as empty. Stored and StoredGroups return the read error, so a backup and a sync stop on it with nothing pushed. The error, for reads and refused writes alike, names the file, says magpie left it unchanged and to fix it or move it aside (errors.Is ErrUnreadable). store writes through writePrivate, a 0600 temp file renamed over it (steady.Rename waits out a Windows reader holding it, a symlink keeps its target), instead of os.WriteFile's truncate-then-write, and the read goes through steady.ReadFile. TestFileErrorSaysUnreadable, TestWriteErrorsSayLeftUnchanged, TestStoredRefusesUnreadable, TestStoreReplacesAtomically, TestSyncStopsOnUnreadableProviders, TestProvidersCmdSaysUnreadable and provider-file-error.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; provider-file-error, click-scroll, old-webkit, providers-off-fold, add-sheet pass.
2026-10-01 19:45:24 +08:00
davsync: an unchanged file on the server costs a request, not a download (Crispin on Discord: 是不是Magpie后台一直在自动同步,导致被坚果云给限制了) — every 3 minutes sync read the whole sealed file even when nothing had changed, and 坚果云's free plan caps monthly downloads and requests per 30 minutes. The sync state now keeps the server's version beside its hash (its ETag, or a Last-Modified at least a second older than the server's Date, so a write in that same second can't go unseen), from the read or from the ETag a write answers with (none said: the next sync reads in full, as before); the next read sends If-None-Match (or If-Modified-Since) and a 304 with nothing changed here ends the sync, while a change here merges with a copy of the server's file kept beside sync-state.json (sync-server.magpie-backup, 0600, checked against the hash; gone or different, the file is read again), so it isn't read twice. A server that ignores the condition sends the file and syncs exactly as today. S3 does the same with If-None-Match on GetObject, signed with the rest (the fake S3 checks every signature). A 429 or 503 (S3's SlowDown) now says the server is limiting how often magpie may reach it, and Run waits as long as Retry-After says (up to 6 h) or else doubles from 6 up to 30 minutes, back to 3 after a sync that goes through; log de-dup is kept, as the message doesn't carry the wait. The 403 paths (there, forbidden) only run when the server answers 403, so a working setup costs one conditional GET per cycle. TestSyncUnchangedIsNotRead (ETag, ETag not on PUT, Last-Modified: three unchanged cycles read 0 bytes, a server change is read once and merged, a change here is pushed without a read, a missing copy is read again), TestSyncRateLimited and TestS3Unchanged fail without the change (with the conditions dropped: 3 full reads of 1932 bytes, 'HTTP 429' with no backoff, unconditional S3 GETs); TestSyncIgnoringConditions guards servers without conditional reads. go vet ./internal/..., go build -tags nogui ./... (darwin, GOOS=linux), GOOS=windows go build ./..., go test -tags nogui ./internal/davsync/ ./internal/gui/ pass.
2026-10-02 11:08:14 +08:00
davsync: an unchanged file on the server costs a request, not a download (Crispin on Discord: 是不是Magpie后台一直在自动同步,导致被坚果云给限制了) — every 3 minutes sync read the whole sealed file even when nothing had changed, and 坚果云's free plan caps monthly downloads and requests per 30 minutes. The sync state now keeps the server's version beside its hash (its ETag, or a Last-Modified at least a second older than the server's Date, so a write in that same second can't go unseen), from the read or from the ETag a write answers with (none said: the next sync reads in full, as before); the next read sends If-None-Match (or If-Modified-Since) and a 304 with nothing changed here ends the sync, while a change here merges with a copy of the server's file kept beside sync-state.json (sync-server.magpie-backup, 0600, checked against the hash; gone or different, the file is read again), so it isn't read twice. A server that ignores the condition sends the file and syncs exactly as today. S3 does the same with If-None-Match on GetObject, signed with the rest (the fake S3 checks every signature). A 429 or 503 (S3's SlowDown) now says the server is limiting how often magpie may reach it, and Run waits as long as Retry-After says (up to 6 h) or else doubles from 6 up to 30 minutes, back to 3 after a sync that goes through; log de-dup is kept, as the message doesn't carry the wait. The 403 paths (there, forbidden) only run when the server answers 403, so a working setup costs one conditional GET per cycle. TestSyncUnchangedIsNotRead (ETag, ETag not on PUT, Last-Modified: three unchanged cycles read 0 bytes, a server change is read once and merged, a change here is pushed without a read, a missing copy is read again), TestSyncRateLimited and TestS3Unchanged fail without the change (with the conditions dropped: 3 full reads of 1932 bytes, 'HTTP 429' with no backoff, unconditional S3 GETs); TestSyncIgnoringConditions guards servers without conditional reads. go vet ./internal/..., go build -tags nogui ./... (darwin, GOOS=linux), GOOS=windows go build ./..., go test -tags nogui ./internal/davsync/ ./internal/gui/ pass.
2026-10-02 11:08:14 +08:00
davsync: an unchanged file on the server costs a request, not a download (Crispin on Discord: 是不是Magpie后台一直在自动同步,导致被坚果云给限制了) — every 3 minutes sync read the whole sealed file even when nothing had changed, and 坚果云's free plan caps monthly downloads and requests per 30 minutes. The sync state now keeps the server's version beside its hash (its ETag, or a Last-Modified at least a second older than the server's Date, so a write in that same second can't go unseen), from the read or from the ETag a write answers with (none said: the next sync reads in full, as before); the next read sends If-None-Match (or If-Modified-Since) and a 304 with nothing changed here ends the sync, while a change here merges with a copy of the server's file kept beside sync-state.json (sync-server.magpie-backup, 0600, checked against the hash; gone or different, the file is read again), so it isn't read twice. A server that ignores the condition sends the file and syncs exactly as today. S3 does the same with If-None-Match on GetObject, signed with the rest (the fake S3 checks every signature). A 429 or 503 (S3's SlowDown) now says the server is limiting how often magpie may reach it, and Run waits as long as Retry-After says (up to 6 h) or else doubles from 6 up to 30 minutes, back to 3 after a sync that goes through; log de-dup is kept, as the message doesn't carry the wait. The 403 paths (there, forbidden) only run when the server answers 403, so a working setup costs one conditional GET per cycle. TestSyncUnchangedIsNotRead (ETag, ETag not on PUT, Last-Modified: three unchanged cycles read 0 bytes, a server change is read once and merged, a change here is pushed without a read, a missing copy is read again), TestSyncRateLimited and TestS3Unchanged fail without the change (with the conditions dropped: 3 full reads of 1932 bytes, 'HTTP 429' with no backoff, unconditional S3 GETs); TestSyncIgnoringConditions guards servers without conditional reads. go vet ./internal/..., go build -tags nogui ./... (darwin, GOOS=linux), GOOS=windows go build ./..., go test -tags nogui ./internal/davsync/ ./internal/gui/ pass.
2026-10-02 11:08:14 +08:00
davsync: an unchanged file on the server costs a request, not a download (Crispin on Discord: 是不是Magpie后台一直在自动同步,导致被坚果云给限制了) — every 3 minutes sync read the whole sealed file even when nothing had changed, and 坚果云's free plan caps monthly downloads and requests per 30 minutes. The sync state now keeps the server's version beside its hash (its ETag, or a Last-Modified at least a second older than the server's Date, so a write in that same second can't go unseen), from the read or from the ETag a write answers with (none said: the next sync reads in full, as before); the next read sends If-None-Match (or If-Modified-Since) and a 304 with nothing changed here ends the sync, while a change here merges with a copy of the server's file kept beside sync-state.json (sync-server.magpie-backup, 0600, checked against the hash; gone or different, the file is read again), so it isn't read twice. A server that ignores the condition sends the file and syncs exactly as today. S3 does the same with If-None-Match on GetObject, signed with the rest (the fake S3 checks every signature). A 429 or 503 (S3's SlowDown) now says the server is limiting how often magpie may reach it, and Run waits as long as Retry-After says (up to 6 h) or else doubles from 6 up to 30 minutes, back to 3 after a sync that goes through; log de-dup is kept, as the message doesn't carry the wait. The 403 paths (there, forbidden) only run when the server answers 403, so a working setup costs one conditional GET per cycle. TestSyncUnchangedIsNotRead (ETag, ETag not on PUT, Last-Modified: three unchanged cycles read 0 bytes, a server change is read once and merged, a change here is pushed without a read, a missing copy is read again), TestSyncRateLimited and TestS3Unchanged fail without the change (with the conditions dropped: 3 full reads of 1932 bytes, 'HTTP 429' with no backoff, unconditional S3 GETs); TestSyncIgnoringConditions guards servers without conditional reads. go vet ./internal/..., go build -tags nogui ./... (darwin, GOOS=linux), GOOS=windows go build ./..., go test -tags nogui ./internal/davsync/ ./internal/gui/ pass.
2026-10-02 11:08:14 +08:00
davsync: an unchanged file on the server costs a request, not a download (Crispin on Discord: 是不是Magpie后台一直在自动同步,导致被坚果云给限制了) — every 3 minutes sync read the whole sealed file even when nothing had changed, and 坚果云's free plan caps monthly downloads and requests per 30 minutes. The sync state now keeps the server's version beside its hash (its ETag, or a Last-Modified at least a second older than the server's Date, so a write in that same second can't go unseen), from the read or from the ETag a write answers with (none said: the next sync reads in full, as before); the next read sends If-None-Match (or If-Modified-Since) and a 304 with nothing changed here ends the sync, while a change here merges with a copy of the server's file kept beside sync-state.json (sync-server.magpie-backup, 0600, checked against the hash; gone or different, the file is read again), so it isn't read twice. A server that ignores the condition sends the file and syncs exactly as today. S3 does the same with If-None-Match on GetObject, signed with the rest (the fake S3 checks every signature). A 429 or 503 (S3's SlowDown) now says the server is limiting how often magpie may reach it, and Run waits as long as Retry-After says (up to 6 h) or else doubles from 6 up to 30 minutes, back to 3 after a sync that goes through; log de-dup is kept, as the message doesn't carry the wait. The 403 paths (there, forbidden) only run when the server answers 403, so a working setup costs one conditional GET per cycle. TestSyncUnchangedIsNotRead (ETag, ETag not on PUT, Last-Modified: three unchanged cycles read 0 bytes, a server change is read once and merged, a change here is pushed without a read, a missing copy is read again), TestSyncRateLimited and TestS3Unchanged fail without the change (with the conditions dropped: 3 full reads of 1932 bytes, 'HTTP 429' with no backoff, unconditional S3 GETs); TestSyncIgnoringConditions guards servers without conditional reads. go vet ./internal/..., go build -tags nogui ./... (darwin, GOOS=linux), GOOS=windows go build ./..., go test -tags nogui ./internal/davsync/ ./internal/gui/ pass.
2026-10-02 11:08:14 +08:00
davsync: an unchanged file on the server costs a request, not a download (Crispin on Discord: 是不是Magpie后台一直在自动同步,导致被坚果云给限制了) — every 3 minutes sync read the whole sealed file even when nothing had changed, and 坚果云's free plan caps monthly downloads and requests per 30 minutes. The sync state now keeps the server's version beside its hash (its ETag, or a Last-Modified at least a second older than the server's Date, so a write in that same second can't go unseen), from the read or from the ETag a write answers with (none said: the next sync reads in full, as before); the next read sends If-None-Match (or If-Modified-Since) and a 304 with nothing changed here ends the sync, while a change here merges with a copy of the server's file kept beside sync-state.json (sync-server.magpie-backup, 0600, checked against the hash; gone or different, the file is read again), so it isn't read twice. A server that ignores the condition sends the file and syncs exactly as today. S3 does the same with If-None-Match on GetObject, signed with the rest (the fake S3 checks every signature). A 429 or 503 (S3's SlowDown) now says the server is limiting how often magpie may reach it, and Run waits as long as Retry-After says (up to 6 h) or else doubles from 6 up to 30 minutes, back to 3 after a sync that goes through; log de-dup is kept, as the message doesn't carry the wait. The 403 paths (there, forbidden) only run when the server answers 403, so a working setup costs one conditional GET per cycle. TestSyncUnchangedIsNotRead (ETag, ETag not on PUT, Last-Modified: three unchanged cycles read 0 bytes, a server change is read once and merged, a change here is pushed without a read, a missing copy is read again), TestSyncRateLimited and TestS3Unchanged fail without the change (with the conditions dropped: 3 full reads of 1932 bytes, 'HTTP 429' with no backoff, unconditional S3 GETs); TestSyncIgnoringConditions guards servers without conditional reads. go vet ./internal/..., go build -tags nogui ./... (darwin, GOOS=linux), GOOS=windows go build ./..., go test -tags nogui ./internal/davsync/ ./internal/gui/ pass.
2026-10-02 11:08:14 +08:00
davsync: an unchanged file on the server costs a request, not a download (Crispin on Discord: 是不是Magpie后台一直在自动同步,导致被坚果云给限制了) — every 3 minutes sync read the whole sealed file even when nothing had changed, and 坚果云's free plan caps monthly downloads and requests per 30 minutes. The sync state now keeps the server's version beside its hash (its ETag, or a Last-Modified at least a second older than the server's Date, so a write in that same second can't go unseen), from the read or from the ETag a write answers with (none said: the next sync reads in full, as before); the next read sends If-None-Match (or If-Modified-Since) and a 304 with nothing changed here ends the sync, while a change here merges with a copy of the server's file kept beside sync-state.json (sync-server.magpie-backup, 0600, checked against the hash; gone or different, the file is read again), so it isn't read twice. A server that ignores the condition sends the file and syncs exactly as today. S3 does the same with If-None-Match on GetObject, signed with the rest (the fake S3 checks every signature). A 429 or 503 (S3's SlowDown) now says the server is limiting how often magpie may reach it, and Run waits as long as Retry-After says (up to 6 h) or else doubles from 6 up to 30 minutes, back to 3 after a sync that goes through; log de-dup is kept, as the message doesn't carry the wait. The 403 paths (there, forbidden) only run when the server answers 403, so a working setup costs one conditional GET per cycle. TestSyncUnchangedIsNotRead (ETag, ETag not on PUT, Last-Modified: three unchanged cycles read 0 bytes, a server change is read once and merged, a change here is pushed without a read, a missing copy is read again), TestSyncRateLimited and TestS3Unchanged fail without the change (with the conditions dropped: 3 full reads of 1932 bytes, 'HTTP 429' with no backoff, unconditional S3 GETs); TestSyncIgnoringConditions guards servers without conditional reads. go vet ./internal/..., go build -tags nogui ./... (darwin, GOOS=linux), GOOS=windows go build ./..., go test -tags nogui ./internal/davsync/ ./internal/gui/ pass.
2026-10-02 11:08:14 +08:00
gateway: a model's web search goes to a search API of the user's own when no provider can search (#419 by HwFee: let me set up Tavily, Brave or SearXNG with my key rather than lose the search tool) — with no provider that searches by itself and no provider magpie could search with, the gateway took the client's web search tool away. Tavily, Brave Search, Exa, Firecrawl and SearXNG (an address of one's own, a key only if it asks for one) can now be set up, in an order: the injected web_search tool is then offered, and a search goes to the searcher's model as before, or, with none or when it fails, to each search API in turn, called directly with no model, its results handed back as the same hits and text the searcher path gives, so clients see the same web_search_tool_result blocks. The order is provider searches itself, searcher provider, search APIs, strip. They are kept in providers.json (0600) like provider keys; a backup carries them, their keys only when it carries keys, and one restored without keys keeps the key here; sync does the same, and a backup or sync from a magpie before them leaves the ones here alone. magpie search lists them, magpie search add <api> [key] [url=…] adds or updates one, magpie search rm <api> removes one; Settings has a Web search section (联网搜索) to add and remove them. Prompts, identity lines and headers to model providers are unchanged. TestWebSearchBySearchAPI, TestNoSearchAPINoTool, TestBackupSearchAPIs, TestSyncSearchAPIs and search-api.test.cjs fail without the change; TestSearchAPIsEachAsked checks each vendor against a fake, TestSearchCLI the command. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; search-api, provider-search, click-scroll, old-webkit pass.
2026-10-01 19:44:44 +08:00
davsync: sync can keep its file in an S3-compatible bucket as well as a WebDAV folder (#296) — AWS S3, Cloudflare R2, Backblaze B2, MinIO, Garage or a NAS. Settings › Sync and backup picks WebDAV or S3: endpoint (empty is AWS), bucket, prefix, region (empty is us-east-1, auto for R2), access key ID and secret, path-style; CLI magpie s3 on s3://bucket/prefix access-key-id=… endpoint=… region=… path-style. The bucket holds the same passphrase-sealed file at <prefix>/magpie/magpie.magpie-backup, signed with SigV4 (no SDK). It is read with its ETag and written with If-Match (If-None-Match: * for the first), so two computers syncing at once don't overwrite each other; a server that refuses conditional writes (501, or 400 naming the header) is remembered and gets a HEAD-then-PUT instead. An IP, localhost or a dotted bucket on https goes path-style on its own. Errors say what to do: wrong key/secret, clock skew, bucket in another region (names it), no such bucket, 403 on the first read on AWS without s3:ListBucket. The secret is kept as the WebDAV password is, sent only to the endpoint and key it was typed for, and may not be the passphrase. sigv4_test (AWS's published vectors), s3_test (fake server: conditions, conflict, fallback, regions, errors), s3_cli_test and s3-sync.test.cjs (Chromium, WebKit, en/zh) fail without the change; tested against MinIO. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestGroupAddSetRemove, a TempDir-cleanup flake under load, passes alone); click-scroll, old-webkit pass.
2026-09-30 17:07:47 +08:00
davsync: sync can keep its file in an S3-compatible bucket as well as a WebDAV folder (#296) — AWS S3, Cloudflare R2, Backblaze B2, MinIO, Garage or a NAS. Settings › Sync and backup picks WebDAV or S3: endpoint (empty is AWS), bucket, prefix, region (empty is us-east-1, auto for R2), access key ID and secret, path-style; CLI magpie s3 on s3://bucket/prefix access-key-id=… endpoint=… region=… path-style. The bucket holds the same passphrase-sealed file at <prefix>/magpie/magpie.magpie-backup, signed with SigV4 (no SDK). It is read with its ETag and written with If-Match (If-None-Match: * for the first), so two computers syncing at once don't overwrite each other; a server that refuses conditional writes (501, or 400 naming the header) is remembered and gets a HEAD-then-PUT instead. An IP, localhost or a dotted bucket on https goes path-style on its own. Errors say what to do: wrong key/secret, clock skew, bucket in another region (names it), no such bucket, 403 on the first read on AWS without s3:ListBucket. The secret is kept as the WebDAV password is, sent only to the endpoint and key it was typed for, and may not be the passphrase. sigv4_test (AWS's published vectors), s3_test (fake server: conditions, conflict, fallback, regions, errors), s3_cli_test and s3-sync.test.cjs (Chromium, WebKit, en/zh) fail without the change; tested against MinIO. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestGroupAddSetRemove, a TempDir-cleanup flake under load, passes alone); click-scroll, old-webkit pass.
2026-09-30 17:07:47 +08:00
provider: a providers.json that can't be read is said where the providers are listed, never backed up or synced as none, and is replaced through a rename (PR #415's review) — #415 made edits refuse a file they couldn't read or decode, but every read for the user still took it as empty: magpie providers said "no providers yet", magpie models "no models yet", the Providers page opened the Add sheet as for a first use, and backup.Collect, so an export and a WebDAV/S3 sync, carried it as no providers, which another computer's Mirror would then take as every provider removed. magpie providers and magpie models now list what is left (the signed-in accounts) and end in the error, the TUI says it over its list, and the Providers page shows a "Your providers file can't be read" box (无法读取供应商配置文件) over the list with magpie's reason and no Add sheet; start-up, the gateway and the catalog still go on with it as empty. Stored and StoredGroups return the read error, so a backup and a sync stop on it with nothing pushed. The error, for reads and refused writes alike, names the file, says magpie left it unchanged and to fix it or move it aside (errors.Is ErrUnreadable). store writes through writePrivate, a 0600 temp file renamed over it (steady.Rename waits out a Windows reader holding it, a symlink keeps its target), instead of os.WriteFile's truncate-then-write, and the read goes through steady.ReadFile. TestFileErrorSaysUnreadable, TestWriteErrorsSayLeftUnchanged, TestStoredRefusesUnreadable, TestStoreReplacesAtomically, TestSyncStopsOnUnreadableProviders, TestProvidersCmdSaysUnreadable and provider-file-error.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; provider-file-error, click-scroll, old-webkit, providers-off-fold, add-sheet pass.
2026-10-01 19:45:24 +08:00