mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 23:44:22 +00:00
Implement MoveFile functionality in FileManager and corresponding tests
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user