2023-03-21 20:57:26 +00:00
|
|
|
package file
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io/fs"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2023-03-28 22:28:11 +00:00
|
|
|
"strings"
|
2023-03-21 20:57:26 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/stashapp/stash/pkg/logger"
|
|
|
|
"github.com/stashapp/stash/pkg/txn"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Renamer interface {
|
|
|
|
Rename(oldpath, newpath string) error
|
|
|
|
}
|
|
|
|
|
|
|
|
type Statter interface {
|
|
|
|
Stat(name string) (fs.FileInfo, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
type DirMakerStatRenamer interface {
|
|
|
|
Statter
|
|
|
|
Renamer
|
|
|
|
Mkdir(name string, perm os.FileMode) error
|
|
|
|
Remove(name string) error
|
|
|
|
}
|
|
|
|
|
|
|
|
type folderCreatorStatRenamerImpl struct {
|
|
|
|
renamerRemoverImpl
|
|
|
|
mkDirFn func(name string, perm os.FileMode) error
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r folderCreatorStatRenamerImpl) Mkdir(name string, perm os.FileMode) error {
|
|
|
|
return r.mkDirFn(name, perm)
|
|
|
|
}
|
|
|
|
|
|
|
|
type Mover struct {
|
|
|
|
Renamer DirMakerStatRenamer
|
2023-03-28 22:28:11 +00:00
|
|
|
Files GetterUpdater
|
|
|
|
Folders FolderStore
|
2023-03-21 20:57:26 +00:00
|
|
|
|
|
|
|
moved map[string]string
|
|
|
|
foldersCreated []string
|
|
|
|
}
|
|
|
|
|
2023-03-28 22:28:11 +00:00
|
|
|
func NewMover(fileStore GetterUpdater, folderStore FolderStore) *Mover {
|
2023-03-21 20:57:26 +00:00
|
|
|
return &Mover{
|
2023-03-28 22:28:11 +00:00
|
|
|
Files: fileStore,
|
|
|
|
Folders: folderStore,
|
2023-03-21 20:57:26 +00:00
|
|
|
Renamer: &folderCreatorStatRenamerImpl{
|
|
|
|
renamerRemoverImpl: newRenamerRemoverImpl(),
|
|
|
|
mkDirFn: os.Mkdir,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Move moves the file to the given folder and basename. If basename is empty, then the existing basename is used.
|
|
|
|
// Assumes that the parent folder exists in the filesystem.
|
|
|
|
func (m *Mover) Move(ctx context.Context, f File, folder *Folder, basename string) error {
|
|
|
|
fBase := f.Base()
|
|
|
|
|
|
|
|
// don't allow moving files in zip files
|
|
|
|
if fBase.ZipFileID != nil {
|
2023-03-28 22:28:11 +00:00
|
|
|
return fmt.Errorf("cannot move file %s, is in a zip file", fBase.Path)
|
2023-03-21 20:57:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if basename == "" {
|
|
|
|
basename = fBase.Basename
|
|
|
|
}
|
|
|
|
|
|
|
|
// modify the database first
|
|
|
|
|
|
|
|
oldPath := fBase.Path
|
|
|
|
|
|
|
|
if folder.ID == fBase.ParentFolderID && (basename == "" || basename == fBase.Basename) {
|
|
|
|
// nothing to do
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ensure that the new path doesn't already exist
|
|
|
|
newPath := filepath.Join(folder.Path, basename)
|
|
|
|
if _, err := m.Renamer.Stat(newPath); !errors.Is(err, fs.ErrNotExist) {
|
|
|
|
return fmt.Errorf("file %s already exists", newPath)
|
|
|
|
}
|
|
|
|
|
2023-04-19 03:06:53 +00:00
|
|
|
if err := TransferZipFolderHierarchy(ctx, m.Folders, fBase.ID, oldPath, newPath); err != nil {
|
2023-03-28 22:28:11 +00:00
|
|
|
return fmt.Errorf("moving folder hierarchy for file %s: %w", fBase.Path, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// move contained files if file is a zip file
|
|
|
|
zipFiles, err := m.Files.FindByZipFileID(ctx, fBase.ID)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("finding contained files in file %s: %w", fBase.Path, err)
|
|
|
|
}
|
|
|
|
for _, zf := range zipFiles {
|
|
|
|
zfBase := zf.Base()
|
|
|
|
oldZfPath := zfBase.Path
|
|
|
|
oldZfDir := filepath.Dir(oldZfPath)
|
|
|
|
|
|
|
|
// sanity check - ignore files which aren't under oldPath
|
|
|
|
if !strings.HasPrefix(oldZfPath, oldPath) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
relZfDir, err := filepath.Rel(oldPath, oldZfDir)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("moving contained file %s: %w", zfBase.ID, err)
|
|
|
|
}
|
|
|
|
newZfDir := filepath.Join(newPath, relZfDir)
|
|
|
|
|
|
|
|
// folder should have been created by moveZipFolderHierarchy
|
|
|
|
newZfFolder, err := GetOrCreateFolderHierarchy(ctx, m.Folders, newZfDir)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("getting or creating folder hierarchy: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// update file parent folder
|
|
|
|
zfBase.ParentFolderID = newZfFolder.ID
|
|
|
|
if err := m.Files.Update(ctx, zf); err != nil {
|
|
|
|
return fmt.Errorf("updating file %s: %w", oldZfPath, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-21 20:57:26 +00:00
|
|
|
fBase.ParentFolderID = folder.ID
|
|
|
|
fBase.Basename = basename
|
|
|
|
fBase.UpdatedAt = time.Now()
|
|
|
|
// leave ModTime as is. It may or may not be changed by this operation
|
|
|
|
|
2023-03-28 22:28:11 +00:00
|
|
|
if err := m.Files.Update(ctx, f); err != nil {
|
2023-03-21 20:57:26 +00:00
|
|
|
return fmt.Errorf("updating file %s: %w", oldPath, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// then move the file
|
|
|
|
return m.moveFile(oldPath, newPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Mover) CreateFolderHierarchy(path string) error {
|
|
|
|
info, err := m.Renamer.Stat(path)
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
|
|
// create the parent folder
|
|
|
|
parentPath := filepath.Dir(path)
|
|
|
|
if err := m.CreateFolderHierarchy(parentPath); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// create the folder
|
|
|
|
if err := m.Renamer.Mkdir(path, 0755); err != nil {
|
|
|
|
return fmt.Errorf("creating folder %s: %w", path, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
m.foldersCreated = append(m.foldersCreated, path)
|
|
|
|
} else {
|
|
|
|
return fmt.Errorf("getting info for %s: %w", path, err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if !info.IsDir() {
|
|
|
|
return fmt.Errorf("%s is not a directory", path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Mover) moveFile(oldPath, newPath string) error {
|
|
|
|
if err := m.Renamer.Rename(oldPath, newPath); err != nil {
|
|
|
|
return fmt.Errorf("renaming file %s to %s: %w", oldPath, newPath, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if m.moved == nil {
|
|
|
|
m.moved = make(map[string]string)
|
|
|
|
}
|
|
|
|
|
|
|
|
m.moved[newPath] = oldPath
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Mover) RegisterHooks(ctx context.Context, mgr txn.Manager) {
|
|
|
|
txn.AddPostCommitHook(ctx, func(ctx context.Context) {
|
|
|
|
m.commit()
|
|
|
|
})
|
|
|
|
|
|
|
|
txn.AddPostRollbackHook(ctx, func(ctx context.Context) {
|
|
|
|
m.rollback()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Mover) commit() {
|
|
|
|
m.moved = nil
|
|
|
|
m.foldersCreated = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *Mover) rollback() {
|
|
|
|
// move files back to their original location
|
|
|
|
for newPath, oldPath := range m.moved {
|
|
|
|
if err := m.Renamer.Rename(newPath, oldPath); err != nil {
|
|
|
|
logger.Errorf("error moving file %s back to %s: %s", newPath, oldPath, err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove folders created in reverse order
|
|
|
|
for i := len(m.foldersCreated) - 1; i >= 0; i-- {
|
|
|
|
folder := m.foldersCreated[i]
|
|
|
|
if err := m.Renamer.Remove(folder); err != nil {
|
|
|
|
logger.Errorf("error removing folder %s: %s", folder, err.Error())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|