diff --git a/server/internal/api/routes.go b/server/internal/api/routes.go index 9b37986..af4df5b 100644 --- a/server/internal/api/routes.go +++ b/server/internal/api/routes.go @@ -12,7 +12,7 @@ import ( ) // SetupRoutes configures the API routes -func SetupRoutes(r chi.Router, db *db.DB, fs *filesystem.FileSystem, authMiddleware *auth.Middleware, sessionService *auth.SessionService) { +func SetupRoutes(r chi.Router, db *db.DB, fs *filesystem.Storage, authMiddleware *auth.Middleware, sessionService *auth.SessionService) { handler := &handlers.Handler{ DB: db, diff --git a/server/internal/filesystem/files.go b/server/internal/filesystem/files.go index 9b876d8..eb5ab16 100644 --- a/server/internal/filesystem/files.go +++ b/server/internal/filesystem/files.go @@ -10,22 +10,29 @@ import ( "strings" ) -// FileNode represents a file or directory in the file system. -type FileNode struct { - ID string `json:"id"` - Name string `json:"name"` - Path string `json:"path"` - Children []FileNode `json:"children,omitempty"` +// StorageNode represents a file or directory in the storage. +type StorageNode struct { + ID string `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Children []StorageNode `json:"children,omitempty"` } // ListFilesRecursively returns a list of all files in the workspace directory and its subdirectories. -func (fs *FileSystem) ListFilesRecursively(userID, workspaceID int) ([]FileNode, error) { - workspacePath := fs.GetWorkspacePath(userID, workspaceID) - return fs.walkDirectory(workspacePath, "") +// Parameters: +// - userID: the ID of the user who owns the workspace +// - workspaceID: the ID of the workspace to list files in +// Returns: +// - nodes: a list of files and directories in the workspace +// - error: any error that occurred during listing +func (s *Storage) ListFilesRecursively(userID, workspaceID int) ([]StorageNode, error) { + workspacePath := s.GetWorkspacePath(userID, workspaceID) + return s.walkDirectory(workspacePath, "") } -func (fs *FileSystem) walkDirectory(dir, prefix string) ([]FileNode, error) { - entries, err := os.ReadDir(dir) +// walkDirectory recursively walks the directory and returns a list of files and directories. +func (s *Storage) walkDirectory(dir, prefix string) ([]StorageNode, error) { + entries, err := s.fs.ReadDir(dir) if err != nil { return nil, err } @@ -49,7 +56,7 @@ func (fs *FileSystem) walkDirectory(dir, prefix string) ([]FileNode, error) { }) // Create combined slice with directories first, then files - nodes := make([]FileNode, 0, len(entries)) + nodes := make([]StorageNode, 0, len(entries)) // Add directories first for _, entry := range dirs { @@ -57,12 +64,12 @@ func (fs *FileSystem) walkDirectory(dir, prefix string) ([]FileNode, error) { path := filepath.Join(prefix, name) fullPath := filepath.Join(dir, name) - children, err := fs.walkDirectory(fullPath, path) + children, err := s.walkDirectory(fullPath, path) if err != nil { return nil, err } - node := FileNode{ + node := StorageNode{ ID: path, Name: name, Path: path, @@ -76,7 +83,7 @@ func (fs *FileSystem) walkDirectory(dir, prefix string) ([]FileNode, error) { name := entry.Name() path := filepath.Join(prefix, name) - node := FileNode{ + node := StorageNode{ ID: path, Name: name, Path: path, @@ -88,9 +95,16 @@ func (fs *FileSystem) walkDirectory(dir, prefix string) ([]FileNode, error) { } // FindFileByName returns a list of file paths that match the given filename. -func (fs *FileSystem) FindFileByName(userID, workspaceID int, filename string) ([]string, error) { +// Parameters: +// - userID: the ID of the user who owns the workspace +// - workspaceID: the ID of the workspace to search for the file +// - filename: the name of the file to search for +// Returns: +// - foundPaths: a list of file paths that match the filename +// - error: any error that occurred during the search +func (s *Storage) FindFileByName(userID, workspaceID int, filename string) ([]string, error) { var foundPaths []string - workspacePath := fs.GetWorkspacePath(userID, workspaceID) + workspacePath := s.GetWorkspacePath(userID, workspaceID) err := filepath.Walk(workspacePath, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -120,36 +134,56 @@ func (fs *FileSystem) FindFileByName(userID, workspaceID int, filename string) ( } // GetFileContent returns the content of the file at the given path. -func (fs *FileSystem) GetFileContent(userID, workspaceID int, filePath string) ([]byte, error) { - fullPath, err := fs.ValidatePath(userID, workspaceID, filePath) +// Parameters: +// - userID: the ID of the user who owns the workspace +// - workspaceID: the ID of the workspace to get the file from +// - filePath: the path of the file to get +// Returns: +// - content: the content of the file +// - error: any error that occurred during reading +func (s *Storage) GetFileContent(userID, workspaceID int, filePath string) ([]byte, error) { + fullPath, err := s.ValidatePath(userID, workspaceID, filePath) if err != nil { return nil, err } - return os.ReadFile(fullPath) + return s.fs.ReadFile(fullPath) } // SaveFile writes the content to the file at the given path. -func (fs *FileSystem) SaveFile(userID, workspaceID int, filePath string, content []byte) error { - fullPath, err := fs.ValidatePath(userID, workspaceID, filePath) +// Parameters: +// - userID: the ID of the user who owns the workspace +// - workspaceID: the ID of the workspace to save the file to +// - filePath: the path of the file to save +// - content: the content to write to the file +// Returns: +// - error: any error that occurred during saving +func (s *Storage) SaveFile(userID, workspaceID int, filePath string, content []byte) error { + fullPath, err := s.ValidatePath(userID, workspaceID, filePath) if err != nil { return err } dir := filepath.Dir(fullPath) - if err := os.MkdirAll(dir, 0755); err != nil { + if err := s.fs.MkdirAll(dir, 0755); err != nil { return err } - return os.WriteFile(fullPath, content, 0644) + return s.fs.WriteFile(fullPath, content, 0644) } // DeleteFile deletes the file at the given path. -func (fs *FileSystem) DeleteFile(userID, workspaceID int, filePath string) error { - fullPath, err := fs.ValidatePath(userID, workspaceID, filePath) +// Parameters: +// - userID: the ID of the user who owns the workspace +// - workspaceID: the ID of the workspace to delete the file from +// - filePath: the path of the file to delete +// Returns: +// - error: any error that occurred during deletion +func (s *Storage) DeleteFile(userID, workspaceID int, filePath string) error { + fullPath, err := s.ValidatePath(userID, workspaceID, filePath) if err != nil { return err } - return os.Remove(fullPath) + return s.fs.Remove(fullPath) } // FileCountStats holds statistics about files in a workspace @@ -165,19 +199,27 @@ type FileCountStats struct { // Returns: // - result: statistics about the files in the workspace // - error: any error that occurred during counting -func (fs *FileSystem) GetFileStats(userID, workspaceID int) (*FileCountStats, error) { - workspacePath := fs.GetWorkspacePath(userID, workspaceID) +func (s *Storage) GetFileStats(userID, workspaceID int) (*FileCountStats, error) { + workspacePath := s.GetWorkspacePath(userID, workspaceID) // Check if workspace exists - if _, err := os.Stat(workspacePath); os.IsNotExist(err) { + if _, err := s.fs.Stat(workspacePath); s.fs.IsNotExist(err) { return nil, fmt.Errorf("workspace directory does not exist") } - return fs.countFilesInPath(workspacePath) + return s.countFilesInPath(workspacePath) } -func (fs *FileSystem) countFilesInPath(directoryPath string) (*FileCountStats, error) { +// GetTotalFileStats returns the total file statistics for the storage. +// Returns: +// - result: statistics about the files in the storage +func (s *Storage) GetTotalFileStats() (*FileCountStats, error) { + return s.countFilesInPath(s.RootDir) +} + +// countFilesInPath counts the total number of files and the total size of files in the given directory. +func (s *Storage) countFilesInPath(directoryPath string) (*FileCountStats, error) { result := &FileCountStats{} err := filepath.WalkDir(directoryPath, func(path string, d os.DirEntry, err error) error { diff --git a/server/internal/filesystem/filesystem.go b/server/internal/filesystem/filesystem.go index 35f5637..a08a678 100644 --- a/server/internal/filesystem/filesystem.go +++ b/server/internal/filesystem/filesystem.go @@ -2,28 +2,65 @@ package filesystem import ( "fmt" + "io/fs" "novamd/internal/gitutils" + "os" "path/filepath" "strings" ) -// FileSystem represents the file system structure. -type FileSystem struct { +// fileSystem defines the interface for filesystem operations +type fileSystem interface { + ReadFile(path string) ([]byte, error) + WriteFile(path string, data []byte, perm fs.FileMode) error + Remove(path string) error + MkdirAll(path string, perm fs.FileMode) error + RemoveAll(path string) error + ReadDir(path string) ([]fs.DirEntry, error) + Stat(path string) (fs.FileInfo, error) + IsNotExist(err error) bool +} + +// Storage represents the file system structure. +type Storage struct { + fs fileSystem RootDir string GitRepos map[int]map[int]*gitutils.GitRepo // map[userID]map[workspaceID]*gitutils.GitRepo } -// New creates a new FileSystem instance. -func New(rootDir string) *FileSystem { - return &FileSystem{ +// New creates a new Storage instance. +// Parameters: +// - rootDir: the root directory for the storage +// Returns: +// - result: the new Storage instance +func New(rootDir string) *Storage { + return NewWithFS(rootDir, &osFS{}) +} + +// NewWithFS creates a new Storage instance with the given filesystem. +// Parameters: +// - rootDir: the root directory for the storage +// - fs: the filesystem implementation to use +// Returns: +// - result: the new Storage instance +func NewWithFS(rootDir string, fs fileSystem) *Storage { + return &Storage{ + fs: fs, RootDir: rootDir, GitRepos: make(map[int]map[int]*gitutils.GitRepo), } } // ValidatePath validates the given path and returns the cleaned path if it is valid. -func (fs *FileSystem) ValidatePath(userID, workspaceID int, path string) (string, error) { - workspacePath := fs.GetWorkspacePath(userID, workspaceID) +// Parameters: +// - userID: the ID of the user who owns the workspace +// - workspaceID: the ID of the workspace to validate the path for +// - path: the path to validate +// Returns: +// - result: the cleaned path if it is valid +// - error: any error that occurred during validation +func (s *Storage) ValidatePath(userID, workspaceID int, path string) (string, error) { + workspacePath := s.GetWorkspacePath(userID, workspaceID) fullPath := filepath.Join(workspacePath, path) cleanPath := filepath.Clean(fullPath) @@ -34,7 +71,31 @@ func (fs *FileSystem) ValidatePath(userID, workspaceID int, path string) (string return cleanPath, nil } -// GetTotalFileStats returns the total file statistics for the file system. -func (fs *FileSystem) GetTotalFileStats() (*FileCountStats, error) { - return fs.countFilesInPath(fs.RootDir) +// osFS implements the FileSystem interface using the real filesystem. +type osFS struct{} + +// ReadFile reads the file at the given path. +func (f *osFS) ReadFile(path string) ([]byte, error) { return os.ReadFile(path) } + +// WriteFile writes the given data to the file at the given path. +func (f *osFS) WriteFile(path string, data []byte, perm fs.FileMode) error { + return os.WriteFile(path, data, perm) } + +// Remove deletes the file at the given path. +func (f *osFS) Remove(path string) error { return os.Remove(path) } + +// MkdirAll creates the directory at the given path and any necessary parents. +func (f *osFS) MkdirAll(path string, perm fs.FileMode) error { return os.MkdirAll(path, perm) } + +// RemoveAll removes the file or directory at the given path. +func (f *osFS) RemoveAll(path string) error { return os.RemoveAll(path) } + +// ReadDir reads the directory at the given path. +func (f *osFS) ReadDir(path string) ([]fs.DirEntry, error) { return os.ReadDir(path) } + +// Stat returns the FileInfo for the file at the given path. +func (f *osFS) Stat(path string) (fs.FileInfo, error) { return os.Stat(path) } + +// IsNotExist returns true if the error is a "file does not exist" error. +func (f *osFS) IsNotExist(err error) bool { return os.IsNotExist(err) } diff --git a/server/internal/filesystem/git.go b/server/internal/filesystem/git.go index de83ad9..f70d7ad 100644 --- a/server/internal/filesystem/git.go +++ b/server/internal/filesystem/git.go @@ -6,28 +6,45 @@ import ( ) // SetupGitRepo sets up a Git repository for the given user and workspace IDs. -func (fs *FileSystem) SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToken string) error { - workspacePath := fs.GetWorkspacePath(userID, workspaceID) - if _, ok := fs.GitRepos[userID]; !ok { - fs.GitRepos[userID] = make(map[int]*gitutils.GitRepo) +// Parameters: +// - userID: the ID of the user who owns the workspace +// - workspaceID: the ID of the workspace to set up the Git repository for +// - gitURL: the URL of the Git repository +// - gitUser: the username for the Git repository +// - gitToken: the access token for the Git repository +// Returns: +// - error: any error that occurred during setup +func (s *Storage) SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToken string) error { + workspacePath := s.GetWorkspacePath(userID, workspaceID) + if _, ok := s.GitRepos[userID]; !ok { + s.GitRepos[userID] = make(map[int]*gitutils.GitRepo) } - fs.GitRepos[userID][workspaceID] = gitutils.New(gitURL, gitUser, gitToken, workspacePath) - return fs.GitRepos[userID][workspaceID].EnsureRepo() + s.GitRepos[userID][workspaceID] = gitutils.New(gitURL, gitUser, gitToken, workspacePath) + return s.GitRepos[userID][workspaceID].EnsureRepo() } // DisableGitRepo disables the Git repository for the given user and workspace IDs. -func (fs *FileSystem) DisableGitRepo(userID, workspaceID int) { - if userRepos, ok := fs.GitRepos[userID]; ok { +// Parameters: +// - userID: the ID of the user who owns the workspace +// - workspaceID: the ID of the workspace to disable the Git repository for +func (s *Storage) DisableGitRepo(userID, workspaceID int) { + if userRepos, ok := s.GitRepos[userID]; ok { delete(userRepos, workspaceID) if len(userRepos) == 0 { - delete(fs.GitRepos, userID) + delete(s.GitRepos, userID) } } } // StageCommitAndPush stages, commits, and pushes the changes to the Git repository. -func (fs *FileSystem) StageCommitAndPush(userID, workspaceID int, message string) error { - repo, ok := fs.getGitRepo(userID, workspaceID) +// Parameters: +// - userID: the ID of the user who owns the workspace +// - workspaceID: the ID of the workspace to commit and push +// - message: the commit message +// Returns: +// - error: any error that occurred during the operation +func (s *Storage) StageCommitAndPush(userID, workspaceID int, message string) error { + repo, ok := s.getGitRepo(userID, workspaceID) if !ok { return fmt.Errorf("git settings not configured for this workspace") } @@ -40,8 +57,13 @@ func (fs *FileSystem) StageCommitAndPush(userID, workspaceID int, message string } // Pull pulls the changes from the remote Git repository. -func (fs *FileSystem) Pull(userID, workspaceID int) error { - repo, ok := fs.getGitRepo(userID, workspaceID) +// Parameters: +// - userID: the ID of the user who owns the workspace +// - workspaceID: the ID of the workspace to pull changes for +// Returns: +// - error: any error that occurred during the operation +func (s *Storage) Pull(userID, workspaceID int) error { + repo, ok := s.getGitRepo(userID, workspaceID) if !ok { return fmt.Errorf("git settings not configured for this workspace") } @@ -50,8 +72,8 @@ func (fs *FileSystem) Pull(userID, workspaceID int) error { } // getGitRepo returns the Git repository for the given user and workspace IDs. -func (fs *FileSystem) getGitRepo(userID, workspaceID int) (*gitutils.GitRepo, bool) { - userRepos, ok := fs.GitRepos[userID] +func (s *Storage) getGitRepo(userID, workspaceID int) (*gitutils.GitRepo, bool) { + userRepos, ok := s.GitRepos[userID] if !ok { return nil, false } diff --git a/server/internal/filesystem/workspace.go b/server/internal/filesystem/workspace.go index 122c816..891eaae 100644 --- a/server/internal/filesystem/workspace.go +++ b/server/internal/filesystem/workspace.go @@ -2,19 +2,28 @@ package filesystem import ( "fmt" - "os" "path/filepath" ) // GetWorkspacePath returns the path to the workspace directory for the given user and workspace IDs. -func (fs *FileSystem) GetWorkspacePath(userID, workspaceID int) string { - return filepath.Join(fs.RootDir, fmt.Sprintf("%d", userID), fmt.Sprintf("%d", workspaceID)) +// Parameters: +// - userID: the ID of the user who owns the workspace +// - workspaceID: the ID of the workspace +// Returns: +// - result: the path to the workspace directory +func (s *Storage) GetWorkspacePath(userID, workspaceID int) string { + return filepath.Join(s.RootDir, fmt.Sprintf("%d", userID), fmt.Sprintf("%d", workspaceID)) } // InitializeUserWorkspace creates the workspace directory for the given user and workspace IDs. -func (fs *FileSystem) InitializeUserWorkspace(userID, workspaceID int) error { - workspacePath := fs.GetWorkspacePath(userID, workspaceID) - err := os.MkdirAll(workspacePath, 0755) +// Parameters: +// - userID: the ID of the user who owns the workspace +// - workspaceID: the ID of the workspace to initialize +// Returns: +// - error: any error that occurred during the operation +func (s *Storage) InitializeUserWorkspace(userID, workspaceID int) error { + workspacePath := s.GetWorkspacePath(userID, workspaceID) + err := s.fs.MkdirAll(workspacePath, 0755) if err != nil { return fmt.Errorf("failed to create workspace directory: %w", err) } @@ -23,18 +32,17 @@ func (fs *FileSystem) InitializeUserWorkspace(userID, workspaceID int) error { } // DeleteUserWorkspace deletes the workspace directory for the given user and workspace IDs. -func (fs *FileSystem) DeleteUserWorkspace(userID, workspaceID int) error { - workspacePath := fs.GetWorkspacePath(userID, workspaceID) - err := os.RemoveAll(workspacePath) +// Parameters: +// - userID: the ID of the user who owns the workspace +// - workspaceID: the ID of the workspace to delete +// Returns: +// - error: any error that occurred during the operation +func (s *Storage) DeleteUserWorkspace(userID, workspaceID int) error { + workspacePath := s.GetWorkspacePath(userID, workspaceID) + err := s.fs.RemoveAll(workspacePath) if err != nil { return fmt.Errorf("failed to delete workspace directory: %w", err) } return nil } - -// CreateWorkspaceDirectory creates the workspace directory for the given user and workspace IDs. -func (fs *FileSystem) CreateWorkspaceDirectory(userID, workspaceID int) error { - dir := fs.GetWorkspacePath(userID, workspaceID) - return os.MkdirAll(dir, 0755) -} diff --git a/server/internal/handlers/handlers.go b/server/internal/handlers/handlers.go index a9d4e75..c96ccac 100644 --- a/server/internal/handlers/handlers.go +++ b/server/internal/handlers/handlers.go @@ -10,11 +10,11 @@ import ( // Handler provides common functionality for all handlers type Handler struct { DB *db.DB - FS *filesystem.FileSystem + FS *filesystem.Storage } // NewHandler creates a new handler with the given dependencies -func NewHandler(db *db.DB, fs *filesystem.FileSystem) *Handler { +func NewHandler(db *db.DB, fs *filesystem.Storage) *Handler { return &Handler{ DB: db, FS: fs, diff --git a/server/internal/user/user.go b/server/internal/user/user.go index fb83cc1..530833e 100644 --- a/server/internal/user/user.go +++ b/server/internal/user/user.go @@ -14,10 +14,10 @@ import ( type UserService struct { DB *db.DB - FS *filesystem.FileSystem + FS *filesystem.Storage } -func NewUserService(database *db.DB, fs *filesystem.FileSystem) *UserService { +func NewUserService(database *db.DB, fs *filesystem.Storage) *UserService { return &UserService{ DB: database, FS: fs,