package file import ( "context" "errors" "fmt" "io/fs" "os" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/txn" ) const deleteFileSuffix = ".delete" // RenamerRemover provides access to the Rename and Remove functions. type RenamerRemover interface { Rename(oldpath, newpath string) error Remove(name string) error RemoveAll(path string) error Stat(name string) (fs.FileInfo, error) } type renamerRemoverImpl struct { RenameFn func(oldpath, newpath string) error RemoveFn func(name string) error RemoveAllFn func(path string) error StatFn func(path string) (fs.FileInfo, error) } func (r renamerRemoverImpl) Rename(oldpath, newpath string) error { return r.RenameFn(oldpath, newpath) } func (r renamerRemoverImpl) Remove(name string) error { return r.RemoveFn(name) } func (r renamerRemoverImpl) RemoveAll(path string) error { return r.RemoveAllFn(path) } func (r renamerRemoverImpl) Stat(path string) (fs.FileInfo, error) { return r.StatFn(path) } // Deleter is used to safely delete files and directories from the filesystem. // During a transaction, files and directories are marked for deletion using // the Files and Dirs methods. This will rename the files/directories to be // deleted. If the transaction is rolled back, then the files/directories can // be restored to their original state with the Abort method. If the // transaction is committed, the marked files are then deleted from the // filesystem using the Complete method. type Deleter struct { RenamerRemover RenamerRemover files []string dirs []string } func NewDeleter() *Deleter { return &Deleter{ RenamerRemover: renamerRemoverImpl{ RenameFn: os.Rename, RemoveFn: os.Remove, RemoveAllFn: os.RemoveAll, StatFn: os.Stat, }, } } // RegisterHooks registers post-commit and post-rollback hooks. func (d *Deleter) RegisterHooks(ctx context.Context, mgr txn.Manager) { txn.AddPostCommitHook(ctx, func(ctx context.Context) error { d.Commit() return nil }) txn.AddPostRollbackHook(ctx, func(ctx context.Context) error { d.Rollback() return nil }) } // Files designates files to be deleted. Each file marked will be renamed to add // a `.delete` suffix. An error is returned if a file could not be renamed. // Note that if an error is returned, then some files may be left renamed. // Abort should be called to restore marked files if this function returns an // error. func (d *Deleter) Files(paths []string) error { for _, p := range paths { // fail silently if the file does not exist if _, err := d.RenamerRemover.Stat(p); err != nil { if errors.Is(err, fs.ErrNotExist) { logger.Warnf("File %q does not exist and therefore cannot be deleted. Ignoring.", p) continue } return fmt.Errorf("check file %q exists: %w", p, err) } if err := d.renameForDelete(p); err != nil { return fmt.Errorf("marking file %q for deletion: %w", p, err) } d.files = append(d.files, p) } return nil } // Dirs designates directories to be deleted. Each directory marked will be renamed to add // a `.delete` suffix. An error is returned if a directory could not be renamed. // Note that if an error is returned, then some directories may be left renamed. // Abort should be called to restore marked files/directories if this function returns an // error. func (d *Deleter) Dirs(paths []string) error { for _, p := range paths { // fail silently if the file does not exist if _, err := d.RenamerRemover.Stat(p); err != nil { if errors.Is(err, fs.ErrNotExist) { logger.Warnf("Directory %q does not exist and therefore cannot be deleted. Ignoring.", p) continue } return fmt.Errorf("check directory %q exists: %w", p, err) } if err := d.renameForDelete(p); err != nil { return fmt.Errorf("marking directory %q for deletion: %w", p, err) } d.dirs = append(d.dirs, p) } return nil } // Rollback tries to rename all marked files and directories back to their // original names and clears the marked list. Any errors encountered are // logged. All files will be attempted regardless of any errors occurred. func (d *Deleter) Rollback() { for _, f := range append(d.files, d.dirs...) { if err := d.renameForRestore(f); err != nil { logger.Warnf("Error restoring %q: %v", f, err) } } d.files = nil d.dirs = nil } // Commit deletes all files marked for deletion and clears the marked list. // Any errors encountered are logged. All files will be attempted, regardless // of the errors encountered. func (d *Deleter) Commit() { for _, f := range d.files { if err := d.RenamerRemover.Remove(f + deleteFileSuffix); err != nil { logger.Warnf("Error deleting file %q: %v", f+deleteFileSuffix, err) } } for _, f := range d.dirs { if err := d.RenamerRemover.RemoveAll(f + deleteFileSuffix); err != nil { logger.Warnf("Error deleting directory %q: %v", f+deleteFileSuffix, err) } } d.files = nil d.dirs = nil } func (d *Deleter) renameForDelete(path string) error { return d.RenamerRemover.Rename(path, path+deleteFileSuffix) } func (d *Deleter) renameForRestore(path string) error { return d.RenamerRemover.Rename(path+deleteFileSuffix, path) } func Destroy(ctx context.Context, destroyer Destroyer, f File, fileDeleter *Deleter, deleteFile bool) error { if err := destroyer.Destroy(ctx, f.Base().ID); err != nil { return err } // don't delete files in zip files if deleteFile && f.Base().ZipFileID == nil { if err := fileDeleter.Files([]string{f.Base().Path}); err != nil { return err } } return nil } type ZipDestroyer struct { FileDestroyer GetterDestroyer FolderDestroyer FolderGetterDestroyer } func (d *ZipDestroyer) DestroyZip(ctx context.Context, f File, fileDeleter *Deleter, deleteFile bool) error { // destroy contained files files, err := d.FileDestroyer.FindByZipFileID(ctx, f.Base().ID) if err != nil { return err } for _, ff := range files { if err := d.FileDestroyer.Destroy(ctx, ff.Base().ID); err != nil { return err } } // destroy contained folders folders, err := d.FolderDestroyer.FindByZipFileID(ctx, f.Base().ID) if err != nil { return err } for _, ff := range folders { if err := d.FolderDestroyer.Destroy(ctx, ff.ID); err != nil { return err } } if err := d.FileDestroyer.Destroy(ctx, f.Base().ID); err != nil { return err } if deleteFile { if err := fileDeleter.Files([]string{f.Base().Path}); err != nil { return err } } return nil }