mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Refactor file deletion (#1954)
* Add file deleter * Change scene delete code * Add image/gallery delete code * Don't remove stash library paths * Fail silently if file does not exist
This commit is contained in:
161
pkg/file/delete.go
Normal file
161
pkg/file/delete.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user