Implement MoveFile functionality in FileManager and corresponding tests

This commit is contained in:
2025-07-11 19:49:08 +02:00
parent 5a6895ecdc
commit 9bb95f603c
4 changed files with 158 additions and 0 deletions

View File

@@ -14,6 +14,7 @@ type FileManager interface {
FindFileByName(userID, workspaceID int, filename string) ([]string, error) FindFileByName(userID, workspaceID int, filename string) ([]string, error)
GetFileContent(userID, workspaceID int, filePath string) ([]byte, error) GetFileContent(userID, workspaceID int, filePath string) ([]byte, error)
SaveFile(userID, workspaceID int, filePath string, content []byte) error SaveFile(userID, workspaceID int, filePath string, content []byte) error
MoveFile(userID, workspaceID int, srcPath string, dstPath string) error
DeleteFile(userID, workspaceID int, filePath string) error DeleteFile(userID, workspaceID int, filePath string) error
GetFileStats(userID, workspaceID int) (*FileCountStats, error) GetFileStats(userID, workspaceID int) (*FileCountStats, error)
GetTotalFileStats() (*FileCountStats, error) GetTotalFileStats() (*FileCountStats, error)
@@ -174,6 +175,34 @@ func (s *Service) SaveFile(userID, workspaceID int, filePath string, content []b
return nil return nil
} }
// MoveFile moves a file from srcPath to dstPath within the workspace directory.
// Both paths must be relative to the workspace directory given by userID and workspaceID.
// If the destination file already exists, it will be overwritten.
func (s *Service) MoveFile(userID, workspaceID int, srcPath string, dstPath string) error {
log := getLogger()
srcFullPath, err := s.ValidatePath(userID, workspaceID, srcPath)
if err != nil {
return err
}
dstFullPath, err := s.ValidatePath(userID, workspaceID, dstPath)
if err != nil {
return err
}
if err := s.fs.MoveFile(srcFullPath, dstFullPath); err != nil {
return fmt.Errorf("failed to move file: %w", err)
}
log.Debug("file moved",
"userID", userID,
"workspaceID", workspaceID,
"src", srcPath,
"dst", dstPath)
return nil
}
// DeleteFile deletes the file at the given filePath. // DeleteFile deletes the file at the given filePath.
// Path must be a relative path within the workspace directory given by userID and workspaceID. // Path must be a relative path within the workspace directory given by userID and workspaceID.
func (s *Service) DeleteFile(userID, workspaceID int, filePath string) error { func (s *Service) DeleteFile(userID, workspaceID int, filePath string) error {

View File

@@ -407,3 +407,105 @@ func TestDeleteFile(t *testing.T) {
}) })
} }
} }
func TestMoveFile(t *testing.T) {
mockFS := NewMockFS()
s := storage.NewServiceWithOptions("test-root", storage.Options{
Fs: mockFS,
NewGitClient: nil,
})
testCases := []struct {
name string
userID int
workspaceID int
srcPath string
dstPath string
mockErr error
wantErr bool
}{
{
name: "successful move",
userID: 1,
workspaceID: 1,
srcPath: "test.md",
dstPath: "moved.md",
mockErr: nil,
wantErr: false,
},
{
name: "move to subdirectory",
userID: 1,
workspaceID: 1,
srcPath: "test.md",
dstPath: "subdir/test.md",
mockErr: nil,
wantErr: false,
},
{
name: "invalid source path",
userID: 1,
workspaceID: 1,
srcPath: "../../../etc/passwd",
dstPath: "test.md",
mockErr: nil,
wantErr: true,
},
{
name: "invalid destination path",
userID: 1,
workspaceID: 1,
srcPath: "test.md",
dstPath: "../../../etc/passwd",
mockErr: nil,
wantErr: true,
},
{
name: "filesystem move error",
userID: 1,
workspaceID: 1,
srcPath: "test.md",
dstPath: "moved.md",
mockErr: fs.ErrPermission,
wantErr: true,
},
{
name: "same source and destination",
userID: 1,
workspaceID: 1,
srcPath: "test.md",
dstPath: "test.md",
mockErr: nil,
wantErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockFS.MoveFileError = tc.mockErr
err := s.MoveFile(tc.userID, tc.workspaceID, tc.srcPath, tc.dstPath)
if tc.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expectedSrcPath := filepath.Join("test-root", "1", "1", tc.srcPath)
expectedDstPath := filepath.Join("test-root", "1", "1", tc.dstPath)
if dstPath, ok := mockFS.MoveCalls[expectedSrcPath]; ok {
if dstPath != expectedDstPath {
t.Errorf("move destination = %q, want %q", dstPath, expectedDstPath)
}
} else {
t.Error("expected move call not made")
}
})
}
}

View File

@@ -10,6 +10,7 @@ import (
type fileSystem interface { type fileSystem interface {
ReadFile(path string) ([]byte, error) ReadFile(path string) ([]byte, error)
WriteFile(path string, data []byte, perm fs.FileMode) error WriteFile(path string, data []byte, perm fs.FileMode) error
MoveFile(src, dst string) error
Remove(path string) error Remove(path string) error
MkdirAll(path string, perm fs.FileMode) error MkdirAll(path string, perm fs.FileMode) error
RemoveAll(path string) error RemoveAll(path string) error
@@ -38,6 +39,21 @@ func (f *osFS) WriteFile(path string, data []byte, perm fs.FileMode) error {
return os.WriteFile(path, data, perm) return os.WriteFile(path, data, perm)
} }
// MoveFile moves the file from src to dst, overwriting if necessary.
func (f *osFS) MoveFile(src, dst string) error {
if err := os.Rename(src, dst); err != nil {
if os.IsExist(err) {
// If the destination exists, remove it and try again
if err := os.Remove(dst); err != nil && !os.IsNotExist(err) {
return err
}
return os.Rename(src, dst)
}
return err
}
return nil
}
// Remove deletes the file at the given path. // Remove deletes the file at the given path.
func (f *osFS) Remove(path string) error { return os.Remove(path) } func (f *osFS) Remove(path string) error { return os.Remove(path) }

View File

@@ -43,6 +43,7 @@ type mockFS struct {
// Record operations for verification // Record operations for verification
ReadCalls map[string]int ReadCalls map[string]int
WriteCalls map[string][]byte WriteCalls map[string][]byte
MoveCalls map[string]string
RemoveCalls []string RemoveCalls []string
MkdirCalls []string MkdirCalls []string
@@ -56,6 +57,7 @@ type mockFS struct {
err error err error
} }
WriteFileError error WriteFileError error
MoveFileError error
RemoveError error RemoveError error
MkdirError error MkdirError error
StatError error StatError error
@@ -66,6 +68,7 @@ func NewMockFS() *mockFS {
return &mockFS{ return &mockFS{
ReadCalls: make(map[string]int), ReadCalls: make(map[string]int),
WriteCalls: make(map[string][]byte), WriteCalls: make(map[string][]byte),
MoveCalls: make(map[string]string),
RemoveCalls: make([]string, 0), RemoveCalls: make([]string, 0),
MkdirCalls: make([]string, 0), MkdirCalls: make([]string, 0),
ReadFileReturns: make(map[string]struct { ReadFileReturns: make(map[string]struct {
@@ -88,6 +91,14 @@ func (m *mockFS) WriteFile(path string, data []byte, _ fs.FileMode) error {
return m.WriteFileError return m.WriteFileError
} }
func (m *mockFS) MoveFile(src, dst string) error {
m.MoveCalls[src] = dst
if src == dst {
return nil // No-op if source and destination are the same
}
return m.MoveFileError
}
func (m *mockFS) Remove(path string) error { func (m *mockFS) Remove(path string) error {
m.RemoveCalls = append(m.RemoveCalls, path) m.RemoveCalls = append(m.RemoveCalls, path)
return m.RemoveError return m.RemoveError