Skip to content
28 changes: 25 additions & 3 deletions internal/server/blob/blob_backend_s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,12 +221,34 @@ func (s *S3Backend) CompleteMultipartUpload(ctx context.Context, params *Complet
return nil, err
}

return &PutObjectResponse{
// Get the object metadata to retrieve the actual size
headResp, err := s.s3Client.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &s.config.BucketName,
Key: &params.Key,
})
if err != nil {
return nil, err
}

result := &PutObjectResponse{
Key: params.Key,
Version: aws.ToString(res.VersionId),
ETag: strings.ReplaceAll(aws.ToString(res.ETag), "\"", ""),
LastModified: time.Now().UTC(),
}, nil
Size: aws.ToInt64(headResp.ContentLength),
LastModified: aws.ToTime(headResp.LastModified),
}

// Call the afterPutObject hook to update the index
if s.hooks != nil && s.hooks.AfterPutObject != nil {
putParams := &PutObjectParams{
Key: params.Key,
ETag: result.ETag,
Size: result.Size,
}
s.hooks.AfterPutObject(putParams, result)
}

return result, nil
}

// ===================================================================================================
Expand Down
120 changes: 110 additions & 10 deletions internal/server/handlers/blob/blob_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,6 @@ func New(blob *blob.BlobService, acl *acl.ACLService) *BlobHandler {
return &BlobHandler{blob: blob, acl: acl}
}

func (h *BlobHandler) UploadMultipart(ctx *gin.Context) {
// todo
api.AbortWithError(ctx, http.StatusNotImplemented, api.CodeInvalidRequest, fmt.Errorf("not implemented"))
}

func (h *BlobHandler) UploadComplete(ctx *gin.Context) {
// todo
api.AbortWithError(ctx, http.StatusNotImplemented, api.CodeInvalidRequest, fmt.Errorf("not implemented"))
}

func (h *BlobHandler) ListObjects(ctx *gin.Context) {
res, err := h.blob.Index().List()
if err != nil {
Expand Down Expand Up @@ -77,3 +67,113 @@ func IsReservedPath(path string) bool {

return false
}

// UploadMultipart initiates a multipart upload and returns presigned URLs for each part
func (h *BlobHandler) UploadMultipart(ctx *gin.Context) {
var req MultipartUploadRequest
user := ctx.GetString("user")

if err := ctx.ShouldBindJSON(&req); err != nil {
api.AbortWithError(ctx, http.StatusBadRequest, api.CodeInvalidRequest, fmt.Errorf("failed to bind json: %w", err))
return
}

// Validate key format
if !datasite.IsValidPath(req.Key) {
api.AbortWithError(ctx, http.StatusBadRequest, api.CodeDatasiteInvalidPath, fmt.Errorf("invalid key"))
return
}

// Check for reserved paths
if IsReservedPath(req.Key) {
api.AbortWithError(ctx, http.StatusBadRequest, api.CodeDatasiteInvalidPath, fmt.Errorf("reserved path"))
return
}

// Check permissions
if err := h.checkPermissions(req.Key, user, acl.AccessWrite); err != nil {
api.AbortWithError(ctx, http.StatusForbidden, api.CodeAccessDenied, err)
return
}

// Create multipart upload parameters
params := &blob.PutObjectMultipartParams{
Key: req.Key,
Parts: uint16(req.Parts),
}

// Initiate multipart upload
resp, err := h.blob.Backend().PutObjectMultipart(ctx, params)
if err != nil {
api.AbortWithError(ctx, http.StatusInternalServerError, api.CodeBlobPutFailed, fmt.Errorf("failed to initiate multipart upload: %w", err))
return
}

// Return response
ctx.JSON(http.StatusOK, &MultipartUploadResponse{
Key: resp.Key,
UploadID: resp.UploadID,
URLs: resp.URLs,
})
}

// UploadComplete completes a multipart upload
func (h *BlobHandler) UploadComplete(ctx *gin.Context) {
var req CompleteUploadRequest
user := ctx.GetString("user")

if err := ctx.ShouldBindJSON(&req); err != nil {
api.AbortWithError(ctx, http.StatusBadRequest, api.CodeInvalidRequest, fmt.Errorf("failed to bind json: %w", err))
return
}

// Validate key format
if !datasite.IsValidPath(req.Key) {
api.AbortWithError(ctx, http.StatusBadRequest, api.CodeDatasiteInvalidPath, fmt.Errorf("invalid key"))
return
}

// Check for reserved paths
if IsReservedPath(req.Key) {
api.AbortWithError(ctx, http.StatusBadRequest, api.CodeDatasiteInvalidPath, fmt.Errorf("reserved path"))
return
}

// Check permissions
if err := h.checkPermissions(req.Key, user, acl.AccessWrite); err != nil {
api.AbortWithError(ctx, http.StatusForbidden, api.CodeAccessDenied, err)
return
}

// Convert request parts to backend format
parts := make([]*blob.CompletedPart, len(req.Parts))
for i, part := range req.Parts {
parts[i] = &blob.CompletedPart{
PartNumber: part.PartNumber,
ETag: part.ETag,
}
}

// Create complete multipart upload parameters
params := &blob.CompleteMultipartUploadParams{
Key: req.Key,
UploadID: req.UploadID,
Parts: parts,
}

// Complete multipart upload
resp, err := h.blob.Backend().CompleteMultipartUpload(ctx, params)
if err != nil {
api.AbortWithError(ctx, http.StatusInternalServerError, api.CodeBlobPutFailed, fmt.Errorf("failed to complete multipart upload: %w", err))
return
}

// Return response
ctx.JSON(http.StatusOK, &CompleteUploadResponse{
Key: resp.Key,
Version: resp.Version,
ETag: resp.ETag,
Size: resp.Size,
LastModified: resp.LastModified.Format("2006-01-02T15:04:05Z"),
})
}
31 changes: 31 additions & 0 deletions internal/server/handlers/blob/blob_handler_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,34 @@ type DeleteResponse struct {
Deleted []string `json:"deleted"`
Errors []*BlobAPIError `json:"errors"`
}

// Multipart upload types
type MultipartUploadRequest struct {
Key string `json:"key" binding:"required"`
Parts int `json:"parts" binding:"required,min=1,max=10000"`
}

type MultipartUploadResponse struct {
Key string `json:"key"`
UploadID string `json:"uploadId"`
URLs []string `json:"urls"`
}

type CompleteUploadRequest struct {
Key string `json:"key" binding:"required"`
UploadID string `json:"uploadId" binding:"required"`
Parts []CompletedPartRequest `json:"parts" binding:"required,min=1"`
}

type CompletedPartRequest struct {
PartNumber int `json:"partNumber" binding:"required,min=1"`
ETag string `json:"etag" binding:"required"`
}

type CompleteUploadResponse struct {
Key string `json:"key"`
Version string `json:"version"`
ETag string `json:"etag"`
Size int64 `json:"size"`
LastModified string `json:"lastModified"`
}
Loading
Loading