From e4510298ed697f8d1cc44f7c05cc3bbe09ccbb80 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Thu, 14 Nov 2024 21:13:45 +0100 Subject: [PATCH] Rename filesystem interfaces and structs --- server/cmd/server/main.go | 6 +- server/internal/api/routes.go | 8 +-- server/internal/filesystem/storage.go | 58 ------------------- server/internal/handlers/admin_handlers.go | 12 ++-- server/internal/handlers/file_handlers.go | 14 ++--- server/internal/handlers/git_handlers.go | 4 +- server/internal/handlers/handlers.go | 12 ++-- server/internal/handlers/user_handlers.go | 2 +- .../internal/handlers/workspace_handlers.go | 6 +- .../internal/{filesystem => storage}/files.go | 51 +++++++++------- .../{filesystem => storage}/filesystem.go | 2 +- server/internal/storage/filesystem_test.go | 46 +++++++++++++++ .../internal/{filesystem => storage}/git.go | 20 +++++-- server/internal/storage/service.go | 42 ++++++++++++++ .../{filesystem => storage}/workspace.go | 37 ++++++++++-- server/internal/user/user.go | 14 ++--- 16 files changed, 206 insertions(+), 128 deletions(-) delete mode 100644 server/internal/filesystem/storage.go rename server/internal/{filesystem => storage}/files.go (80%) rename server/internal/{filesystem => storage}/filesystem.go (98%) create mode 100644 server/internal/storage/filesystem_test.go rename server/internal/{filesystem => storage}/git.go (78%) create mode 100644 server/internal/storage/service.go rename server/internal/{filesystem => storage}/workspace.go (51%) diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go index 96cdda5..c6204ba 100644 --- a/server/cmd/server/main.go +++ b/server/cmd/server/main.go @@ -17,8 +17,8 @@ import ( "novamd/internal/auth" "novamd/internal/config" "novamd/internal/db" - "novamd/internal/filesystem" "novamd/internal/handlers" + "novamd/internal/storage" ) func main() { @@ -45,7 +45,7 @@ func main() { } // Initialize filesystem - fs := filesystem.New(cfg.WorkDir) + s := storage.NewService(cfg.WorkDir) // Initialize JWT service jwtService, err := auth.NewJWTService(auth.JWTConfig{ @@ -95,7 +95,7 @@ func main() { // Set up routes r.Route("/api/v1", func(r chi.Router) { r.Use(httprate.LimitByIP(cfg.RateLimitRequests, cfg.RateLimitWindow)) - api.SetupRoutes(r, database, fs, authMiddleware, sessionService) + api.SetupRoutes(r, database, s, authMiddleware, sessionService) }) // Handle all other routes with static file server diff --git a/server/internal/api/routes.go b/server/internal/api/routes.go index ce8d4c3..71a873b 100644 --- a/server/internal/api/routes.go +++ b/server/internal/api/routes.go @@ -4,19 +4,19 @@ package api import ( "novamd/internal/auth" "novamd/internal/db" - "novamd/internal/filesystem" "novamd/internal/handlers" "novamd/internal/middleware" + "novamd/internal/storage" "github.com/go-chi/chi/v5" ) // SetupRoutes configures the API routes -func SetupRoutes(r chi.Router, db *db.DB, s *filesystem.Storage, authMiddleware *auth.Middleware, sessionService *auth.SessionService) { +func SetupRoutes(r chi.Router, db *db.DB, s storage.Manager, authMiddleware *auth.Middleware, sessionService *auth.SessionService) { handler := &handlers.Handler{ - DB: db, - S: s, + DB: db, + Storage: s, } // Public routes (no authentication required) diff --git a/server/internal/filesystem/storage.go b/server/internal/filesystem/storage.go deleted file mode 100644 index 6c8d098..0000000 --- a/server/internal/filesystem/storage.go +++ /dev/null @@ -1,58 +0,0 @@ -package filesystem - -import ( - "fmt" - "novamd/internal/gitutils" - "path/filepath" - "strings" -) - -// 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 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. -// 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) - - if !strings.HasPrefix(cleanPath, workspacePath) { - return "", fmt.Errorf("invalid path: outside of workspace") - } - - return cleanPath, nil -} diff --git a/server/internal/handlers/admin_handlers.go b/server/internal/handlers/admin_handlers.go index f861e5a..059c8cb 100644 --- a/server/internal/handlers/admin_handlers.go +++ b/server/internal/handlers/admin_handlers.go @@ -4,9 +4,9 @@ import ( "encoding/json" "net/http" "novamd/internal/db" - "novamd/internal/filesystem" "novamd/internal/httpcontext" "novamd/internal/models" + "novamd/internal/storage" "strconv" "time" @@ -91,7 +91,7 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc { } // Initialize user workspace - if err := h.S.InitializeUserWorkspace(insertedUser.ID, insertedUser.LastWorkspaceID); err != nil { + if err := h.Storage.InitializeUserWorkspace(insertedUser.ID, insertedUser.LastWorkspaceID); err != nil { http.Error(w, "Failed to initialize user workspace", http.StatusInternalServerError) return } @@ -218,7 +218,7 @@ type WorkspaceStats struct { WorkspaceID int `json:"workspaceID"` WorkspaceName string `json:"workspaceName"` WorkspaceCreatedAt time.Time `json:"workspaceCreatedAt"` - *filesystem.FileCountStats + *storage.FileCountStats } // AdminListWorkspaces returns a list of all workspaces and their stats @@ -248,7 +248,7 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc { workspaceData.WorkspaceName = ws.Name workspaceData.WorkspaceCreatedAt = ws.CreatedAt - fileStats, err := h.S.GetFileStats(ws.UserID, ws.ID) + fileStats, err := h.Storage.GetFileStats(ws.UserID, ws.ID) if err != nil { http.Error(w, "Failed to get file stats", http.StatusInternalServerError) return @@ -266,7 +266,7 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc { // SystemStats holds system-wide statistics type SystemStats struct { *db.UserStats - *filesystem.FileCountStats + *storage.FileCountStats } // AdminGetSystemStats returns system-wide statistics for admins @@ -278,7 +278,7 @@ func (h *Handler) AdminGetSystemStats() http.HandlerFunc { return } - fileStats, err := h.S.GetTotalFileStats() + fileStats, err := h.Storage.GetTotalFileStats() if err != nil { http.Error(w, "Failed to get file stats", http.StatusInternalServerError) return diff --git a/server/internal/handlers/file_handlers.go b/server/internal/handlers/file_handlers.go index 3e23772..d970fa1 100644 --- a/server/internal/handlers/file_handlers.go +++ b/server/internal/handlers/file_handlers.go @@ -17,7 +17,7 @@ func (h *Handler) ListFiles() http.HandlerFunc { return } - files, err := h.S.ListFilesRecursively(ctx.UserID, ctx.Workspace.ID) + files, err := h.Storage.ListFilesRecursively(ctx.UserID, ctx.Workspace.ID) if err != nil { http.Error(w, "Failed to list files", http.StatusInternalServerError) return @@ -40,7 +40,7 @@ func (h *Handler) LookupFileByName() http.HandlerFunc { return } - filePaths, err := h.S.FindFileByName(ctx.UserID, ctx.Workspace.ID, filename) + filePaths, err := h.Storage.FindFileByName(ctx.UserID, ctx.Workspace.ID, filename) if err != nil { http.Error(w, "File not found", http.StatusNotFound) return @@ -58,7 +58,7 @@ func (h *Handler) GetFileContent() http.HandlerFunc { } filePath := chi.URLParam(r, "*") - content, err := h.S.GetFileContent(ctx.UserID, ctx.Workspace.ID, filePath) + content, err := h.Storage.GetFileContent(ctx.UserID, ctx.Workspace.ID, filePath) if err != nil { http.Error(w, "Failed to read file", http.StatusNotFound) return @@ -83,7 +83,7 @@ func (h *Handler) SaveFile() http.HandlerFunc { return } - err = h.S.SaveFile(ctx.UserID, ctx.Workspace.ID, filePath, content) + err = h.Storage.SaveFile(ctx.UserID, ctx.Workspace.ID, filePath, content) if err != nil { http.Error(w, "Failed to save file", http.StatusInternalServerError) return @@ -101,7 +101,7 @@ func (h *Handler) DeleteFile() http.HandlerFunc { } filePath := chi.URLParam(r, "*") - err := h.S.DeleteFile(ctx.UserID, ctx.Workspace.ID, filePath) + err := h.Storage.DeleteFile(ctx.UserID, ctx.Workspace.ID, filePath) if err != nil { http.Error(w, "Failed to delete file", http.StatusInternalServerError) return @@ -125,7 +125,7 @@ func (h *Handler) GetLastOpenedFile() http.HandlerFunc { return } - if _, err := h.S.ValidatePath(ctx.UserID, ctx.Workspace.ID, filePath); err != nil { + if _, err := h.Storage.ValidatePath(ctx.UserID, ctx.Workspace.ID, filePath); err != nil { http.Error(w, "Invalid file path", http.StatusBadRequest) return } @@ -152,7 +152,7 @@ func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc { // Validate the file path exists in the workspace if requestBody.FilePath != "" { - if _, err := h.S.ValidatePath(ctx.UserID, ctx.Workspace.ID, requestBody.FilePath); err != nil { + if _, err := h.Storage.ValidatePath(ctx.UserID, ctx.Workspace.ID, requestBody.FilePath); err != nil { http.Error(w, "Invalid file path", http.StatusBadRequest) return } diff --git a/server/internal/handlers/git_handlers.go b/server/internal/handlers/git_handlers.go index 2836d35..f8ee589 100644 --- a/server/internal/handlers/git_handlers.go +++ b/server/internal/handlers/git_handlers.go @@ -28,7 +28,7 @@ func (h *Handler) StageCommitAndPush() http.HandlerFunc { return } - err := h.S.StageCommitAndPush(ctx.UserID, ctx.Workspace.ID, requestBody.Message) + err := h.Storage.StageCommitAndPush(ctx.UserID, ctx.Workspace.ID, requestBody.Message) if err != nil { http.Error(w, "Failed to stage, commit, and push changes: "+err.Error(), http.StatusInternalServerError) return @@ -45,7 +45,7 @@ func (h *Handler) PullChanges() http.HandlerFunc { return } - err := h.S.Pull(ctx.UserID, ctx.Workspace.ID) + err := h.Storage.Pull(ctx.UserID, ctx.Workspace.ID) if err != nil { http.Error(w, "Failed to pull changes: "+err.Error(), http.StatusInternalServerError) return diff --git a/server/internal/handlers/handlers.go b/server/internal/handlers/handlers.go index d45f721..743e40d 100644 --- a/server/internal/handlers/handlers.go +++ b/server/internal/handlers/handlers.go @@ -4,20 +4,20 @@ import ( "encoding/json" "net/http" "novamd/internal/db" - "novamd/internal/filesystem" + "novamd/internal/storage" ) // Handler provides common functionality for all handlers type Handler struct { - DB *db.DB - S *filesystem.Storage + DB *db.DB + Storage storage.Manager } // NewHandler creates a new handler with the given dependencies -func NewHandler(db *db.DB, fs *filesystem.Storage) *Handler { +func NewHandler(db *db.DB, s storage.Manager) *Handler { return &Handler{ - DB: db, - S: fs, + DB: db, + Storage: s, } } diff --git a/server/internal/handlers/user_handlers.go b/server/internal/handlers/user_handlers.go index a949f11..fa57020 100644 --- a/server/internal/handlers/user_handlers.go +++ b/server/internal/handlers/user_handlers.go @@ -200,7 +200,7 @@ func (h *Handler) DeleteAccount() http.HandlerFunc { // Delete workspace directories for _, workspace := range workspaces { - if err := h.S.DeleteUserWorkspace(ctx.UserID, workspace.ID); err != nil { + if err := h.Storage.DeleteUserWorkspace(ctx.UserID, workspace.ID); err != nil { http.Error(w, "Failed to delete workspace files", http.StatusInternalServerError) return } diff --git a/server/internal/handlers/workspace_handlers.go b/server/internal/handlers/workspace_handlers.go index 7c0fea4..0f2a012 100644 --- a/server/internal/handlers/workspace_handlers.go +++ b/server/internal/handlers/workspace_handlers.go @@ -45,7 +45,7 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc { return } - if err := h.S.InitializeUserWorkspace(workspace.UserID, workspace.ID); err != nil { + if err := h.Storage.InitializeUserWorkspace(workspace.UserID, workspace.ID); err != nil { http.Error(w, "Failed to initialize workspace directory", http.StatusInternalServerError) return } @@ -107,7 +107,7 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc { // Handle Git repository setup/teardown if Git settings changed if gitSettingsChanged(&workspace, ctx.Workspace) { if workspace.GitEnabled { - if err := h.S.SetupGitRepo( + if err := h.Storage.SetupGitRepo( ctx.UserID, ctx.Workspace.ID, workspace.GitURL, @@ -119,7 +119,7 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc { } } else { - h.S.DisableGitRepo(ctx.UserID, ctx.Workspace.ID) + h.Storage.DisableGitRepo(ctx.UserID, ctx.Workspace.ID) } } diff --git a/server/internal/filesystem/files.go b/server/internal/storage/files.go similarity index 80% rename from server/internal/filesystem/files.go rename to server/internal/storage/files.go index eb5ab16..136cc84 100644 --- a/server/internal/filesystem/files.go +++ b/server/internal/storage/files.go @@ -1,6 +1,6 @@ -// Package filesystem provides functionalities to interact with the file system, +// Package storage provides functionalities to interact with the file system, // including listing files, finding files by name, getting file content, saving files, and deleting files. -package filesystem +package storage import ( "fmt" @@ -10,12 +10,23 @@ import ( "strings" ) -// 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"` +// FileManager provides functionalities to interact with files in the storage. +type FileManager interface { + ListFilesRecursively(userID, workspaceID int) ([]FileNode, error) + FindFileByName(userID, workspaceID int, filename string) ([]string, error) + GetFileContent(userID, workspaceID int, filePath string) ([]byte, error) + SaveFile(userID, workspaceID int, filePath string, content []byte) error + DeleteFile(userID, workspaceID int, filePath string) error + GetFileStats(userID, workspaceID int) (*FileCountStats, error) + GetTotalFileStats() (*FileCountStats, error) +} + +// FileNode represents a file or directory in the storage. +type FileNode struct { + ID string `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Children []FileNode `json:"children,omitempty"` } // ListFilesRecursively returns a list of all files in the workspace directory and its subdirectories. @@ -25,13 +36,13 @@ type StorageNode struct { // 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) { +func (s *Service) ListFilesRecursively(userID, workspaceID int) ([]FileNode, error) { workspacePath := s.GetWorkspacePath(userID, workspaceID) return s.walkDirectory(workspacePath, "") } // walkDirectory recursively walks the directory and returns a list of files and directories. -func (s *Storage) walkDirectory(dir, prefix string) ([]StorageNode, error) { +func (s *Service) walkDirectory(dir, prefix string) ([]FileNode, error) { entries, err := s.fs.ReadDir(dir) if err != nil { return nil, err @@ -56,7 +67,7 @@ func (s *Storage) walkDirectory(dir, prefix string) ([]StorageNode, error) { }) // Create combined slice with directories first, then files - nodes := make([]StorageNode, 0, len(entries)) + nodes := make([]FileNode, 0, len(entries)) // Add directories first for _, entry := range dirs { @@ -69,7 +80,7 @@ func (s *Storage) walkDirectory(dir, prefix string) ([]StorageNode, error) { return nil, err } - node := StorageNode{ + node := FileNode{ ID: path, Name: name, Path: path, @@ -83,7 +94,7 @@ func (s *Storage) walkDirectory(dir, prefix string) ([]StorageNode, error) { name := entry.Name() path := filepath.Join(prefix, name) - node := StorageNode{ + node := FileNode{ ID: path, Name: name, Path: path, @@ -102,7 +113,7 @@ func (s *Storage) walkDirectory(dir, prefix string) ([]StorageNode, error) { // 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) { +func (s *Service) FindFileByName(userID, workspaceID int, filename string) ([]string, error) { var foundPaths []string workspacePath := s.GetWorkspacePath(userID, workspaceID) @@ -141,7 +152,7 @@ func (s *Storage) FindFileByName(userID, workspaceID int, filename string) ([]st // 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) { +func (s *Service) GetFileContent(userID, workspaceID int, filePath string) ([]byte, error) { fullPath, err := s.ValidatePath(userID, workspaceID, filePath) if err != nil { return nil, err @@ -157,7 +168,7 @@ func (s *Storage) GetFileContent(userID, workspaceID int, filePath string) ([]by // - 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 { +func (s *Service) SaveFile(userID, workspaceID int, filePath string, content []byte) error { fullPath, err := s.ValidatePath(userID, workspaceID, filePath) if err != nil { return err @@ -178,7 +189,7 @@ func (s *Storage) SaveFile(userID, workspaceID int, filePath string, content []b // - 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 { +func (s *Service) DeleteFile(userID, workspaceID int, filePath string) error { fullPath, err := s.ValidatePath(userID, workspaceID, filePath) if err != nil { return err @@ -199,7 +210,7 @@ type FileCountStats struct { // Returns: // - result: statistics about the files in the workspace // - error: any error that occurred during counting -func (s *Storage) GetFileStats(userID, workspaceID int) (*FileCountStats, error) { +func (s *Service) GetFileStats(userID, workspaceID int) (*FileCountStats, error) { workspacePath := s.GetWorkspacePath(userID, workspaceID) // Check if workspace exists @@ -214,12 +225,12 @@ func (s *Storage) GetFileStats(userID, workspaceID int) (*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) { +func (s *Service) 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) { +func (s *Service) 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/storage/filesystem.go similarity index 98% rename from server/internal/filesystem/filesystem.go rename to server/internal/storage/filesystem.go index 5cafc06..f5ca0b9 100644 --- a/server/internal/filesystem/filesystem.go +++ b/server/internal/storage/filesystem.go @@ -1,4 +1,4 @@ -package filesystem +package storage import ( "io/fs" diff --git a/server/internal/storage/filesystem_test.go b/server/internal/storage/filesystem_test.go new file mode 100644 index 0000000..956599b --- /dev/null +++ b/server/internal/storage/filesystem_test.go @@ -0,0 +1,46 @@ +package storage_test + +import ( + "io/fs" + "os" + "testing/fstest" +) + +// mapFS adapts testing.MapFS to implement our fileSystem interface +type mapFS struct { + fstest.MapFS +} + +func NewMapFS() *mapFS { + return &mapFS{ + MapFS: make(fstest.MapFS), + } +} + +// Only implement the methods that MapFS doesn't already provide +func (m *mapFS) WriteFile(path string, data []byte, perm fs.FileMode) error { + m.MapFS[path] = &fstest.MapFile{ + Data: data, + Mode: perm, + } + return nil +} + +func (m *mapFS) Remove(path string) error { + delete(m.MapFS, path) + return nil +} + +func (m *mapFS) MkdirAll(_ string, _ fs.FileMode) error { + // For MapFS, we don't actually need to create directories + return nil +} + +func (m *mapFS) RemoveAll(path string) error { + delete(m.MapFS, path) + return nil +} + +func (m *mapFS) IsNotExist(err error) bool { + return os.IsNotExist(err) +} diff --git a/server/internal/filesystem/git.go b/server/internal/storage/git.go similarity index 78% rename from server/internal/filesystem/git.go rename to server/internal/storage/git.go index f70d7ad..cebd1b0 100644 --- a/server/internal/filesystem/git.go +++ b/server/internal/storage/git.go @@ -1,10 +1,18 @@ -package filesystem +package storage import ( "fmt" "novamd/internal/gitutils" ) +// RepositoryManager defines the interface for managing Git repositories. +type RepositoryManager interface { + SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToken string) error + DisableGitRepo(userID, workspaceID int) + StageCommitAndPush(userID, workspaceID int, message string) error + Pull(userID, workspaceID int) error +} + // SetupGitRepo sets up a Git repository for the given user and workspace IDs. // Parameters: // - userID: the ID of the user who owns the workspace @@ -14,7 +22,7 @@ import ( // - 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 { +func (s *Service) 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) @@ -27,7 +35,7 @@ func (s *Storage) SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToke // 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) { +func (s *Service) DisableGitRepo(userID, workspaceID int) { if userRepos, ok := s.GitRepos[userID]; ok { delete(userRepos, workspaceID) if len(userRepos) == 0 { @@ -43,7 +51,7 @@ func (s *Storage) DisableGitRepo(userID, workspaceID int) { // - message: the commit message // Returns: // - error: any error that occurred during the operation -func (s *Storage) StageCommitAndPush(userID, workspaceID int, message string) error { +func (s *Service) 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") @@ -62,7 +70,7 @@ func (s *Storage) StageCommitAndPush(userID, workspaceID int, message string) er // - 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 { +func (s *Service) Pull(userID, workspaceID int) error { repo, ok := s.getGitRepo(userID, workspaceID) if !ok { return fmt.Errorf("git settings not configured for this workspace") @@ -72,7 +80,7 @@ func (s *Storage) Pull(userID, workspaceID int) error { } // getGitRepo returns the Git repository for the given user and workspace IDs. -func (s *Storage) getGitRepo(userID, workspaceID int) (*gitutils.GitRepo, bool) { +func (s *Service) getGitRepo(userID, workspaceID int) (*gitutils.GitRepo, bool) { userRepos, ok := s.GitRepos[userID] if !ok { return nil, false diff --git a/server/internal/storage/service.go b/server/internal/storage/service.go new file mode 100644 index 0000000..5b57b4b --- /dev/null +++ b/server/internal/storage/service.go @@ -0,0 +1,42 @@ +package storage + +import ( + "novamd/internal/gitutils" +) + +// Manager interface combines all storage interfaces. +type Manager interface { + FileManager + WorkspaceManager + RepositoryManager +} + +// Service represents the file system structure. +type Service struct { + fs fileSystem + RootDir string + GitRepos map[int]map[int]*gitutils.GitRepo // map[userID]map[workspaceID]*gitutils.GitRepo +} + +// NewService creates a new Storage instance. +// Parameters: +// - rootDir: the root directory for the storage +// Returns: +// - result: the new Storage instance +func NewService(rootDir string) *Service { + return NewServiceWithFS(rootDir, &osFS{}) +} + +// NewServiceWithFS 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 NewServiceWithFS(rootDir string, fs fileSystem) *Service { + return &Service{ + fs: fs, + RootDir: rootDir, + GitRepos: make(map[int]map[int]*gitutils.GitRepo), + } +} diff --git a/server/internal/filesystem/workspace.go b/server/internal/storage/workspace.go similarity index 51% rename from server/internal/filesystem/workspace.go rename to server/internal/storage/workspace.go index 891eaae..b72155f 100644 --- a/server/internal/filesystem/workspace.go +++ b/server/internal/storage/workspace.go @@ -1,17 +1,46 @@ -package filesystem +package storage import ( "fmt" "path/filepath" + "strings" ) +// WorkspaceManager provides functionalities to interact with workspaces in the storage. +type WorkspaceManager interface { + ValidatePath(userID, workspaceID int, path string) (string, error) + GetWorkspacePath(userID, workspaceID int) string + InitializeUserWorkspace(userID, workspaceID int) error + DeleteUserWorkspace(userID, workspaceID int) error +} + +// ValidatePath validates the given path and returns the cleaned path if it is valid. +// 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 *Service) ValidatePath(userID, workspaceID int, path string) (string, error) { + workspacePath := s.GetWorkspacePath(userID, workspaceID) + fullPath := filepath.Join(workspacePath, path) + cleanPath := filepath.Clean(fullPath) + + if !strings.HasPrefix(cleanPath, workspacePath) { + return "", fmt.Errorf("invalid path: outside of workspace") + } + + return cleanPath, nil +} + // GetWorkspacePath returns the path to the workspace directory for the given user and workspace IDs. // 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 { +func (s *Service) GetWorkspacePath(userID, workspaceID int) string { return filepath.Join(s.RootDir, fmt.Sprintf("%d", userID), fmt.Sprintf("%d", workspaceID)) } @@ -21,7 +50,7 @@ func (s *Storage) GetWorkspacePath(userID, workspaceID int) string { // - 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 { +func (s *Service) InitializeUserWorkspace(userID, workspaceID int) error { workspacePath := s.GetWorkspacePath(userID, workspaceID) err := s.fs.MkdirAll(workspacePath, 0755) if err != nil { @@ -37,7 +66,7 @@ func (s *Storage) InitializeUserWorkspace(userID, workspaceID int) error { // - 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 { +func (s *Service) DeleteUserWorkspace(userID, workspaceID int) error { workspacePath := s.GetWorkspacePath(userID, workspaceID) err := s.fs.RemoveAll(workspacePath) if err != nil { diff --git a/server/internal/user/user.go b/server/internal/user/user.go index 530833e..638ec39 100644 --- a/server/internal/user/user.go +++ b/server/internal/user/user.go @@ -8,19 +8,19 @@ import ( "golang.org/x/crypto/bcrypt" "novamd/internal/db" - "novamd/internal/filesystem" "novamd/internal/models" + "novamd/internal/storage" ) type UserService struct { - DB *db.DB - FS *filesystem.Storage + DB *db.DB + Storage storage.Manager } -func NewUserService(database *db.DB, fs *filesystem.Storage) *UserService { +func NewUserService(database *db.DB, s storage.Manager) *UserService { return &UserService{ - DB: database, - FS: fs, + DB: database, + Storage: s, } } @@ -53,7 +53,7 @@ func (s *UserService) SetupAdminUser(adminEmail, adminPassword string) (*models. } // Initialize workspace directory - err = s.FS.InitializeUserWorkspace(createdUser.ID, createdUser.LastWorkspaceID) + err = s.Storage.InitializeUserWorkspace(createdUser.ID, createdUser.LastWorkspaceID) if err != nil { return nil, fmt.Errorf("failed to initialize admin workspace: %w", err) }