Rename filesystem interfaces and structs

This commit is contained in:
2024-11-14 21:13:45 +01:00
parent 5311d2e144
commit e4510298ed
16 changed files with 206 additions and 128 deletions

View File

@@ -17,8 +17,8 @@ import (
"novamd/internal/auth" "novamd/internal/auth"
"novamd/internal/config" "novamd/internal/config"
"novamd/internal/db" "novamd/internal/db"
"novamd/internal/filesystem"
"novamd/internal/handlers" "novamd/internal/handlers"
"novamd/internal/storage"
) )
func main() { func main() {
@@ -45,7 +45,7 @@ func main() {
} }
// Initialize filesystem // Initialize filesystem
fs := filesystem.New(cfg.WorkDir) s := storage.NewService(cfg.WorkDir)
// Initialize JWT service // Initialize JWT service
jwtService, err := auth.NewJWTService(auth.JWTConfig{ jwtService, err := auth.NewJWTService(auth.JWTConfig{
@@ -95,7 +95,7 @@ func main() {
// Set up routes // Set up routes
r.Route("/api/v1", func(r chi.Router) { r.Route("/api/v1", func(r chi.Router) {
r.Use(httprate.LimitByIP(cfg.RateLimitRequests, cfg.RateLimitWindow)) 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 // Handle all other routes with static file server

View File

@@ -4,19 +4,19 @@ package api
import ( import (
"novamd/internal/auth" "novamd/internal/auth"
"novamd/internal/db" "novamd/internal/db"
"novamd/internal/filesystem"
"novamd/internal/handlers" "novamd/internal/handlers"
"novamd/internal/middleware" "novamd/internal/middleware"
"novamd/internal/storage"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
// SetupRoutes configures the API routes // 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{ handler := &handlers.Handler{
DB: db, DB: db,
S: s, Storage: s,
} }
// Public routes (no authentication required) // Public routes (no authentication required)

View File

@@ -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
}

View File

@@ -4,9 +4,9 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"novamd/internal/db" "novamd/internal/db"
"novamd/internal/filesystem"
"novamd/internal/httpcontext" "novamd/internal/httpcontext"
"novamd/internal/models" "novamd/internal/models"
"novamd/internal/storage"
"strconv" "strconv"
"time" "time"
@@ -91,7 +91,7 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc {
} }
// Initialize user workspace // 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) http.Error(w, "Failed to initialize user workspace", http.StatusInternalServerError)
return return
} }
@@ -218,7 +218,7 @@ type WorkspaceStats struct {
WorkspaceID int `json:"workspaceID"` WorkspaceID int `json:"workspaceID"`
WorkspaceName string `json:"workspaceName"` WorkspaceName string `json:"workspaceName"`
WorkspaceCreatedAt time.Time `json:"workspaceCreatedAt"` WorkspaceCreatedAt time.Time `json:"workspaceCreatedAt"`
*filesystem.FileCountStats *storage.FileCountStats
} }
// AdminListWorkspaces returns a list of all workspaces and their stats // 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.WorkspaceName = ws.Name
workspaceData.WorkspaceCreatedAt = ws.CreatedAt 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 { if err != nil {
http.Error(w, "Failed to get file stats", http.StatusInternalServerError) http.Error(w, "Failed to get file stats", http.StatusInternalServerError)
return return
@@ -266,7 +266,7 @@ func (h *Handler) AdminListWorkspaces() http.HandlerFunc {
// SystemStats holds system-wide statistics // SystemStats holds system-wide statistics
type SystemStats struct { type SystemStats struct {
*db.UserStats *db.UserStats
*filesystem.FileCountStats *storage.FileCountStats
} }
// AdminGetSystemStats returns system-wide statistics for admins // AdminGetSystemStats returns system-wide statistics for admins
@@ -278,7 +278,7 @@ func (h *Handler) AdminGetSystemStats() http.HandlerFunc {
return return
} }
fileStats, err := h.S.GetTotalFileStats() fileStats, err := h.Storage.GetTotalFileStats()
if err != nil { if err != nil {
http.Error(w, "Failed to get file stats", http.StatusInternalServerError) http.Error(w, "Failed to get file stats", http.StatusInternalServerError)
return return

View File

@@ -17,7 +17,7 @@ func (h *Handler) ListFiles() http.HandlerFunc {
return return
} }
files, err := h.S.ListFilesRecursively(ctx.UserID, ctx.Workspace.ID) files, err := h.Storage.ListFilesRecursively(ctx.UserID, ctx.Workspace.ID)
if err != nil { if err != nil {
http.Error(w, "Failed to list files", http.StatusInternalServerError) http.Error(w, "Failed to list files", http.StatusInternalServerError)
return return
@@ -40,7 +40,7 @@ func (h *Handler) LookupFileByName() http.HandlerFunc {
return 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 { if err != nil {
http.Error(w, "File not found", http.StatusNotFound) http.Error(w, "File not found", http.StatusNotFound)
return return
@@ -58,7 +58,7 @@ func (h *Handler) GetFileContent() http.HandlerFunc {
} }
filePath := chi.URLParam(r, "*") 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 { if err != nil {
http.Error(w, "Failed to read file", http.StatusNotFound) http.Error(w, "Failed to read file", http.StatusNotFound)
return return
@@ -83,7 +83,7 @@ func (h *Handler) SaveFile() http.HandlerFunc {
return 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 { if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError) http.Error(w, "Failed to save file", http.StatusInternalServerError)
return return
@@ -101,7 +101,7 @@ func (h *Handler) DeleteFile() http.HandlerFunc {
} }
filePath := chi.URLParam(r, "*") 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 { if err != nil {
http.Error(w, "Failed to delete file", http.StatusInternalServerError) http.Error(w, "Failed to delete file", http.StatusInternalServerError)
return return
@@ -125,7 +125,7 @@ func (h *Handler) GetLastOpenedFile() http.HandlerFunc {
return 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) http.Error(w, "Invalid file path", http.StatusBadRequest)
return return
} }
@@ -152,7 +152,7 @@ func (h *Handler) UpdateLastOpenedFile() http.HandlerFunc {
// Validate the file path exists in the workspace // Validate the file path exists in the workspace
if requestBody.FilePath != "" { 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) http.Error(w, "Invalid file path", http.StatusBadRequest)
return return
} }

View File

@@ -28,7 +28,7 @@ func (h *Handler) StageCommitAndPush() http.HandlerFunc {
return 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 { if err != nil {
http.Error(w, "Failed to stage, commit, and push changes: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Failed to stage, commit, and push changes: "+err.Error(), http.StatusInternalServerError)
return return
@@ -45,7 +45,7 @@ func (h *Handler) PullChanges() http.HandlerFunc {
return return
} }
err := h.S.Pull(ctx.UserID, ctx.Workspace.ID) err := h.Storage.Pull(ctx.UserID, ctx.Workspace.ID)
if err != nil { if err != nil {
http.Error(w, "Failed to pull changes: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Failed to pull changes: "+err.Error(), http.StatusInternalServerError)
return return

View File

@@ -4,20 +4,20 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"novamd/internal/db" "novamd/internal/db"
"novamd/internal/filesystem" "novamd/internal/storage"
) )
// Handler provides common functionality for all handlers // Handler provides common functionality for all handlers
type Handler struct { type Handler struct {
DB *db.DB DB *db.DB
S *filesystem.Storage Storage storage.Manager
} }
// NewHandler creates a new handler with the given dependencies // 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{ return &Handler{
DB: db, DB: db,
S: fs, Storage: s,
} }
} }

View File

@@ -200,7 +200,7 @@ func (h *Handler) DeleteAccount() http.HandlerFunc {
// Delete workspace directories // Delete workspace directories
for _, workspace := range workspaces { 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) http.Error(w, "Failed to delete workspace files", http.StatusInternalServerError)
return return
} }

View File

@@ -45,7 +45,7 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
return 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) http.Error(w, "Failed to initialize workspace directory", http.StatusInternalServerError)
return return
} }
@@ -107,7 +107,7 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
// Handle Git repository setup/teardown if Git settings changed // Handle Git repository setup/teardown if Git settings changed
if gitSettingsChanged(&workspace, ctx.Workspace) { if gitSettingsChanged(&workspace, ctx.Workspace) {
if workspace.GitEnabled { if workspace.GitEnabled {
if err := h.S.SetupGitRepo( if err := h.Storage.SetupGitRepo(
ctx.UserID, ctx.UserID,
ctx.Workspace.ID, ctx.Workspace.ID,
workspace.GitURL, workspace.GitURL,
@@ -119,7 +119,7 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
} }
} else { } else {
h.S.DisableGitRepo(ctx.UserID, ctx.Workspace.ID) h.Storage.DisableGitRepo(ctx.UserID, ctx.Workspace.ID)
} }
} }

View File

@@ -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. // including listing files, finding files by name, getting file content, saving files, and deleting files.
package filesystem package storage
import ( import (
"fmt" "fmt"
@@ -10,12 +10,23 @@ import (
"strings" "strings"
) )
// StorageNode represents a file or directory in the storage. // FileManager provides functionalities to interact with files in the storage.
type StorageNode struct { 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"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path string `json:"path"`
Children []StorageNode `json:"children,omitempty"` Children []FileNode `json:"children,omitempty"`
} }
// ListFilesRecursively returns a list of all files in the workspace directory and its subdirectories. // ListFilesRecursively returns a list of all files in the workspace directory and its subdirectories.
@@ -25,13 +36,13 @@ type StorageNode struct {
// Returns: // Returns:
// - nodes: a list of files and directories in the workspace // - nodes: a list of files and directories in the workspace
// - error: any error that occurred during listing // - 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) workspacePath := s.GetWorkspacePath(userID, workspaceID)
return s.walkDirectory(workspacePath, "") return s.walkDirectory(workspacePath, "")
} }
// walkDirectory recursively walks the directory and returns a list of files and directories. // 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) entries, err := s.fs.ReadDir(dir)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -56,7 +67,7 @@ func (s *Storage) walkDirectory(dir, prefix string) ([]StorageNode, error) {
}) })
// Create combined slice with directories first, then files // Create combined slice with directories first, then files
nodes := make([]StorageNode, 0, len(entries)) nodes := make([]FileNode, 0, len(entries))
// Add directories first // Add directories first
for _, entry := range dirs { for _, entry := range dirs {
@@ -69,7 +80,7 @@ func (s *Storage) walkDirectory(dir, prefix string) ([]StorageNode, error) {
return nil, err return nil, err
} }
node := StorageNode{ node := FileNode{
ID: path, ID: path,
Name: name, Name: name,
Path: path, Path: path,
@@ -83,7 +94,7 @@ func (s *Storage) walkDirectory(dir, prefix string) ([]StorageNode, error) {
name := entry.Name() name := entry.Name()
path := filepath.Join(prefix, name) path := filepath.Join(prefix, name)
node := StorageNode{ node := FileNode{
ID: path, ID: path,
Name: name, Name: name,
Path: path, Path: path,
@@ -102,7 +113,7 @@ func (s *Storage) walkDirectory(dir, prefix string) ([]StorageNode, error) {
// Returns: // Returns:
// - foundPaths: a list of file paths that match the filename // - foundPaths: a list of file paths that match the filename
// - error: any error that occurred during the search // - 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 var foundPaths []string
workspacePath := s.GetWorkspacePath(userID, workspaceID) workspacePath := s.GetWorkspacePath(userID, workspaceID)
@@ -141,7 +152,7 @@ func (s *Storage) FindFileByName(userID, workspaceID int, filename string) ([]st
// Returns: // Returns:
// - content: the content of the file // - content: the content of the file
// - error: any error that occurred during reading // - 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) fullPath, err := s.ValidatePath(userID, workspaceID, filePath)
if err != nil { if err != nil {
return nil, err 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 // - content: the content to write to the file
// Returns: // Returns:
// - error: any error that occurred during saving // - 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) fullPath, err := s.ValidatePath(userID, workspaceID, filePath)
if err != nil { if err != nil {
return err 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 // - filePath: the path of the file to delete
// Returns: // Returns:
// - error: any error that occurred during deletion // - 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) fullPath, err := s.ValidatePath(userID, workspaceID, filePath)
if err != nil { if err != nil {
return err return err
@@ -199,7 +210,7 @@ type FileCountStats struct {
// Returns: // Returns:
// - result: statistics about the files in the workspace // - result: statistics about the files in the workspace
// - error: any error that occurred during counting // - 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) workspacePath := s.GetWorkspacePath(userID, workspaceID)
// Check if workspace exists // 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. // GetTotalFileStats returns the total file statistics for the storage.
// Returns: // Returns:
// - result: statistics about the files in the storage // - 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) return s.countFilesInPath(s.RootDir)
} }
// countFilesInPath counts the total number of files and the total size of files in the given directory. // 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{} result := &FileCountStats{}
err := filepath.WalkDir(directoryPath, func(path string, d os.DirEntry, err error) error { err := filepath.WalkDir(directoryPath, func(path string, d os.DirEntry, err error) error {

View File

@@ -1,4 +1,4 @@
package filesystem package storage
import ( import (
"io/fs" "io/fs"

View File

@@ -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)
}

View File

@@ -1,10 +1,18 @@
package filesystem package storage
import ( import (
"fmt" "fmt"
"novamd/internal/gitutils" "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. // SetupGitRepo sets up a Git repository for the given user and workspace IDs.
// Parameters: // Parameters:
// - userID: the ID of the user who owns the workspace // - userID: the ID of the user who owns the workspace
@@ -14,7 +22,7 @@ import (
// - gitToken: the access token for the Git repository // - gitToken: the access token for the Git repository
// Returns: // Returns:
// - error: any error that occurred during setup // - 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) workspacePath := s.GetWorkspacePath(userID, workspaceID)
if _, ok := s.GitRepos[userID]; !ok { if _, ok := s.GitRepos[userID]; !ok {
s.GitRepos[userID] = make(map[int]*gitutils.GitRepo) s.GitRepos[userID] = make(map[int]*gitutils.GitRepo)
@@ -27,7 +35,7 @@ func (s *Storage) SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToke
// Parameters: // Parameters:
// - userID: the ID of the user who owns the workspace // - userID: the ID of the user who owns the workspace
// - workspaceID: the ID of the workspace to disable the Git repository for // - 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 { if userRepos, ok := s.GitRepos[userID]; ok {
delete(userRepos, workspaceID) delete(userRepos, workspaceID)
if len(userRepos) == 0 { if len(userRepos) == 0 {
@@ -43,7 +51,7 @@ func (s *Storage) DisableGitRepo(userID, workspaceID int) {
// - message: the commit message // - message: the commit message
// Returns: // Returns:
// - error: any error that occurred during the operation // - 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) repo, ok := s.getGitRepo(userID, workspaceID)
if !ok { if !ok {
return fmt.Errorf("git settings not configured for this workspace") 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 // - workspaceID: the ID of the workspace to pull changes for
// Returns: // Returns:
// - error: any error that occurred during the operation // - 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) repo, ok := s.getGitRepo(userID, workspaceID)
if !ok { if !ok {
return fmt.Errorf("git settings not configured for this workspace") 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. // 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] userRepos, ok := s.GitRepos[userID]
if !ok { if !ok {
return nil, false return nil, false

View File

@@ -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),
}
}

View File

@@ -1,17 +1,46 @@
package filesystem package storage
import ( import (
"fmt" "fmt"
"path/filepath" "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. // GetWorkspacePath returns the path to the workspace directory for the given user and workspace IDs.
// Parameters: // Parameters:
// - userID: the ID of the user who owns the workspace // - userID: the ID of the user who owns the workspace
// - workspaceID: the ID of the workspace // - workspaceID: the ID of the workspace
// Returns: // Returns:
// - result: the path to the workspace directory // - 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)) 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 // - workspaceID: the ID of the workspace to initialize
// Returns: // Returns:
// - error: any error that occurred during the operation // - 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) workspacePath := s.GetWorkspacePath(userID, workspaceID)
err := s.fs.MkdirAll(workspacePath, 0755) err := s.fs.MkdirAll(workspacePath, 0755)
if err != nil { if err != nil {
@@ -37,7 +66,7 @@ func (s *Storage) InitializeUserWorkspace(userID, workspaceID int) error {
// - workspaceID: the ID of the workspace to delete // - workspaceID: the ID of the workspace to delete
// Returns: // Returns:
// - error: any error that occurred during the operation // - 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) workspacePath := s.GetWorkspacePath(userID, workspaceID)
err := s.fs.RemoveAll(workspacePath) err := s.fs.RemoveAll(workspacePath)
if err != nil { if err != nil {

View File

@@ -8,19 +8,19 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"novamd/internal/db" "novamd/internal/db"
"novamd/internal/filesystem"
"novamd/internal/models" "novamd/internal/models"
"novamd/internal/storage"
) )
type UserService struct { type UserService struct {
DB *db.DB DB *db.DB
FS *filesystem.Storage Storage storage.Manager
} }
func NewUserService(database *db.DB, fs *filesystem.Storage) *UserService { func NewUserService(database *db.DB, s storage.Manager) *UserService {
return &UserService{ return &UserService{
DB: database, DB: database,
FS: fs, Storage: s,
} }
} }
@@ -53,7 +53,7 @@ func (s *UserService) SetupAdminUser(adminEmail, adminPassword string) (*models.
} }
// Initialize workspace directory // Initialize workspace directory
err = s.FS.InitializeUserWorkspace(createdUser.ID, createdUser.LastWorkspaceID) err = s.Storage.InitializeUserWorkspace(createdUser.ID, createdUser.LastWorkspaceID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initialize admin workspace: %w", err) return nil, fmt.Errorf("failed to initialize admin workspace: %w", err)
} }