@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user