Update 1.5.0
release-tag / release-image (push) Successful in 10m52s

This commit is contained in:
2026-08-27 07:51:39 +02:00
parent 1decb831d6
commit 8c67c7a7fa
58 changed files with 10768 additions and 626 deletions
+28 -1
View File
@@ -377,6 +377,24 @@ func (s *Store) archive(key, bucket string) (string, error) {
if err := os.Rename(path, dst); err != nil {
return "", fmt.Errorf("move staging file to %s: %w", bucket, err)
}
rollbackRename := func(cause error) error {
if err := os.Rename(dst, path); err != nil {
return fmt.Errorf("%v; staging archive rollback failed: %w", cause, err)
}
if err := syncDir(s.dir); err != nil {
return fmt.Errorf("%v; staging rollback source sync failed: %w", cause, err)
}
if err := syncDir(archiveDir); err != nil {
return fmt.Errorf("%v; staging rollback archive sync failed: %w", cause, err)
}
return cause
}
if err := syncDir(s.dir); err != nil {
return "", rollbackRename(fmt.Errorf("sync staging directory: %w", err))
}
if err := syncDir(archiveDir); err != nil {
return "", rollbackRename(fmt.Errorf("sync staging archive: %w", err))
}
return dst, nil
}
@@ -433,7 +451,16 @@ func atomicWrite(path string, payload []byte, mode os.FileMode) error {
if err := os.Rename(tmpName, path); err != nil {
return err
}
return nil
return syncDir(filepath.Dir(path))
}
func syncDir(dir string) error {
f, err := os.Open(dir)
if err != nil {
return err
}
defer f.Close()
return f.Sync()
}
func summarize(result Result) Summary {
@@ -686,6 +686,10 @@ func (s *Store) ImportDocument(doc map[string]any, preferredBase string) (Summar
_ = os.Remove(tmpName)
return Summary{}, err
}
if err := syncDir(s.dataDir); err != nil {
_ = os.Remove(path)
return Summary{}, err
}
rec, err := s.readRecord(path)
if err != nil {
_ = os.Remove(path)
@@ -697,6 +701,47 @@ func (s *Store) ImportDocument(doc map[string]any, preferredBase string) (Summar
return summarize(rec), nil
}
// RollbackImported removes exactly the production record created by ImportDocument.
// The checksum guard prevents rollback from deleting a file that was changed after
// import. It is used to keep staging promotion transactional.
func (s *Store) RollbackImported(summary Summary) error {
s.mu.Lock()
defer s.mu.Unlock()
rec, ok := s.records[summary.Key]
if !ok {
return os.ErrNotExist
}
if summary.Checksum == "" || rec.Checksum != summary.Checksum {
return fmt.Errorf("refusing rollback: production record %s changed after import", summary.RelPath)
}
if err := os.Remove(rec.Path); err != nil {
return err
}
// The namespace mutation already happened once Remove succeeded. Reflect it
// in memory even when the durability fsync below reports an I/O error;
// otherwise this process would serve a record whose file no longer exists.
delete(s.records, summary.Key)
for i, key := range s.order {
if key == summary.Key {
s.order = append(s.order[:i], s.order[i+1:]...)
break
}
}
if err := syncDir(s.dataDir); err != nil {
return fmt.Errorf("sync production directory after rollback: %w", err)
}
return nil
}
func syncDir(dir string) error {
f, err := os.Open(dir)
if err != nil {
return err
}
defer f.Close()
return f.Sync()
}
func safeFilenameBase(value string) string {
value = strings.TrimSpace(value)
if value == "" {
@@ -947,6 +992,9 @@ func (s *Store) writeRecord(rec *record, doc map[string]any) (*record, error) {
_ = os.Remove(tmpName)
return nil, err
}
if err := syncDir(filepath.Dir(rec.Path)); err != nil {
return nil, err
}
return s.readRecord(rec.Path)
}