Implement backend upload/download API enhancements

- Add S3 multipart upload support for files > 100MB
- Add resumable upload API endpoints (init, upload part, complete, abort, status)
- Add HTTP range request support for partial downloads
- Add HEAD request endpoint for artifact metadata
- Add format-specific metadata extraction (deb, rpm, tar.gz, wheel, jar, zip)
- Add format_metadata column to artifacts table
- Add database migration for schema updates
- Add deduplication indicator in upload response
- Set Accept-Ranges header on downloads
- Return Content-Length header on all downloads
This commit is contained in:
Mondo Diaz
2025-12-11 17:07:10 -06:00
parent cb3d62b02a
commit 6eb2f9db7b
6 changed files with 1118 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Optional, List
from typing import Optional, List, Dict, Any
from pydantic import BaseModel
from uuid import UUID
@@ -51,6 +51,7 @@ class ArtifactResponse(BaseModel):
created_at: datetime
created_by: str
ref_count: int
format_metadata: Optional[Dict[str, Any]] = None
class Config:
from_attributes = True
@@ -81,6 +82,53 @@ class UploadResponse(BaseModel):
project: str
package: str
tag: Optional[str]
format_metadata: Optional[Dict[str, Any]] = None
deduplicated: bool = False
# Resumable upload schemas
class ResumableUploadInitRequest(BaseModel):
"""Request to initiate a resumable upload"""
expected_hash: str # SHA256 hash of the file (client must compute)
filename: str
content_type: Optional[str] = None
size: int
tag: Optional[str] = None
class ResumableUploadInitResponse(BaseModel):
"""Response from initiating a resumable upload"""
upload_id: Optional[str] # None if file already exists
already_exists: bool
artifact_id: Optional[str] = None # Set if already_exists is True
chunk_size: int # Recommended chunk size for parts
class ResumableUploadPartResponse(BaseModel):
"""Response from uploading a part"""
part_number: int
etag: str
class ResumableUploadCompleteRequest(BaseModel):
"""Request to complete a resumable upload"""
tag: Optional[str] = None
class ResumableUploadCompleteResponse(BaseModel):
"""Response from completing a resumable upload"""
artifact_id: str
size: int
project: str
package: str
tag: Optional[str]
class ResumableUploadStatusResponse(BaseModel):
"""Status of a resumable upload"""
upload_id: str
uploaded_parts: List[int]
total_uploaded_bytes: int
# Consumer schemas