From 589158a8ed20163b36a6bd1d283804b9e2605a47 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 3 Dec 2025 00:56:44 +0000 Subject: [PATCH] feat: impl directory endpoints --- apps/backend/internal/catalog/directory.go | 213 +++++++++++++++++++++ apps/backend/internal/catalog/file.go | 160 +++++++++++++++- apps/backend/internal/catalog/http.go | 180 ++--------------- 3 files changed, 391 insertions(+), 162 deletions(-) create mode 100644 apps/backend/internal/catalog/directory.go diff --git a/apps/backend/internal/catalog/directory.go b/apps/backend/internal/catalog/directory.go new file mode 100644 index 0000000..e8f3855 --- /dev/null +++ b/apps/backend/internal/catalog/directory.go @@ -0,0 +1,213 @@ +package catalog + +import ( + "errors" + "time" + + "github.com/get-drexa/drexa/internal/account" + "github.com/get-drexa/drexa/internal/httperr" + "github.com/get-drexa/drexa/internal/virtualfs" + "github.com/gofiber/fiber/v2" +) + +const ( + DirItemKindDirectory = "directory" + DirItemKindFile = "file" +) + +type DirectoryInfo struct { + Kind string `json:"kind"` + ID string `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt *time.Time `json:"deletedAt,omitempty"` +} + +type createDirectoryRequest struct { + ParentID string `json:"parentID"` + Name string `json:"name"` +} + +func (h *HTTPHandler) currentDirectoryMiddleware(c *fiber.Ctx) error { + account := account.CurrentAccount(c) + if account == nil { + return c.SendStatus(fiber.StatusUnauthorized) + } + + directoryID := c.Params("directoryID") + node, err := h.vfs.FindNodeByPublicID(c.Context(), h.db, account.ID, directoryID) + if err != nil { + if errors.Is(err, virtualfs.ErrNodeNotFound) { + return c.SendStatus(fiber.StatusNotFound) + } + return httperr.Internal(err) + } + + c.Locals("directory", node) + + return c.Next() +} + +func mustCurrentDirectoryNode(c *fiber.Ctx) *virtualfs.Node { + return c.Locals("directory").(*virtualfs.Node) +} + +func (h *HTTPHandler) createDirectory(c *fiber.Ctx) error { + account := account.CurrentAccount(c) + if account == nil { + return c.SendStatus(fiber.StatusUnauthorized) + } + + req := new(createDirectoryRequest) + if err := c.BodyParser(req); err != nil { + return c.SendStatus(fiber.StatusBadRequest) + } + + parent, err := h.vfs.FindNodeByPublicID(c.Context(), h.db, account.ID, req.ParentID) + if err != nil { + if errors.Is(err, virtualfs.ErrNodeNotFound) { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Parent not found"}) + } + return httperr.Internal(err) + } + + if parent.Kind != virtualfs.NodeKindDirectory { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Parent is not a directory"}) + } + + node, err := h.vfs.CreateDirectory(c.Context(), h.db, account.ID, parent.ID, req.Name) + if err != nil { + return httperr.Internal(err) + } + + return c.JSON(DirectoryInfo{ + Kind: DirItemKindDirectory, + ID: node.PublicID, + Name: node.Name, + CreatedAt: node.CreatedAt, + UpdatedAt: node.UpdatedAt, + DeletedAt: node.DeletedAt, + }) +} + +func (h *HTTPHandler) fetchDirectory(c *fiber.Ctx) error { + node := mustCurrentDirectoryNode(c) + i := DirectoryInfo{ + Kind: DirItemKindDirectory, + ID: node.PublicID, + Name: node.Name, + CreatedAt: node.CreatedAt, + UpdatedAt: node.UpdatedAt, + DeletedAt: node.DeletedAt, + } + return c.JSON(i) +} + +func (h *HTTPHandler) listDirectory(c *fiber.Ctx) error { + node := mustCurrentDirectoryNode(c) + children, err := h.vfs.ListChildren(c.Context(), h.db, node) + if err != nil { + if errors.Is(err, virtualfs.ErrNodeNotFound) { + return c.SendStatus(fiber.StatusNotFound) + } + return httperr.Internal(err) + } + + items := make([]any, len(children)) + for i, child := range children { + switch child.Kind { + case virtualfs.NodeKindDirectory: + items[i] = DirectoryInfo{ + Kind: DirItemKindDirectory, + ID: child.PublicID, + Name: child.Name, + CreatedAt: child.CreatedAt, + UpdatedAt: child.UpdatedAt, + DeletedAt: child.DeletedAt, + } + case virtualfs.NodeKindFile: + items[i] = FileInfo{ + Kind: DirItemKindFile, + ID: child.PublicID, + Name: child.Name, + Size: child.Size, + MimeType: child.MimeType, + CreatedAt: child.CreatedAt, + UpdatedAt: child.UpdatedAt, + DeletedAt: child.DeletedAt, + } + } + } + + return c.JSON(items) +} + +func (h *HTTPHandler) patchDirectory(c *fiber.Ctx) error { + node := mustCurrentDirectoryNode(c) + + patch := new(patchDirectoryRequest) + if err := c.BodyParser(patch); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"}) + } + + tx, err := h.db.BeginTx(c.Context(), nil) + if err != nil { + return httperr.Internal(err) + } + defer tx.Rollback() + + if patch.Name != "" { + err := h.vfs.RenameNode(c.Context(), tx, node, patch.Name) + if err != nil { + if errors.Is(err, virtualfs.ErrNodeNotFound) { + return c.SendStatus(fiber.StatusNotFound) + } + return httperr.Internal(err) + } + } + + err = tx.Commit() + if err != nil { + return httperr.Internal(err) + } + + return c.JSON(DirectoryInfo{ + Kind: DirItemKindDirectory, + ID: node.PublicID, + Name: node.Name, + CreatedAt: node.CreatedAt, + UpdatedAt: node.UpdatedAt, + DeletedAt: node.DeletedAt, + }) +} + +func (h *HTTPHandler) deleteDirectory(c *fiber.Ctx) error { + node := mustCurrentDirectoryNode(c) + + tx, err := h.db.BeginTx(c.Context(), nil) + if err != nil { + return httperr.Internal(err) + } + defer tx.Rollback() + + shouldTrash := c.Query("trash") == "true" + if shouldTrash { + err = h.vfs.SoftDeleteNode(c.Context(), h.db, node) + if err != nil { + return httperr.Internal(err) + } + } else { + err = h.vfs.PermanentlyDeleteNode(c.Context(), h.db, node) + if err != nil { + return httperr.Internal(err) + } + } + + err = tx.Commit() + if err != nil { + return httperr.Internal(err) + } + + return c.SendStatus(fiber.StatusNoContent) +} diff --git a/apps/backend/internal/catalog/file.go b/apps/backend/internal/catalog/file.go index 0390d40..ba9b564 100644 --- a/apps/backend/internal/catalog/file.go +++ b/apps/backend/internal/catalog/file.go @@ -1,8 +1,18 @@ package catalog -import "time" +import ( + "errors" + "fmt" + "time" + + "github.com/get-drexa/drexa/internal/account" + "github.com/get-drexa/drexa/internal/httperr" + "github.com/get-drexa/drexa/internal/virtualfs" + "github.com/gofiber/fiber/v2" +) type FileInfo struct { + Kind string `json:"kind"` ID string `json:"id"` Name string `json:"name"` Size int64 `json:"size"` @@ -11,3 +21,151 @@ type FileInfo struct { UpdatedAt time.Time `json:"updatedAt"` DeletedAt *time.Time `json:"deletedAt,omitempty"` } + +func mustCurrentFileNode(c *fiber.Ctx) *virtualfs.Node { + return c.Locals("file").(*virtualfs.Node) +} + +func (h *HTTPHandler) currentFileMiddleware(c *fiber.Ctx) error { + account := account.CurrentAccount(c) + if account == nil { + return c.SendStatus(fiber.StatusUnauthorized) + } + + fileID := c.Params("fileID") + node, err := h.vfs.FindNodeByPublicID(c.Context(), h.db, account.ID, fileID) + if err != nil { + if errors.Is(err, virtualfs.ErrNodeNotFound) { + return c.SendStatus(fiber.StatusNotFound) + } + return httperr.Internal(err) + } + + c.Locals("file", node) + + return c.Next() +} + +func (h *HTTPHandler) fetchFile(c *fiber.Ctx) error { + node := mustCurrentFileNode(c) + i := FileInfo{ + Kind: DirItemKindFile, + ID: node.PublicID, + Name: node.Name, + Size: node.Size, + MimeType: node.MimeType, + CreatedAt: node.CreatedAt, + UpdatedAt: node.UpdatedAt, + DeletedAt: node.DeletedAt, + } + return c.JSON(i) +} + +func (h *HTTPHandler) downloadFile(c *fiber.Ctx) error { + node := mustCurrentFileNode(c) + + content, err := h.vfs.ReadFile(c.Context(), h.db, node) + if err != nil { + if errors.Is(err, virtualfs.ErrUnsupportedOperation) { + return c.SendStatus(fiber.StatusNotFound) + } + return httperr.Internal(err) + } + + if content.URL != "" { + return c.Redirect(content.URL, fiber.StatusTemporaryRedirect) + } + + if content.Reader != nil { + if node.MimeType != "" { + c.Set("Content-Type", node.MimeType) + } + if content.Size > 0 { + return c.SendStream(content.Reader, int(content.Size)) + } + return c.SendStream(content.Reader) + } + + return httperr.Internal(errors.New("vfs returned neither a reader nor a URL")) +} + +func (h *HTTPHandler) patchFile(c *fiber.Ctx) error { + node := mustCurrentFileNode(c) + + patch := new(patchFileRequest) + if err := c.BodyParser(patch); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"}) + } + + tx, err := h.db.BeginTx(c.Context(), nil) + if err != nil { + return httperr.Internal(err) + } + defer tx.Rollback() + + if patch.Name != "" { + err := h.vfs.RenameNode(c.Context(), tx, node, patch.Name) + if err != nil { + if errors.Is(err, virtualfs.ErrNodeNotFound) { + return c.SendStatus(fiber.StatusNotFound) + } + return httperr.Internal(err) + } + } + + err = tx.Commit() + if err != nil { + return httperr.Internal(err) + } + + fmt.Printf("node deleted at: %v\n", node.DeletedAt) + + return c.JSON(FileInfo{ + ID: node.PublicID, + Name: node.Name, + Size: node.Size, + MimeType: node.MimeType, + CreatedAt: node.CreatedAt, + UpdatedAt: node.UpdatedAt, + DeletedAt: node.DeletedAt, + }) +} + +func (h *HTTPHandler) deleteFile(c *fiber.Ctx) error { + node := mustCurrentFileNode(c) + + tx, err := h.db.BeginTx(c.Context(), nil) + if err != nil { + return httperr.Internal(err) + } + defer tx.Rollback() + + shouldTrash := c.Query("trash") == "true" + if shouldTrash { + err = h.vfs.SoftDeleteNode(c.Context(), tx, node) + if err != nil { + return httperr.Internal(err) + } + return c.JSON(FileInfo{ + ID: node.PublicID, + Name: node.Name, + Size: node.Size, + MimeType: node.MimeType, + CreatedAt: node.CreatedAt, + UpdatedAt: node.UpdatedAt, + DeletedAt: node.DeletedAt, + }) + } else { + err = h.vfs.PermanentlyDeleteNode(c.Context(), tx, node) + if err != nil { + return httperr.Internal(err) + } + + err = tx.Commit() + if err != nil { + return httperr.Internal(err) + } + + return c.SendStatus(fiber.StatusNoContent) + } +} diff --git a/apps/backend/internal/catalog/http.go b/apps/backend/internal/catalog/http.go index a944679..5fc8578 100644 --- a/apps/backend/internal/catalog/http.go +++ b/apps/backend/internal/catalog/http.go @@ -1,11 +1,6 @@ package catalog import ( - "errors" - "fmt" - - "github.com/get-drexa/drexa/internal/account" - "github.com/get-drexa/drexa/internal/httperr" "github.com/get-drexa/drexa/internal/virtualfs" "github.com/gofiber/fiber/v2" "github.com/uptrace/bun" @@ -20,165 +15,28 @@ type patchFileRequest struct { Name string `json:"name"` } +type patchDirectoryRequest struct { + Name string `json:"name"` +} + func NewHTTPHandler(vfs *virtualfs.VirtualFS, db *bun.DB) *HTTPHandler { return &HTTPHandler{vfs: vfs, db: db} } func (h *HTTPHandler) RegisterRoutes(api fiber.Router) { - g := api.Group("/files/:fileID") - g.Use(h.currentFileMiddleware) - g.Get("/", h.fetchFile) - g.Get("/content", h.downloadFile) - g.Patch("/", h.patchFile) - g.Delete("/", h.deleteFile) -} - -func mustCurrentFileNode(c *fiber.Ctx) *virtualfs.Node { - return c.Locals("file").(*virtualfs.Node) -} - -func (h *HTTPHandler) currentFileMiddleware(c *fiber.Ctx) error { - account := account.CurrentAccount(c) - if account == nil { - return c.SendStatus(fiber.StatusUnauthorized) - } - - fileID := c.Params("fileID") - node, err := h.vfs.FindNodeByPublicID(c.Context(), h.db, account.ID, fileID) - if err != nil { - if errors.Is(err, virtualfs.ErrNodeNotFound) { - return c.SendStatus(fiber.StatusNotFound) - } - return httperr.Internal(err) - } - - c.Locals("file", node) - - return c.Next() -} - -func (h *HTTPHandler) fetchFile(c *fiber.Ctx) error { - node := mustCurrentFileNode(c) - i := FileInfo{ - ID: node.PublicID, - Name: node.Name, - Size: node.Size, - MimeType: node.MimeType, - CreatedAt: node.CreatedAt, - UpdatedAt: node.UpdatedAt, - } - if node.DeletedAt != nil { - i.DeletedAt = node.DeletedAt - } - - return c.JSON(i) -} - -func (h *HTTPHandler) downloadFile(c *fiber.Ctx) error { - node := mustCurrentFileNode(c) - - content, err := h.vfs.ReadFile(c.Context(), h.db, node) - if err != nil { - if errors.Is(err, virtualfs.ErrUnsupportedOperation) { - return c.SendStatus(fiber.StatusNotFound) - } - return httperr.Internal(err) - } - - if content.URL != "" { - return c.Redirect(content.URL, fiber.StatusTemporaryRedirect) - } - - if content.Reader != nil { - if node.MimeType != "" { - c.Set("Content-Type", node.MimeType) - } - if content.Size > 0 { - return c.SendStream(content.Reader, int(content.Size)) - } - return c.SendStream(content.Reader) - } - - return httperr.Internal(errors.New("vfs returned neither a reader nor a URL")) -} - -func (h *HTTPHandler) patchFile(c *fiber.Ctx) error { - node := mustCurrentFileNode(c) - - patch := new(patchFileRequest) - if err := c.BodyParser(patch); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"}) - } - - tx, err := h.db.BeginTx(c.Context(), nil) - if err != nil { - return httperr.Internal(err) - } - defer tx.Rollback() - - if patch.Name != "" { - err := h.vfs.RenameNode(c.Context(), tx, node, patch.Name) - if err != nil { - if errors.Is(err, virtualfs.ErrNodeNotFound) { - return c.SendStatus(fiber.StatusNotFound) - } - return httperr.Internal(err) - } - } - - err = tx.Commit() - if err != nil { - return httperr.Internal(err) - } - - fmt.Printf("node deleted at: %v\n", node.DeletedAt) - - return c.JSON(FileInfo{ - ID: node.PublicID, - Name: node.Name, - Size: node.Size, - MimeType: node.MimeType, - CreatedAt: node.CreatedAt, - UpdatedAt: node.UpdatedAt, - DeletedAt: node.DeletedAt, - }) -} - -func (h *HTTPHandler) deleteFile(c *fiber.Ctx) error { - node := mustCurrentFileNode(c) - - tx, err := h.db.BeginTx(c.Context(), nil) - if err != nil { - return httperr.Internal(err) - } - defer tx.Rollback() - - shouldTrash := c.Query("trash") == "true" - if shouldTrash { - err = h.vfs.SoftDeleteNode(c.Context(), tx, node) - if err != nil { - return httperr.Internal(err) - } - return c.JSON(FileInfo{ - ID: node.PublicID, - Name: node.Name, - Size: node.Size, - MimeType: node.MimeType, - CreatedAt: node.CreatedAt, - UpdatedAt: node.UpdatedAt, - DeletedAt: node.DeletedAt, - }) - } else { - err = h.vfs.PermanentlyDeleteNode(c.Context(), tx, node) - if err != nil { - return httperr.Internal(err) - } - - err = tx.Commit() - if err != nil { - return httperr.Internal(err) - } - - return c.SendStatus(fiber.StatusNoContent) - } + fg := api.Group("/files/:fileID") + fg.Use(h.currentFileMiddleware) + fg.Get("/", h.fetchFile) + fg.Get("/content", h.downloadFile) + fg.Patch("/", h.patchFile) + fg.Delete("/", h.deleteFile) + + api.Post("/directories", h.createDirectory) + + dg := api.Group("/directories/:directoryID") + dg.Use(h.currentDirectoryMiddleware) + dg.Get("/", h.fetchDirectory) + dg.Get("/content", h.listDirectory) + dg.Patch("/", h.patchDirectory) + dg.Delete("/", h.deleteDirectory) }