diff --git a/internal/api/resolver_mutation_file.go b/internal/api/resolver_mutation_file.go index 4bc056e058e..0b8b84ea0a2 100644 --- a/internal/api/resolver_mutation_file.go +++ b/internal/api/resolver_mutation_file.go @@ -13,8 +13,9 @@ import ( func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput) (bool, error) { if err := r.withTxn(ctx, func(ctx context.Context) error { - qb := r.repository.File - mover := file.NewMover(qb) + fileStore := r.repository.File + folderStore := r.repository.Folder + mover := file.NewMover(fileStore, folderStore) mover.RegisterHooks(ctx, r.txnManager) var ( @@ -36,7 +37,7 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput) return fmt.Errorf("invalid folder id %s: %w", *input.DestinationFolderID, err) } - folder, err = r.repository.Folder.Find(ctx, file.FolderID(folderID)) + folder, err = folderStore.Find(ctx, file.FolderID(folderID)) if err != nil { return fmt.Errorf("finding destination folder: %w", err) } @@ -44,6 +45,10 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput) if folder == nil { return fmt.Errorf("folder with id %d not found", input.DestinationFolderID) } + + if folder.ZipFileID != nil { + return fmt.Errorf("cannot move to %s, is in a zip file", folder.Path) + } case input.DestinationFolder != nil: folderPath := *input.DestinationFolder @@ -54,7 +59,7 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput) // get or create folder hierarchy var err error - folder, err = file.GetOrCreateFolderHierarchy(ctx, r.repository.Folder, folderPath) + folder, err = file.GetOrCreateFolderHierarchy(ctx, folderStore, folderPath) if err != nil { return fmt.Errorf("getting or creating folder hierarchy: %w", err) } @@ -78,7 +83,7 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput) for _, fileIDInt := range fileIDs { fileID := file.ID(fileIDInt) - f, err := qb.Find(ctx, fileID) + f, err := fileStore.Find(ctx, fileID) if err != nil { return fmt.Errorf("finding file %d: %w", fileID, err) } diff --git a/internal/manager/repository.go b/internal/manager/repository.go index c6ea17f8542..41ac5f12ed5 100644 --- a/internal/manager/repository.go +++ b/internal/manager/repository.go @@ -35,7 +35,6 @@ type SceneReaderWriter interface { type FileReaderWriter interface { file.Store - file.Finder Query(ctx context.Context, options models.FileQueryOptions) (*models.FileQueryResult, error) GetCaptions(ctx context.Context, fileID file.ID) ([]*models.VideoCaption, error) IsPrimary(ctx context.Context, fileID file.ID) (bool, error) @@ -43,7 +42,6 @@ type FileReaderWriter interface { type FolderReaderWriter interface { file.FolderStore - Find(ctx context.Context, id file.FolderID) (*file.Folder, error) } type Repository struct { diff --git a/pkg/file/file.go b/pkg/file/file.go index 55b1f2e676e..445edba9e12 100644 --- a/pkg/file/file.go +++ b/pkg/file/file.go @@ -180,6 +180,11 @@ type Destroyer interface { Destroy(ctx context.Context, id ID) error } +type GetterUpdater interface { + Getter + Updater +} + type GetterDestroyer interface { Getter Destroyer diff --git a/pkg/file/folder.go b/pkg/file/folder.go index 75d5716e376..719d1a1f93b 100644 --- a/pkg/file/folder.go +++ b/pkg/file/folder.go @@ -32,6 +32,10 @@ func (f *Folder) Info(fs FS) (fs.FileInfo, error) { return f.info(fs, f.Path) } +type FolderFinder interface { + Find(ctx context.Context, id FolderID) (*Folder, error) +} + // FolderPathFinder finds Folders by their path. type FolderPathFinder interface { FindByPath(ctx context.Context, path string) (*Folder, error) @@ -39,6 +43,7 @@ type FolderPathFinder interface { // FolderGetter provides methods to find Folders. type FolderGetter interface { + FolderFinder FolderPathFinder FindByZipFileID(ctx context.Context, zipFileID ID) ([]*Folder, error) FindAllInPaths(ctx context.Context, p []string, limit, offset int) ([]*Folder, error) diff --git a/pkg/file/move.go b/pkg/file/move.go index f965489ef95..3e29e328cec 100644 --- a/pkg/file/move.go +++ b/pkg/file/move.go @@ -7,6 +7,7 @@ import ( "io/fs" "os" "path/filepath" + "strings" "time" "github.com/stashapp/stash/pkg/logger" @@ -39,15 +40,17 @@ func (r folderCreatorStatRenamerImpl) Mkdir(name string, perm os.FileMode) error type Mover struct { Renamer DirMakerStatRenamer - Updater Updater + Files GetterUpdater + Folders FolderStore moved map[string]string foldersCreated []string } -func NewMover(u Updater) *Mover { +func NewMover(fileStore GetterUpdater, folderStore FolderStore) *Mover { return &Mover{ - Updater: u, + Files: fileStore, + Folders: folderStore, Renamer: &folderCreatorStatRenamerImpl{ renamerRemoverImpl: newRenamerRemoverImpl(), mkDirFn: os.Mkdir, @@ -62,7 +65,7 @@ func (m *Mover) Move(ctx context.Context, f File, folder *Folder, basename strin // don't allow moving files in zip files if fBase.ZipFileID != nil { - return fmt.Errorf("cannot move file %s in zip file", f.Base().Path) + return fmt.Errorf("cannot move file %s, is in a zip file", fBase.Path) } if basename == "" { @@ -84,12 +87,50 @@ func (m *Mover) Move(ctx context.Context, f File, folder *Folder, basename strin return fmt.Errorf("file %s already exists", newPath) } + if err := m.transferZipFolderHierarchy(ctx, fBase.ID, oldPath, newPath); err != nil { + 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) + } + } + 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 - if err := m.Updater.Update(ctx, f); err != nil { + if err := m.Files.Update(ctx, f); err != nil { return fmt.Errorf("updating file %s: %w", oldPath, err) } @@ -125,6 +166,49 @@ func (m *Mover) CreateFolderHierarchy(path string) error { return nil } +// transferZipFolderHierarchy creates the folder hierarchy for zipFileID under newPath, and removes +// ZipFileID from folders under oldPath. +func (m *Mover) transferZipFolderHierarchy(ctx context.Context, zipFileID ID, oldPath string, newPath string) error { + zipFolders, err := m.Folders.FindByZipFileID(ctx, zipFileID) + if err != nil { + return err + } + + for _, oldFolder := range zipFolders { + oldZfPath := oldFolder.Path + + // sanity check - ignore folders which aren't under oldPath + if !strings.HasPrefix(oldZfPath, oldPath) { + continue + } + + relZfPath, err := filepath.Rel(oldPath, oldZfPath) + if err != nil { + return err + } + newZfPath := filepath.Join(newPath, relZfPath) + + newFolder, err := GetOrCreateFolderHierarchy(ctx, m.Folders, newZfPath) + if err != nil { + return err + } + + // add ZipFileID to new folder + newFolder.ZipFileID = &zipFileID + if err = m.Folders.Update(ctx, newFolder); err != nil { + return err + } + + // remove ZipFileID from old folder + oldFolder.ZipFileID = nil + if err = m.Folders.Update(ctx, oldFolder); err != nil { + return err + } + } + + 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)