feat(backup): download the full backup as ZIP
GET /export?format=zip streams the JSON backup compressed (plain JSON stays the API default). The MindSift import recognises the backup ZIP next to the plain JSON and the Markdown archive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1601a6a2d5
commit
1df32ece92
7 files changed
+122
-25
No files matched your search
@@ -2,6 +2,7 @@ package storage
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/sandrodach/mindsift/internal/domain"
|
||||
@@ -300,6 +302,63 @@ func PlanMindSiftArchiveImport(r io.ReaderAt, size int64) (*ImportPlan, error) {
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// BackupFileName is the JSON inside the backup ZIP.
|
||||
const BackupFileName = "mindsift-backup.json"
|
||||
|
||||
// WriteBackupZip streams the JSON backup compressed into a ZIP with a single file.
|
||||
func WriteBackupZip(exp *domain.FullExport, w io.Writer) error {
|
||||
zw := zip.NewWriter(w)
|
||||
f, err := zw.CreateHeader(&zip.FileHeader{Name: BackupFileName, Method: zip.Deflate, Modified: parseISO(exp.ExportedAt, time.Now())})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(exp); err != nil {
|
||||
return err
|
||||
}
|
||||
return zw.Close()
|
||||
}
|
||||
|
||||
// PlanMindSiftImport picks the format of MindSift's own data: plain JSON backup, the backup ZIP
|
||||
// (one JSON inside) or the Markdown archive.
|
||||
func PlanMindSiftImport(data []byte, fileName string) (*ImportPlan, error) {
|
||||
if strings.HasSuffix(strings.ToLower(fileName), ".json") || (len(data) > 0 && data[0] == '{') {
|
||||
return PlanMindSiftJSONImport(data)
|
||||
}
|
||||
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return nil, errors.New("Die Datei ist weder ein MindSift-Backup (.json/.zip) noch ein MindSift-Markdown-Archiv")
|
||||
}
|
||||
var backup *zip.File
|
||||
for _, f := range zr.File {
|
||||
name := cleanArchivePath(f.Name)
|
||||
if strings.EqualFold(path.Ext(name), ".md") {
|
||||
backup = nil
|
||||
break
|
||||
}
|
||||
if strings.EqualFold(path.Ext(name), ".json") && !hiddenPath(name) {
|
||||
backup = f
|
||||
}
|
||||
}
|
||||
if backup == nil {
|
||||
return PlanMindSiftArchiveImport(bytes.NewReader(data), int64(len(data)))
|
||||
}
|
||||
if backup.UncompressedSize64 > maxImportUnpackedBytes {
|
||||
return nil, errors.New("Backup ist entpackt zu groß (max. 500 MB)")
|
||||
}
|
||||
rc, err := backup.Open()
|
||||
if err != nil {
|
||||
return nil, errors.New("Backup im Archiv ist nicht lesbar")
|
||||
}
|
||||
defer rc.Close()
|
||||
raw, err := io.ReadAll(io.LimitReader(rc, maxImportUnpackedBytes+1))
|
||||
if err != nil || len(raw) > maxImportUnpackedBytes {
|
||||
return nil, errors.New("Backup im Archiv ist nicht lesbar oder zu groß")
|
||||
}
|
||||
return PlanMindSiftJSONImport(raw)
|
||||
}
|
||||
|
||||
// PlanMindSiftJSONImport reads the JSON backup ("Backup herunterladen"). Notes shared with the
|
||||
// exporting user belong to someone else and are skipped; discarded thoughts are not restored.
|
||||
func PlanMindSiftJSONImport(data []byte) (*ImportPlan, error) {
|
||||
|
||||
Reference in new issue
Block a user