feat: impl upload api endpoints

This commit is contained in:
2025-11-29 17:25:11 +00:00
parent 6aee150a59
commit 39824e45d9
5 changed files with 155 additions and 22 deletions

View File

@@ -8,8 +8,11 @@ import (
"strconv" "strconv"
"github.com/get-drexa/drexa/internal/auth" "github.com/get-drexa/drexa/internal/auth"
"github.com/get-drexa/drexa/internal/blob"
"github.com/get-drexa/drexa/internal/database" "github.com/get-drexa/drexa/internal/database"
"github.com/get-drexa/drexa/internal/upload"
"github.com/get-drexa/drexa/internal/user" "github.com/get-drexa/drexa/internal/user"
"github.com/get-drexa/drexa/internal/virtualfs"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
@@ -21,21 +24,34 @@ type ServerConfig struct {
JWTSecretKey []byte JWTSecretKey []byte
} }
func NewServer(c ServerConfig) *fiber.App { func NewServer(c ServerConfig) (*fiber.App, error) {
app := fiber.New() app := fiber.New()
db := database.NewFromPostgres(c.PostgresURL) db := database.NewFromPostgres(c.PostgresURL)
// TODO: load correct blob store and resolver from config
blobStore := blob.NewFSStore(blob.FSStoreConfig{
Root: os.Getenv("BLOB_ROOT"),
UploadURL: os.Getenv("BLOB_UPLOAD_URL"),
})
keyResolver := virtualfs.NewFlatKeyResolver()
vfs, err := virtualfs.NewVirtualFS(db, blobStore, keyResolver)
if err != nil {
return nil, fmt.Errorf("failed to create virtual file system: %w", err)
}
userService := user.NewService(db) userService := user.NewService(db)
authService := auth.NewService(db, userService, auth.TokenConfig{ authService := auth.NewService(db, userService, auth.TokenConfig{
Issuer: c.JWTIssuer, Issuer: c.JWTIssuer,
Audience: c.JWTAudience, Audience: c.JWTAudience,
SecretKey: c.JWTSecretKey, SecretKey: c.JWTSecretKey,
}) })
uploadService := upload.NewService(vfs, blobStore)
api := app.Group("/api") api := app.Group("/api")
auth.RegisterAPIRoutes(api, authService) auth.RegisterAPIRoutes(api, authService)
upload.RegisterAPIRoutes(api, uploadService)
return app return app, nil
} }
// ServerConfigFromEnv creates a ServerConfig from environment variables. // ServerConfigFromEnv creates a ServerConfig from environment variables.

View File

@@ -0,0 +1,104 @@
package upload
import (
"errors"
"github.com/get-drexa/drexa/internal/auth"
"github.com/gofiber/fiber/v2"
)
const uploadServiceKey = "uploadService"
type createUploadRequest struct {
ParentID string `json:"parentId"`
Name string `json:"name"`
}
type updateUploadRequest struct {
Status Status `json:"status"`
}
func RegisterAPIRoutes(api fiber.Router, s *Service) {
upload := api.Group("/uploads", func(c *fiber.Ctx) error {
c.Locals(uploadServiceKey, s)
return c.Next()
})
upload.Post("/", createUpload)
upload.Put("/:uploadID/content", receiveUpload)
upload.Patch("/:uploadID", updateUpload)
}
func mustUploadService(c *fiber.Ctx) *Service {
return c.Locals(uploadServiceKey).(*Service)
}
func createUpload(c *fiber.Ctx) error {
u, err := auth.AuthenticatedUser(c)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"})
}
s := mustUploadService(c)
req := new(createUploadRequest)
if err := c.BodyParser(req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"})
}
upload, err := s.CreateUpload(c.Context(), u.ID, CreateUploadOptions{
ParentID: req.ParentID,
Name: req.Name,
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal server error"})
}
return c.JSON(upload)
}
func receiveUpload(c *fiber.Ctx) error {
u, err := auth.AuthenticatedUser(c)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"})
}
s := mustUploadService(c)
uploadID := c.Params("uploadID")
err = s.ReceiveUpload(c.Context(), u.ID, uploadID, c.Request().BodyStream())
defer c.Request().CloseBodyStream()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal server error"})
}
return c.SendStatus(fiber.StatusNoContent)
}
func updateUpload(c *fiber.Ctx) error {
u, err := auth.AuthenticatedUser(c)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"})
}
s := mustUploadService(c)
req := new(updateUploadRequest)
if err := c.BodyParser(req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"})
}
if req.Status == StatusCompleted {
upload, err := s.CompleteUpload(c.Context(), u.ID, c.Params("uploadID"))
if err != nil {
if errors.Is(err, ErrNotFound) {
return c.SendStatus(fiber.StatusNotFound)
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal server error"})
}
return c.JSON(upload)
}
return c.SendStatus(fiber.StatusBadRequest)
}

View File

@@ -33,17 +33,6 @@ type CreateUploadOptions struct {
Name string Name string
} }
type Upload struct {
ID string
TargetNode *virtualfs.Node
UploadURL string
}
type CreateUploadResult struct {
UploadID string
UploadURL string
}
func (s *Service) CreateUpload(ctx context.Context, userID uuid.UUID, opts CreateUploadOptions) (*Upload, error) { func (s *Service) CreateUpload(ctx context.Context, userID uuid.UUID, opts CreateUploadOptions) (*Upload, error) {
parentNode, err := s.vfs.FindNodeByPublicID(ctx, userID, opts.ParentID) parentNode, err := s.vfs.FindNodeByPublicID(ctx, userID, opts.ParentID)
if err != nil { if err != nil {
@@ -77,6 +66,7 @@ func (s *Service) CreateUpload(ctx context.Context, userID uuid.UUID, opts Creat
upload := &Upload{ upload := &Upload{
ID: node.PublicID, ID: node.PublicID,
Status: StatusPending,
TargetNode: node, TargetNode: node,
UploadURL: uploadURL, UploadURL: uploadURL,
} }
@@ -106,36 +96,37 @@ func (s *Service) ReceiveUpload(ctx context.Context, userID uuid.UUID, uploadID
return err return err
} }
s.pendingUploads.Delete(uploadID) upload.Status = StatusCompleted
return nil return nil
} }
func (s *Service) CompleteUpload(ctx context.Context, userID uuid.UUID, uploadID string) error { func (s *Service) CompleteUpload(ctx context.Context, userID uuid.UUID, uploadID string) (*Upload, error) {
n, ok := s.pendingUploads.Load(uploadID) n, ok := s.pendingUploads.Load(uploadID)
if !ok { if !ok {
return ErrNotFound return nil, ErrNotFound
} }
upload, ok := n.(*Upload) upload, ok := n.(*Upload)
if !ok { if !ok {
return ErrNotFound return nil, ErrNotFound
} }
if upload.TargetNode.UserID != userID { if upload.TargetNode.UserID != userID {
return ErrNotFound return nil, ErrNotFound
} }
if upload.TargetNode.Status == virtualfs.NodeStatusReady { if upload.TargetNode.Status == virtualfs.NodeStatusReady && upload.Status == StatusCompleted {
return nil return upload, nil
} }
err := s.vfs.WriteFile(ctx, upload.TargetNode, virtualfs.FileContentFromBlobKey(upload.TargetNode.BlobKey)) err := s.vfs.WriteFile(ctx, upload.TargetNode, virtualfs.FileContentFromBlobKey(upload.TargetNode.BlobKey))
if err != nil { if err != nil {
return err return nil, err
} }
upload.Status = StatusCompleted
s.pendingUploads.Delete(uploadID) s.pendingUploads.Delete(uploadID)
return nil return upload, nil
} }

View File

@@ -0,0 +1,18 @@
package upload
import "github.com/get-drexa/drexa/internal/virtualfs"
type Status string
const (
StatusPending Status = "pending"
StatusCompleted Status = "completed"
StatusFailed Status = "failed"
)
type Upload struct {
ID string `json:"id"`
Status Status `json:"status"`
TargetNode *virtualfs.Node `json:"-"`
UploadURL string `json:"uploadUrl"`
}

View File

@@ -15,6 +15,10 @@ type BlobKeyResolver interface {
type FlatKeyResolver struct{} type FlatKeyResolver struct{}
func NewFlatKeyResolver() *FlatKeyResolver {
return &FlatKeyResolver{}
}
func (r *FlatKeyResolver) KeyMode() blob.KeyMode { func (r *FlatKeyResolver) KeyMode() blob.KeyMode {
return blob.KeyModeStable return blob.KeyModeStable
} }