package api import ( "context" "errors" "net/http" "git.fabledsword.com/bvandeusen/minstrel/internal/apierror" "git.fabledsword.com/bvandeusen/minstrel/internal/backup" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/library" ) // SetupTokenHeader carries the setup token on a restore upload, whose body is // the backup file itself. const SetupTokenHeader = "X-Setup-Token" type restoreResp struct { // Accounts is how many accounts came back; everyone signs in with the // password they had. Accounts int64 `json:"accounts"` // ScanStarted says the library scan that refills what the backup leaves // out is under way. ScanStarted bool `json:"scan_started"` } // handleRestore implements POST /api/auth/restore: a new server, with no // accounts yet, loads a backup file sent as the request body. It asks for the // setup token from the server log, as creating the first account does, since // the backup's admins become this server's admins. Then it starts a library // scan, which finds each file at its path and fills in what the backup left // out. // // 200 restored · 400 not_a_backup · 403 setup_token_invalid · // 409 not_empty (accounts or a library already) · 409 newer_backup func (h *handlers) handleRestore(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := dbq.New(h.pool) n, err := q.CountUsers(ctx) if err != nil { writeErrWithLog(w, h.logger, "restore: count users", apierror.Internal(err)) return } if n > 0 { writeErr(w, apierror.Conflict("not_empty", "this server already has accounts; a backup restores into a new one")) return } if h.requireSetupToken && !h.setupToken.Matches(r.Header.Get(SetupTokenHeader)) { h.logger.Warn("restore: needs the setup token", "setup_token", h.setupToken.Value()) writeErr(w, apierror.Forbidden("setup_token_invalid", "the setup token from the server log is required to restore a backup")) return } manifest, contents, err := backup.Read(r.Body) if err != nil { var tooBig *http.MaxBytesError if errors.As(err, &tooBig) { writeErr(w, &apierror.Error{Status: http.StatusRequestEntityTooLarge, Code: "too_large", Message: "the file is larger than a backup can be"}) return } writeErr(w, apierror.BadRequest("not_a_backup", "that file isn't a Minstrel backup")) return } switch err := backup.Restore(ctx, h.pool, contents); { case errors.Is(err, backup.ErrNotEmpty): writeErr(w, apierror.Conflict("not_empty", "this server already has accounts or a library; a backup restores into a new one")) return case errors.Is(err, backup.ErrNewerSchema): writeErr(w, apierror.Conflict("newer_backup", "the backup was made by a newer Minstrel; update this server first")) return case errors.Is(err, backup.ErrNotABackup): writeErr(w, apierror.BadRequest("not_a_backup", "that file isn't a Minstrel backup")) return case err != nil: writeErrWithLog(w, h.logger, "restore: load the backup", apierror.Internal(err)) return } out := restoreResp{} if out.Accounts, err = q.CountUsers(ctx); err != nil { h.logger.Warn("restore: count restored accounts", "err", err) } h.logger.Info("restored a backup", "made_at", manifest.CreatedAt, "build", manifest.Build, "schema", manifest.Schema, "accounts", out.Accounts) if h.scanner != nil { started, _, err := library.TryStartScan(context.Background(), h.pool, h.scanner, h.coverart, h.logger.With("source", "restore"), h.scanCfg) if err != nil { h.logger.Warn("restore: start the library scan", "err", err) } out.ScanStarted = started } writeJSON(w, http.StatusOK, out) }