Files
magpie/internal/davsync/archive.go
T
yetone 3c04877cfc 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

89 lines
2.4 KiB
Go

package davsync
import (
"context"
"errors"
"io"
"net/http"
"strings"
)
// Bucket is the S3 sync's bucket for files other than the backup — the
// gateway's request archive — each at <prefix>/magpie/<name>, beside the
// backup, written as it is: no passphrase seals them, so they can be read
// in the bucket too.
type Bucket struct{ s *s3 }
// ErrNoObject is a name with nothing at it.
var ErrNoObject = errors.New("not in the bucket")
// S3Bucket is the bucket sync keeps the backup in; false when sync is off
// or to a WebDAV folder.
func S3Bucket() (*Bucket, bool) {
c, ok := Load()
if !ok || !c.S3() {
return nil, false
}
s, err := newS3(c)
if err != nil {
return nil, false
}
return &Bucket{s}, true
}
// at is the client for name rather than the backup.
func (b *Bucket) at(name string) *s3 {
s := *b.s
s.key = strings.TrimSuffix(s.key, file) + strings.TrimLeft(name, "/")
return &s
}
// Where names the bucket, its prefix and its server, as messages do.
func (b *Bucket) Where() string { return b.at("").where() }
// Put writes data at name, over what is there.
func (b *Bucket) Put(ctx context.Context, name string, data []byte) error {
s := b.at(name)
res, err := s.send(ctx, http.MethodPut, data, map[string]string{"Content-Type": "application/json"})
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
io.Copy(io.Discard, io.LimitReader(res.Body, 64<<10))
return nil
}
return s.explain("write", readError(res))
}
// Get reads what is at name, its first 16 MiB; ErrNoObject when there is
// nothing.
func (b *Bucket) Get(ctx context.Context, name string) ([]byte, error) {
body, _, err := b.Open(ctx, name)
if err != nil {
return nil, err
}
defer body.Close()
return io.ReadAll(io.LimitReader(body, 16<<20))
}
// Open is what is at name as it is read, however large, and its size (-1
// when the server doesn't say); ErrNoObject when there is nothing. The
// caller closes it.
func (b *Bucket) Open(ctx context.Context, name string) (io.ReadCloser, int64, error) {
s := b.at(name)
res, err := s.send(ctx, http.MethodGet, nil, nil)
if err != nil {
return nil, 0, err
}
if res.StatusCode != http.StatusOK {
defer res.Body.Close()
e := readError(res)
if e.Status == http.StatusNotFound && e.Code != "NoSuchBucket" {
return nil, 0, ErrNoObject
}
return nil, 0, s.explain("read", e)
}
return res.Body, res.ContentLength, nil
}