2025-12-03 00:56:44 +00:00
package catalog
import (
2025-12-17 22:59:18 +00:00
"encoding/base64"
2025-12-03 00:56:44 +00:00
"errors"
2025-12-17 22:59:18 +00:00
"fmt"
2025-12-13 19:24:54 +00:00
"slices"
2025-12-17 22:59:18 +00:00
"strconv"
2025-12-13 19:24:54 +00:00
"strings"
2025-12-03 00:56:44 +00:00
"time"
"github.com/get-drexa/drexa/internal/httperr"
2025-12-27 19:27:08 +00:00
"github.com/get-drexa/drexa/internal/sharing"
2025-12-03 00:56:44 +00:00
"github.com/get-drexa/drexa/internal/virtualfs"
"github.com/gofiber/fiber/v2"
)
const (
DirItemKindDirectory = "directory"
DirItemKindFile = "file"
)
2025-12-13 22:44:37 +00:00
// DirectoryInfo represents directory metadata
// @Description Directory information including path and timestamps
2025-12-03 00:56:44 +00:00
type DirectoryInfo struct {
2025-12-13 22:44:37 +00:00
// Item type, always "directory"
Kind string ` json:"kind" example:"directory" `
// Unique directory identifier
ID string ` json:"id" example:"kRp2XYTq9A55" `
2025-12-17 22:59:18 +00:00
// ParentID is the public ID of the directory this directory is in
2025-12-16 01:47:00 +00:00
ParentID string ` json:"parentId,omitempty" example:"kRp2XYTq9A55" `
2025-12-13 22:44:37 +00:00
// Full path from root (included when ?include=path)
Path virtualfs . Path ` json:"path,omitempty" `
// Directory name
Name string ` json:"name" example:"My Documents" `
// When the directory was created (ISO 8601)
CreatedAt time . Time ` json:"createdAt" example:"2024-12-13T15:04:05Z" `
// When the directory was last updated (ISO 8601)
UpdatedAt time . Time ` json:"updatedAt" example:"2024-12-13T16:30:00Z" `
// When the directory was trashed, null if not trashed (ISO 8601)
DeletedAt * time . Time ` json:"deletedAt,omitempty" example:"2024-12-14T10:00:00Z" `
2025-12-03 00:56:44 +00:00
}
2025-12-13 22:44:37 +00:00
// createDirectoryRequest represents a new directory creation request
// @Description Request to create a new directory
2025-12-03 00:56:44 +00:00
type createDirectoryRequest struct {
2025-12-13 22:44:37 +00:00
// ID of the parent directory
ParentID string ` json:"parentID" example:"kRp2XYTq9A55" `
// Name for the new directory
Name string ` json:"name" example:"New Folder" `
2025-12-03 00:56:44 +00:00
}
2025-12-13 22:44:37 +00:00
// postDirectoryContentRequest represents a move items request
// @Description Request to move items into this directory
2025-12-13 19:24:54 +00:00
type postDirectoryContentRequest struct {
2025-12-13 22:44:37 +00:00
// Array of file/directory IDs to move
Items [ ] string ` json:"items" example:"mElnUNCm8F22,kRp2XYTq9A55" `
2025-12-13 19:24:54 +00:00
}
2025-12-17 22:59:18 +00:00
// listDirectoryResponse represents the response to a request to list the contents of a directory
// @Description Response to a request to list the contents of a directory
type listDirectoryResponse struct {
// Items is the list of items in the directory, limited to the limit specified in the request
Items [ ] any ` json:"items" `
// NextCursor is the cursor to use to get the next page of results
NextCursor string ` json:"nextCursor,omitempty" `
}
2025-12-14 16:43:05 +00:00
// moveItemsToDirectoryResponse represents the response to a request
// to move items into a directory.
// @Description Response from moving items to a directory with status for each item
type moveItemsToDirectoryResponse struct {
// Array of items included in the request (FileInfo or DirectoryInfo objects)
Items [ ] any ` json:"items" `
// Array of IDs of successfully moved items
Moved [ ] string ` json:"moved" example:"mElnUNCm8F22,kRp2XYTq9A55" `
// Array of IDs of items that conflicted with existing items in the target directory
Conflicts [ ] string ` json:"conflicts" example:"xYz123AbC456" `
// Array of errors that occurred during the move operation
Errors [ ] moveItemError ` json:"errors" `
}
// moveItemError represents an error that occurred while moving a specific item
// @Description Error details for a failed item move
type moveItemError struct {
// ID of the item that failed to move
ID string ` json:"id" example:"mElnUNCm8F22" `
// Error message describing what went wrong
Error string ` json:"error" example:"permission denied" `
}
2025-12-17 22:59:18 +00:00
type decodedListChildrenCursor struct {
orderBy virtualfs . ListChildrenOrder
orderDirection virtualfs . ListChildrenDirection
nodeID string
}
2025-12-03 00:56:44 +00:00
func ( h * HTTPHandler ) currentDirectoryMiddleware ( c * fiber . Ctx ) error {
2025-12-27 19:27:08 +00:00
scope , ok := scopeFromCtx ( c )
if ! ok {
2025-12-03 00:56:44 +00:00
return c . SendStatus ( fiber . StatusUnauthorized )
}
directoryID := c . Params ( "directoryID" )
2025-12-15 00:13:10 +00:00
var node * virtualfs . Node
if directoryID == "root" {
2025-12-27 19:27:08 +00:00
n , err := h . vfs . FindNode ( c . Context ( ) , h . db , scope . RootNodeID . String ( ) , scope )
2025-12-15 00:13:10 +00:00
if err != nil {
2025-12-27 19:27:08 +00:00
if errors . Is ( err , virtualfs . ErrNodeNotFound ) {
return c . SendStatus ( fiber . StatusNotFound )
}
2025-12-15 00:13:10 +00:00
return httperr . Internal ( err )
2025-12-03 00:56:44 +00:00
}
2025-12-15 00:13:10 +00:00
node = n
} else {
2025-12-27 19:27:08 +00:00
n , err := h . vfs . FindNodeByPublicID ( c . Context ( ) , h . db , directoryID , scope )
2025-12-15 00:13:10 +00:00
if err != nil {
if errors . Is ( err , virtualfs . ErrNodeNotFound ) {
return c . SendStatus ( fiber . StatusNotFound )
}
return httperr . Internal ( err )
}
node = n
2025-12-03 00:56:44 +00:00
}
c . Locals ( "directory" , node )
return c . Next ( )
}
func mustCurrentDirectoryNode ( c * fiber . Ctx ) * virtualfs . Node {
return c . Locals ( "directory" ) . ( * virtualfs . Node )
}
2025-12-13 19:24:54 +00:00
func includeParam ( c * fiber . Ctx ) [ ] string {
return strings . Split ( c . Query ( "include" ) , "," )
}
2025-12-13 22:44:37 +00:00
// createDirectory creates a new directory
// @Summary Create directory
// @Description Create a new directory within a parent directory
// @Tags directories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param accountID path string true "Account ID" format(uuid)
// @Param request body createDirectoryRequest true "Directory details"
// @Param include query string false "Include additional fields" Enums(path)
// @Success 200 {object} DirectoryInfo "Created directory"
// @Failure 400 {object} map[string]string "Parent not found or not a directory"
// @Failure 401 {string} string "Not authenticated"
// @Failure 409 {object} map[string]string "Directory already exists"
// @Router /accounts/{accountID}/directories [post]
2025-12-03 00:56:44 +00:00
func ( h * HTTPHandler ) createDirectory ( c * fiber . Ctx ) error {
2025-12-27 19:27:08 +00:00
scope , ok := scopeFromCtx ( c )
if ! ok {
2025-12-03 00:56:44 +00:00
return c . SendStatus ( fiber . StatusUnauthorized )
}
req := new ( createDirectoryRequest )
if err := c . BodyParser ( req ) ; err != nil {
return c . SendStatus ( fiber . StatusBadRequest )
}
2025-12-05 00:55:41 +00:00
tx , err := h . db . BeginTx ( c . Context ( ) , nil )
if err != nil {
return httperr . Internal ( err )
}
defer tx . Rollback ( )
2025-12-27 19:27:08 +00:00
parent , err := h . vfs . FindNodeByPublicID ( c . Context ( ) , tx , req . ParentID , scope )
2025-12-03 00:56:44 +00:00
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" } )
}
2025-12-27 19:27:08 +00:00
node , err := h . vfs . CreateDirectory ( c . Context ( ) , tx , parent . ID , req . Name , scope )
2025-12-03 00:56:44 +00:00
if err != nil {
2025-12-05 00:38:31 +00:00
if errors . Is ( err , virtualfs . ErrNodeConflict ) {
return c . Status ( fiber . StatusConflict ) . JSON ( fiber . Map { "error" : "Directory already exists" } )
}
2025-12-27 19:27:08 +00:00
if errors . Is ( err , virtualfs . ErrAccessDenied ) {
return c . SendStatus ( fiber . StatusNotFound )
}
2025-12-03 00:56:44 +00:00
return httperr . Internal ( err )
}
2025-12-13 19:24:54 +00:00
i := DirectoryInfo {
2025-12-03 00:56:44 +00:00
Kind : DirItemKindDirectory ,
ID : node . PublicID ,
Name : node . Name ,
CreatedAt : node . CreatedAt ,
UpdatedAt : node . UpdatedAt ,
DeletedAt : node . DeletedAt ,
2025-12-13 19:24:54 +00:00
}
include := includeParam ( c )
if slices . Contains ( include , "path" ) {
2025-12-27 19:27:08 +00:00
p , err := h . vfs . RealPath ( c . Context ( ) , tx , node , scope )
2025-12-13 19:24:54 +00:00
if err != nil {
2025-12-27 19:27:08 +00:00
if errors . Is ( err , virtualfs . ErrAccessDenied ) {
return c . SendStatus ( fiber . StatusNotFound )
}
2025-12-13 19:24:54 +00:00
return httperr . Internal ( err )
}
i . Path = p
}
err = tx . Commit ( )
if err != nil {
return httperr . Internal ( err )
}
return c . JSON ( i )
2025-12-03 00:56:44 +00:00
}
2025-12-13 22:44:37 +00:00
// fetchDirectory returns directory metadata
// @Summary Get directory info
// @Description Retrieve metadata for a specific directory
// @Tags directories
// @Produce json
// @Security BearerAuth
// @Param accountID path string true "Account ID" format(uuid)
// @Param directoryID path string true "Directory ID"
// @Param include query string false "Include additional fields" Enums(path)
// @Success 200 {object} DirectoryInfo "Directory metadata"
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {string} string "Directory not found"
// @Router /accounts/{accountID}/directories/{directoryID} [get]
2025-12-03 00:56:44 +00:00
func ( h * HTTPHandler ) fetchDirectory ( c * fiber . Ctx ) error {
node := mustCurrentDirectoryNode ( c )
2025-12-27 19:27:08 +00:00
scope , ok := scopeFromCtx ( c )
if ! ok {
return c . SendStatus ( fiber . StatusUnauthorized )
}
2025-12-05 00:38:05 +00:00
2025-12-03 00:56:44 +00:00
i := DirectoryInfo {
Kind : DirItemKindDirectory ,
ID : node . PublicID ,
Name : node . Name ,
CreatedAt : node . CreatedAt ,
UpdatedAt : node . UpdatedAt ,
DeletedAt : node . DeletedAt ,
}
2025-12-05 00:38:05 +00:00
2025-12-13 19:24:54 +00:00
include := includeParam ( c )
if slices . Contains ( include , "path" ) {
2025-12-27 19:27:08 +00:00
p , err := h . vfs . RealPath ( c . Context ( ) , h . db , node , scope )
2025-12-05 00:38:05 +00:00
if err != nil {
2025-12-27 19:27:08 +00:00
if errors . Is ( err , virtualfs . ErrAccessDenied ) {
return c . SendStatus ( fiber . StatusNotFound )
}
2025-12-05 00:38:05 +00:00
return httperr . Internal ( err )
}
i . Path = p
}
2025-12-03 00:56:44 +00:00
return c . JSON ( i )
}
2025-12-13 22:44:37 +00:00
// listDirectory returns directory contents
// @Summary List directory contents
2025-12-27 19:27:08 +00:00
// @Description Get all files and subdirectories within a directory with optional pagination and sorting
2025-12-13 22:44:37 +00:00
// @Tags directories
// @Produce json
// @Security BearerAuth
// @Param accountID path string true "Account ID" format(uuid)
2025-12-17 22:59:18 +00:00
// @Param directoryID path string true "Directory ID (use 'root' for the root directory)"
// @Param orderBy query string false "Sort field: name, createdAt, or updatedAt" Enums(name,createdAt,updatedAt)
// @Param dir query string false "Sort direction: asc or desc" Enums(asc,desc)
// @Param limit query integer false "Maximum number of items to return (default: 100, min: 1)"
// @Param cursor query string false "Cursor for pagination (base64-encoded cursor from previous response)"
// @Success 200 {object} listDirectoryResponse "Paginated list of FileInfo and DirectoryInfo objects"
// @Failure 400 {object} map[string]string "Invalid limit or cursor"
2025-12-13 22:44:37 +00:00
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {string} string "Directory not found"
// @Router /accounts/{accountID}/directories/{directoryID}/content [get]
2025-12-03 00:56:44 +00:00
func ( h * HTTPHandler ) listDirectory ( c * fiber . Ctx ) error {
node := mustCurrentDirectoryNode ( c )
2025-12-27 19:27:08 +00:00
scope , ok := scopeFromCtx ( c )
if ! ok {
return c . SendStatus ( fiber . StatusUnauthorized )
}
2025-12-17 22:59:18 +00:00
opts := virtualfs . ListChildrenOptions { }
if by := c . Query ( "orderBy" ) ; by != "" {
switch by {
case "name" :
opts . OrderBy = virtualfs . ListChildrenOrderByName
case "createdAt" :
opts . OrderBy = virtualfs . ListChildrenOrderByCreatedAt
case "updatedAt" :
opts . OrderBy = virtualfs . ListChildrenOrderByUpdatedAt
}
}
if dir := c . Query ( "dir" ) ; dir != "" {
switch dir {
case "asc" :
opts . OrderDirection = virtualfs . ListChildrenDirectionAsc
case "desc" :
opts . OrderDirection = virtualfs . ListChildrenDirectionDesc
}
}
if limit := c . Query ( "limit" ) ; limit != "" {
limit , err := strconv . Atoi ( limit )
if err != nil {
return c . Status ( fiber . StatusBadRequest ) . JSON ( fiber . Map { "error" : "Invalid limit" } )
}
if limit < 1 {
return c . Status ( fiber . StatusBadRequest ) . JSON ( fiber . Map { "error" : "Limit must be at least 1" } )
}
opts . Limit = limit
}
if cursor := c . Query ( "cursor" ) ; cursor != "" {
dc , err := decodeListChildrenCursor ( cursor )
2025-12-18 00:47:41 +00:00
fmt . Printf ( "dc: %v\n" , dc )
2025-12-17 22:59:18 +00:00
if err != nil {
return c . Status ( fiber . StatusBadRequest ) . JSON ( fiber . Map { "error" : "invalid cursor" } )
}
2025-12-27 19:27:08 +00:00
n , err := h . vfs . FindNodeByPublicID ( c . Context ( ) , h . db , dc . nodeID , scope )
2025-12-17 22:59:18 +00:00
if err != nil {
if errors . Is ( err , virtualfs . ErrNodeNotFound ) {
return c . Status ( fiber . StatusBadRequest ) . JSON ( fiber . Map { "error" : "invalid cursor" } )
}
return httperr . Internal ( err )
}
opts . Cursor = & virtualfs . ListChildrenCursor {
Node : n ,
OrderBy : dc . orderBy ,
OrderDirection : dc . orderDirection ,
}
}
2025-12-27 19:27:08 +00:00
children , cursor , err := h . vfs . ListChildren ( c . Context ( ) , h . db , node , opts , scope )
2025-12-03 00:56:44 +00:00
if err != nil {
if errors . Is ( err , virtualfs . ErrNodeNotFound ) {
return c . SendStatus ( fiber . StatusNotFound )
}
2025-12-27 19:27:08 +00:00
if errors . Is ( err , virtualfs . ErrAccessDenied ) {
return c . SendStatus ( fiber . StatusNotFound )
}
2025-12-03 00:56:44 +00:00
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 ,
2025-12-16 01:47:00 +00:00
ParentID : node . PublicID ,
2025-12-03 00:56:44 +00:00
Name : child . Name ,
Size : child . Size ,
MimeType : child . MimeType ,
CreatedAt : child . CreatedAt ,
UpdatedAt : child . UpdatedAt ,
DeletedAt : child . DeletedAt ,
}
}
}
2025-12-18 00:47:41 +00:00
if cursor != nil {
return c . JSON ( listDirectoryResponse {
Items : items ,
NextCursor : encodeListChildrenCursor ( cursor ) ,
} )
}
2025-12-17 22:59:18 +00:00
return c . JSON ( listDirectoryResponse {
2025-12-18 00:47:41 +00:00
Items : items ,
2025-12-17 22:59:18 +00:00
} )
2025-12-18 00:47:41 +00:00
2025-12-03 00:56:44 +00:00
}
2025-12-13 22:44:37 +00:00
// patchDirectory updates directory properties
// @Summary Update directory
// @Description Update directory properties such as name (rename)
// @Tags directories
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param accountID path string true "Account ID" format(uuid)
// @Param directoryID path string true "Directory ID"
// @Param request body patchDirectoryRequest true "Directory update"
// @Success 200 {object} DirectoryInfo "Updated directory metadata"
// @Failure 400 {object} map[string]string "Invalid request"
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {string} string "Directory not found"
// @Router /accounts/{accountID}/directories/{directoryID} [patch]
2025-12-03 00:56:44 +00:00
func ( h * HTTPHandler ) patchDirectory ( c * fiber . Ctx ) error {
node := mustCurrentDirectoryNode ( c )
2025-12-27 19:27:08 +00:00
scope , ok := scopeFromCtx ( c )
if ! ok {
return c . SendStatus ( fiber . StatusUnauthorized )
}
2025-12-03 00:56:44 +00:00
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 != "" {
2025-12-27 19:27:08 +00:00
err := h . vfs . RenameNode ( c . Context ( ) , tx , node , patch . Name , scope )
2025-12-03 00:56:44 +00:00
if err != nil {
if errors . Is ( err , virtualfs . ErrNodeNotFound ) {
return c . SendStatus ( fiber . StatusNotFound )
}
2025-12-27 19:27:08 +00:00
if errors . Is ( err , virtualfs . ErrAccessDenied ) {
return c . SendStatus ( fiber . StatusNotFound )
}
2025-12-03 00:56:44 +00:00
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 ,
} )
}
2025-12-13 22:44:37 +00:00
// deleteDirectory removes a directory
// @Summary Delete directory
// @Description Delete a directory permanently or move it to trash. Deleting a directory also affects all its contents.
// @Tags directories
// @Security BearerAuth
// @Param accountID path string true "Account ID" format(uuid)
// @Param directoryID path string true "Directory ID"
// @Param trash query bool false "Move to trash instead of permanent delete" default(false)
2025-12-27 19:27:08 +00:00
// @Success 200 {object} DirectoryInfo "Trashed directory info (when trash=true)"
2025-12-13 22:44:37 +00:00
// @Success 204 {string} string "Directory deleted"
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {string} string "Directory not found"
// @Router /accounts/{accountID}/directories/{directoryID} [delete]
2025-12-03 00:56:44 +00:00
func ( h * HTTPHandler ) deleteDirectory ( c * fiber . Ctx ) error {
node := mustCurrentDirectoryNode ( c )
2025-12-27 19:27:08 +00:00
scope , ok := scopeFromCtx ( c )
if ! ok {
return c . SendStatus ( fiber . StatusUnauthorized )
}
2025-12-03 00:56:44 +00:00
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 {
2025-12-27 19:27:08 +00:00
_ , err := h . vfs . SoftDeleteNode ( c . Context ( ) , tx , node , scope )
2025-12-03 00:56:44 +00:00
if err != nil {
2025-12-27 19:27:08 +00:00
if errors . Is ( err , virtualfs . ErrAccessDenied ) {
return c . SendStatus ( fiber . StatusNotFound )
}
2025-12-03 00:56:44 +00:00
return httperr . Internal ( err )
}
2025-12-15 00:13:10 +00:00
err = tx . Commit ( )
if err != nil {
return httperr . Internal ( err )
}
return c . JSON ( directoryInfoFromNode ( node ) )
2025-12-03 00:56:44 +00:00
} else {
2025-12-27 19:27:08 +00:00
err = h . vfs . PermanentlyDeleteNode ( c . Context ( ) , tx , node , scope )
2025-12-03 00:56:44 +00:00
if err != nil {
2025-12-27 19:27:08 +00:00
if errors . Is ( err , virtualfs . ErrAccessDenied ) {
return c . SendStatus ( fiber . StatusNotFound )
}
2025-12-03 00:56:44 +00:00
return httperr . Internal ( err )
}
2025-12-15 00:13:10 +00:00
err = tx . Commit ( )
if err != nil {
return httperr . Internal ( err )
}
return c . SendStatus ( fiber . StatusNoContent )
2025-12-03 00:56:44 +00:00
}
2025-12-15 00:13:10 +00:00
}
// deleteDirectories removes multiple directories
// @Summary Bulk delete directories
// @Description Delete multiple directories permanently or move them to trash. Deleting directories also affects all their contents. All items must be directories.
// @Tags directories
// @Security BearerAuth
// @Param accountID path string true "Account ID" format(uuid)
// @Param id query string true "Comma-separated list of directory IDs to delete" example:"kRp2XYTq9A55,xYz123AbC456"
// @Param trash query bool false "Move to trash instead of permanent delete" default(false)
2025-12-27 19:27:08 +00:00
// @Success 200 {array} DirectoryInfo "Trashed directories (when trash=true)"
2025-12-15 00:13:10 +00:00
// @Success 204 {string} string "Directories deleted"
// @Failure 400 {object} map[string]string "All items must be directories"
// @Failure 401 {string} string "Not authenticated"
// @Router /accounts/{accountID}/directories [delete]
func ( h * HTTPHandler ) deleteDirectories ( c * fiber . Ctx ) error {
2025-12-27 19:27:08 +00:00
scope , ok := scopeFromCtx ( c )
if ! ok {
2025-12-15 00:13:10 +00:00
return c . SendStatus ( fiber . StatusUnauthorized )
}
idq := c . Query ( "id" , "" )
if idq == "" {
return c . SendStatus ( fiber . StatusNoContent )
}
ids := strings . Split ( idq , "," )
if len ( ids ) == 0 {
return c . SendStatus ( fiber . StatusNoContent )
}
shouldTrash := c . Query ( "trash" ) == "true"
tx , err := h . db . BeginTx ( c . Context ( ) , nil )
2025-12-03 00:56:44 +00:00
if err != nil {
return httperr . Internal ( err )
}
2025-12-15 00:13:10 +00:00
defer tx . Rollback ( )
2025-12-27 19:27:08 +00:00
nodes , err := h . vfs . FindNodesByPublicID ( c . Context ( ) , tx , ids , scope )
2025-12-15 00:13:10 +00:00
if err != nil {
return httperr . Internal ( err )
}
if len ( nodes ) == 0 {
return c . SendStatus ( fiber . StatusNoContent )
}
for _ , node := range nodes {
if node . Kind != virtualfs . NodeKindDirectory {
return httperr . NewHTTPError ( fiber . StatusBadRequest , "all items must be directories" , nil )
}
}
if shouldTrash {
2025-12-27 19:27:08 +00:00
deleted , err := h . vfs . SoftDeleteNodes ( c . Context ( ) , tx , nodes , scope )
2025-12-15 00:13:10 +00:00
if err != nil {
2025-12-27 19:27:08 +00:00
if errors . Is ( err , virtualfs . ErrAccessDenied ) {
return c . SendStatus ( fiber . StatusNotFound )
}
2025-12-15 00:13:10 +00:00
return httperr . Internal ( err )
}
err = tx . Commit ( )
if err != nil {
return httperr . Internal ( err )
}
res := make ( [ ] DirectoryInfo , 0 , len ( deleted ) )
for _ , node := range deleted {
res = append ( res , directoryInfoFromNode ( node ) )
}
2025-12-27 19:27:08 +00:00
return c . JSON ( res )
2025-12-15 00:13:10 +00:00
} else {
for _ , node := range nodes {
2025-12-27 19:27:08 +00:00
err = h . vfs . PermanentlyDeleteNode ( c . Context ( ) , tx , node , scope )
2025-12-15 00:13:10 +00:00
if err != nil {
2025-12-27 19:27:08 +00:00
if errors . Is ( err , virtualfs . ErrAccessDenied ) {
return c . SendStatus ( fiber . StatusNotFound )
}
2025-12-15 00:13:10 +00:00
return httperr . Internal ( err )
}
}
err = tx . Commit ( )
if err != nil {
return httperr . Internal ( err )
}
return c . SendStatus ( fiber . StatusNoContent )
}
2025-12-03 00:56:44 +00:00
}
2025-12-13 19:24:54 +00:00
2025-12-13 22:44:37 +00:00
// moveItemsToDirectory moves files and directories into this directory
// @Summary Move items to directory
2025-12-14 16:43:05 +00:00
// @Description Move one or more files or directories into this directory. Returns detailed status for each item including which were successfully moved, which had conflicts, and which encountered errors.
2025-12-13 22:44:37 +00:00
// @Tags directories
// @Accept json
2025-12-14 16:43:05 +00:00
// @Produce json
2025-12-13 22:44:37 +00:00
// @Security BearerAuth
// @Param accountID path string true "Account ID" format(uuid)
// @Param directoryID path string true "Target directory ID"
// @Param request body postDirectoryContentRequest true "Items to move"
2025-12-14 16:43:05 +00:00
// @Success 200 {object} moveItemsToDirectoryResponse "Move operation results with moved, conflict, and error states"
2025-12-13 22:44:37 +00:00
// @Failure 400 {object} map[string]string "Invalid request or items not in same directory"
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {object} map[string]string "One or more items not found"
// @Router /accounts/{accountID}/directories/{directoryID}/content [post]
2025-12-13 19:24:54 +00:00
func ( h * HTTPHandler ) moveItemsToDirectory ( c * fiber . Ctx ) error {
2025-12-27 19:27:08 +00:00
scope , ok := scopeFromCtx ( c )
if ! ok {
2025-12-13 19:24:54 +00:00
return c . SendStatus ( fiber . StatusUnauthorized )
}
targetDir := mustCurrentDirectoryNode ( c )
req := new ( postDirectoryContentRequest )
if err := c . BodyParser ( req ) ; err != nil {
return c . Status ( fiber . StatusBadRequest ) . JSON ( fiber . Map { "error" : "Invalid request" } )
}
if len ( req . Items ) == 0 {
return c . SendStatus ( fiber . StatusNoContent )
}
tx , err := h . db . BeginTx ( c . Context ( ) , nil )
if err != nil {
return httperr . Internal ( err )
}
defer tx . Rollback ( )
2025-12-27 19:27:08 +00:00
nodes , err := h . vfs . FindNodesByPublicID ( c . Context ( ) , tx , req . Items , scope )
2025-12-13 19:24:54 +00:00
if err != nil {
return httperr . Internal ( err )
}
if len ( nodes ) != len ( req . Items ) {
return c . Status ( fiber . StatusNotFound ) . JSON ( fiber . Map { "error" : "One or more items not found" } )
}
// Move all nodes to the target directory
2025-12-27 19:27:08 +00:00
result , err := h . vfs . MoveNodesInSameDirectory ( c . Context ( ) , tx , nodes , targetDir . ID , scope )
2025-12-13 19:24:54 +00:00
if err != nil {
if errors . Is ( err , virtualfs . ErrUnsupportedOperation ) {
return c . Status ( fiber . StatusBadRequest ) . JSON ( fiber . Map { "error" : "All items must be in the same directory" } )
}
if errors . Is ( err , virtualfs . ErrNodeConflict ) {
return c . Status ( fiber . StatusConflict ) . JSON ( fiber . Map { "error" : "Name conflict in target directory" } )
}
2025-12-27 19:27:08 +00:00
if errors . Is ( err , virtualfs . ErrAccessDenied ) {
return c . SendStatus ( fiber . StatusNotFound )
}
2025-12-13 19:24:54 +00:00
return httperr . Internal ( err )
}
err = tx . Commit ( )
if err != nil {
return httperr . Internal ( err )
}
2025-12-16 01:47:00 +00:00
res := moveItemsToDirectoryResponse {
Items : make ( [ ] any , 0 ) ,
Moved : make ( [ ] string , 0 ) ,
Conflicts : make ( [ ] string , 0 ) ,
Errors : make ( [ ] moveItemError , 0 ) ,
}
2025-12-14 16:43:05 +00:00
for _ , node := range result . Moved {
res . Items = append ( res . Items , toDirectoryItem ( node ) )
res . Moved = append ( res . Moved , node . PublicID )
}
for _ , node := range result . Conflicts {
res . Items = append ( res . Items , toDirectoryItem ( node ) )
res . Conflicts = append ( res . Conflicts , node . PublicID )
}
for _ , err := range result . Errors {
res . Errors = append ( res . Errors , moveItemError {
ID : err . Node . PublicID ,
Error : err . Error . Error ( ) ,
} )
}
return c . JSON ( res )
2025-12-13 19:24:54 +00:00
}
2025-12-17 22:59:18 +00:00
func encodeListChildrenCursor ( cursor * virtualfs . ListChildrenCursor ) string {
var by int
switch cursor . OrderBy {
case virtualfs . ListChildrenOrderByName :
by = 0
case virtualfs . ListChildrenOrderByCreatedAt :
by = 1
case virtualfs . ListChildrenOrderByUpdatedAt :
by = 2
}
var d int
switch cursor . OrderDirection {
case virtualfs . ListChildrenDirectionAsc :
d = 0
case virtualfs . ListChildrenDirectionDesc :
d = 1
}
2025-12-18 00:47:41 +00:00
s := fmt . Sprintf ( "%d:%d:%s" , by , d , cursor . Node . PublicID )
2025-12-17 22:59:18 +00:00
return base64 . URLEncoding . EncodeToString ( [ ] byte ( s ) )
}
func decodeListChildrenCursor ( s string ) ( * decodedListChildrenCursor , error ) {
bs , err := base64 . URLEncoding . DecodeString ( s )
if err != nil {
return nil , err
}
parts := strings . Split ( string ( bs ) , ":" )
if len ( parts ) != 3 {
return nil , errors . New ( "invalid cursor" )
}
c := new ( decodedListChildrenCursor )
switch parts [ 0 ] {
case "0" :
c . orderBy = virtualfs . ListChildrenOrderByName
case "1" :
c . orderBy = virtualfs . ListChildrenOrderByCreatedAt
case "2" :
c . orderBy = virtualfs . ListChildrenOrderByUpdatedAt
default :
return nil , errors . New ( "invalid cursor" )
}
switch parts [ 1 ] {
case "0" :
c . orderDirection = virtualfs . ListChildrenDirectionAsc
case "1" :
c . orderDirection = virtualfs . ListChildrenDirectionDesc
default :
return nil , errors . New ( "invalid cursor" )
}
c . nodeID = parts [ 2 ]
return c , nil
}
2025-12-27 19:27:08 +00:00
// listDirectoryShares returns all shares that include this directory
// @Summary List directory shares
// @Description Get all share links that include this directory
// @Tags directories
// @Produce json
// @Param accountID path string true "Account ID" format(uuid)
// @Param directoryID path string true "Directory ID"
// @Success 200 {array} sharing.Share "Array of shares"
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {string} string "Directory not found"
// @Security BearerAuth
// @Router /accounts/{accountID}/directories/{directoryID}/shares [get]
func ( h * HTTPHandler ) listDirectoryShares ( c * fiber . Ctx ) error {
node := mustCurrentDirectoryNode ( c )
2025-12-28 22:12:55 +00:00
includesExpired := c . Query ( "includesExpired" ) == "true"
2025-12-27 19:27:08 +00:00
shares , err := h . sharingService . ListShares ( c . Context ( ) , h . db , node . AccountID , sharing . ListSharesOptions {
2025-12-28 22:12:55 +00:00
Items : [ ] * virtualfs . Node { node } ,
IncludesExpired : includesExpired ,
2025-12-27 19:27:08 +00:00
} )
if err != nil {
return httperr . Internal ( err )
}
return c . JSON ( shares )
}