package files import ( "context" "errors" "fmt" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) // Ordner: anlegen, umbenennen, verschieben, löschen, auflisten. // ── Folders ────────────────────────────────────────────────────────────────── func (s *Store) CreateFolder(ctx context.Context, f *Folder) (*Folder, error) { var out Folder err := s.db.QueryRow(ctx, `INSERT INTO folders (owner_id, group_id, parent_id, name) VALUES ($1, $2, $3, $4) RETURNING id, owner_id, group_id, parent_id, name, created_at`, f.OwnerID, f.GroupID, f.ParentID, f.Name, ).Scan(&out.ID, &out.OwnerID, &out.GroupID, &out.ParentID, &out.Name, &out.CreatedAt) if err != nil { if isUniqueViolation(err) { return nil, ErrNameTaken } return nil, fmt.Errorf("create folder: %w", err) } return &out, nil } func (s *Store) GetFolder(ctx context.Context, id, ownerID uuid.UUID) (*Folder, error) { var f Folder err := s.db.QueryRow(ctx, `SELECT id, owner_id, group_id, parent_id, name, created_at FROM folders WHERE id = $1 AND (owner_id = $2 OR group_id = $2)`, id, ownerID, ).Scan(&f.ID, &f.OwnerID, &f.GroupID, &f.ParentID, &f.Name, &f.CreatedAt) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, fmt.Errorf("get folder: %w", err) } return &f, nil } // ListFilesInSubtree returns all non-deleted files inside a folder and all its descendants. func (s *Store) ListFilesInSubtree(ctx context.Context, ownerID, rootFolderID uuid.UUID) ([]*File, error) { rows, err := s.db.Query(ctx, `WITH RECURSIVE subtree AS ( SELECT id FROM folders WHERE id=$1 AND (owner_id = $2 OR group_id = $2) UNION ALL SELECT f.id FROM folders f JOIN subtree s ON f.parent_id=s.id WHERE (f.owner_id = $2 OR f.group_id = $2) ) SELECT `+fileCols+` FROM files WHERE (owner_id = $2 OR group_id = $2) AND is_deleted=false AND folder_id IN (SELECT id FROM subtree)`, rootFolderID, ownerID, ) if err != nil { return nil, fmt.Errorf("list files in subtree: %w", err) } defer rows.Close() files := make([]*File, 0) for rows.Next() { var f File if err := scanFile(rows, &f); err != nil { return nil, err } files = append(files, &f) } return files, rows.Err() } // UpdateFileStorageKey updates only the storage_key of a file (used after blob moves). func (s *Store) UpdateFileStorageKey(ctx context.Context, id uuid.UUID, storageKey string) error { _, err := s.db.Exec(ctx, `UPDATE files SET storage_key=$2 WHERE id=$1`, id, storageKey) return err } // RenameFolder renames a folder. Blob path cascading is handled by the handler. func (s *Store) RenameFolder(ctx context.Context, id, ownerID uuid.UUID, name string) (*Folder, error) { var f Folder err := s.db.QueryRow(ctx, `UPDATE folders SET name=$3 WHERE id=$1 AND (owner_id = $2 OR group_id = $2) RETURNING id, owner_id, group_id, parent_id, name, created_at`, id, ownerID, name, ).Scan(&f.ID, &f.OwnerID, &f.GroupID, &f.ParentID, &f.Name, &f.CreatedAt) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } if err != nil { if isUniqueViolation(err) { return nil, ErrNameTaken } return nil, fmt.Errorf("rename folder: %w", err) } return &f, nil } // DeleteFolder soft-deletes all files in the subtree (storage_keys must already be // updated to trash paths by the caller) and hard-deletes the folder tree. func (s *Store) DeleteFolder(ctx context.Context, id, ownerID uuid.UUID) error { tx, err := s.db.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) _, err = tx.Exec(ctx, `WITH RECURSIVE subtree AS ( SELECT id FROM folders WHERE id=$1 AND (owner_id = $2 OR group_id = $2) UNION ALL SELECT f.id FROM folders f JOIN subtree s ON f.parent_id=s.id WHERE (f.owner_id = $2 OR f.group_id = $2) ) UPDATE files SET is_deleted=true, deleted_at=NOW() WHERE (owner_id = $2 OR group_id = $2) AND is_deleted=false AND folder_id IN (SELECT id FROM subtree)`, id, ownerID, ) if err != nil { return fmt.Errorf("soft-delete files in folder: %w", err) } _, err = tx.Exec(ctx, `WITH RECURSIVE subtree AS ( SELECT id FROM folders WHERE id=$1 AND (owner_id = $2 OR group_id = $2) UNION ALL SELECT f.id FROM folders f JOIN subtree s ON f.parent_id=s.id WHERE (f.owner_id = $2 OR f.group_id = $2) ) DELETE FROM folders WHERE id IN (SELECT id FROM subtree) AND (owner_id = $2 OR group_id = $2)`, id, ownerID, ) if err != nil { return fmt.Errorf("delete folders: %w", err) } return tx.Commit(ctx) } func (s *Store) ListFolders(ctx context.Context, ownerID uuid.UUID, parentID *uuid.UUID) ([]*Folder, error) { var rows pgx.Rows var err error if parentID == nil { rows, err = s.db.Query(ctx, `SELECT id, owner_id, group_id, parent_id, name, created_at FROM folders WHERE (owner_id = $1 OR group_id = $1) AND parent_id IS NULL ORDER BY name`, ownerID, ) } else { rows, err = s.db.Query(ctx, `SELECT id, owner_id, group_id, parent_id, name, created_at FROM folders WHERE (owner_id = $1 OR group_id = $1) AND parent_id = $2 ORDER BY name`, ownerID, parentID, ) } if err != nil { return nil, fmt.Errorf("list folders: %w", err) } defer rows.Close() folders := make([]*Folder, 0) for rows.Next() { var f Folder if err := rows.Scan(&f.ID, &f.OwnerID, &f.GroupID, &f.ParentID, &f.Name, &f.CreatedAt); err != nil { return nil, err } folders = append(folders, &f) } return folders, rows.Err() } // MoveFolder changes a folder's parent. Returns ErrCycle if newParentID is a descendant. func (s *Store) MoveFolder(ctx context.Context, id, ownerID uuid.UUID, newParentID *uuid.UUID) (*Folder, error) { if newParentID != nil { var isDescendant bool err := s.db.QueryRow(ctx, `WITH RECURSIVE subtree AS ( SELECT id FROM folders WHERE id=$1 AND (owner_id = $2 OR group_id = $2) UNION ALL SELECT f.id FROM folders f JOIN subtree s ON f.parent_id=s.id WHERE (f.owner_id = $2 OR f.group_id = $2) ) SELECT EXISTS(SELECT 1 FROM subtree WHERE id=$3)`, id, ownerID, *newParentID, ).Scan(&isDescendant) if err != nil { return nil, fmt.Errorf("cycle check: %w", err) } if isDescendant { return nil, ErrCycle } } var f Folder err := s.db.QueryRow(ctx, `UPDATE folders SET parent_id=$3 WHERE id=$1 AND (owner_id = $2 OR group_id = $2) RETURNING id, owner_id, group_id, parent_id, name, created_at`, id, ownerID, newParentID, ).Scan(&f.ID, &f.OwnerID, &f.GroupID, &f.ParentID, &f.Name, &f.CreatedAt) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } if err != nil { if isUniqueViolation(err) { return nil, ErrNameTaken } return nil, fmt.Errorf("move folder: %w", err) } return &f, nil }