diff --git a/internal/backup/backup.go b/internal/backup/backup.go index 4b7e797f..f1ff8542 100644 --- a/internal/backup/backup.go +++ b/internal/backup/backup.go @@ -109,6 +109,11 @@ var ErrPassphrase = errors.New("wrong passphrase, or the file was changed") // the damaged one from this computer rather than fail on it forever. var ErrCorrupt = errors.New("not a magpie backup") +// ErrNotBackup is a file that doesn't begin like a backup at all: an empty +// one, a page a server answered with, another app's file. It reads as +// ErrCorrupt does, but is never rebuilt over by a sync on its own. +var ErrNotBackup = errors.New("not a magpie backup") + // BundleVersion is the bundle's structure: 2 carries the per-part // credential markers (ProvidersKeys, SettingsKeys, LibraryKeys) as their // own three-state flags; 1 and before had the whole-bundle Keys alone. @@ -278,10 +283,10 @@ func Open(data []byte, pass string) (Bundle, error) { if isBackupJSON(data) { return Bundle{}, ErrCorrupt } - return Bundle{}, errors.New("not a magpie backup") + return Bundle{}, ErrNotBackup } if e.Format != format { - return Bundle{}, errors.New("not a magpie backup") + return Bundle{}, ErrNotBackup } if e.Version != 1 || e.KDF != "pbkdf2-sha256" { return Bundle{}, errors.New("this backup was made by a newer magpie; update magpie to open it") diff --git a/internal/davsync/dav.go b/internal/davsync/dav.go index 0d0d2fec..ba142c59 100644 --- a/internal/davsync/dav.go +++ b/internal/davsync/dav.go @@ -2,7 +2,6 @@ package davsync import ( "bytes" - "compress/gzip" "context" "encoding/xml" "errors" @@ -14,6 +13,8 @@ import ( "strconv" "strings" "time" + + "github.com/yetone/magpie/internal/backup" ) // folder and file are where the backup lives under the address given. @@ -279,21 +280,26 @@ func (d *dav) get(ctx context.Context, have version) (data []byte, v version, er case res.StatusCode != http.StatusOK: return nil, version{}, fmt.Errorf("reading %s from the WebDAV server: HTTP %d", file, res.StatusCode) } - body := io.Reader(res.Body) - // asked for as it is, the file still came compressed: Go unpacks it - // only when it asked for gzip itself - if strings.EqualFold(strings.TrimSpace(res.Header.Get("Content-Encoding")), "gzip") { - zr, err := gzip.NewReader(res.Body) - if err != nil { - return nil, version{}, fmt.Errorf("reading %s from the WebDAV server: %w", file, err) - } - defer zr.Close() - body = zr - } - data, err = io.ReadAll(io.LimitReader(body, 64<<20)) + data, err = io.ReadAll(io.LimitReader(res.Body, 64<<20)) if err != nil { return nil, version{}, err } + // asked for as it is, the file still came compressed: Go unpacks it + // only when it asked for gzip itself + if data, err = unpacked("WebDAV", data, res.Header.Get("Content-Encoding")); err != nil { + return nil, version{}, fmt.Errorf("reading %s from the WebDAV server: %w", file, err) + } + // a redirect that ended at something other than the file — a web + // app's sign-in or files page — is said with where it went: the + // address is the thing to fix + if res.Request != nil && res.Request.Response != nil && !backupHead(bytes.TrimSpace(data)) { + if f := whatIs(data); !f.Replace { + to := *res.Request.URL + to.RawQuery, to.Fragment, to.User = "", "", nil + f.Moved = to.Host + to.Path + return nil, version{}, ¬Backup{f: f, where: "the file " + folder + "/" + file + " on the WebDAV server", cmd: "webdav", err: backup.ErrNotBackup} + } + } return data, versionOf(res.Header), nil } diff --git a/internal/davsync/davsync.go b/internal/davsync/davsync.go index 4f8f2b58..2ea8e50d 100644 --- a/internal/davsync/davsync.go +++ b/internal/davsync/davsync.go @@ -170,6 +170,9 @@ type state struct { // Undo is the file the last Restore kept this computer's setup in, from // just before it: Undo brings it back Undo string `json:"undo,omitempty"` + // File is what the server held where the backup should be, when the + // last sync couldn't read it as one + File *ServerFile `json:"file,omitempty"` } func path(name string) string { return filepath.Join(settings.Dir(), name) } @@ -377,6 +380,9 @@ type View struct { Auto int `json:"auto"` // Undo is whether the last Restore can be undone Undo bool `json:"undo,omitempty"` + // File is what the server holds instead of a backup, when Error is + // that it isn't one; File.Replace offers Upload + File *ServerFile `json:"serverFile,omitempty"` } // OtherView is the other kind's server as the Settings page shows it. @@ -404,6 +410,9 @@ func Status() View { if st.Key == stateKey(c) { v.Last = st.Last v.Undo = st.Undo != "" && isFile(st.Undo) + if st.Error != "" { + v.File = st.File + } if c.Usage && st.Usage != nil { v.UsageError = st.Usage.Error } @@ -480,21 +489,27 @@ func cached(want string) []byte { // that always answers the same unreadable way) would otherwise pile up a // copy each time. The name is sorted by time, so the oldest are the first // to go. -func keepDamaged(data []byte) { +func keepDamaged(data []byte) { keepAside(data, "server-damaged", 3) } + +// keepAside writes data under the sync folder as