diff --git a/internal/api/handlers.download.go b/internal/api/handlers.download.go new file mode 100644 index 0000000..f02e057 --- /dev/null +++ b/internal/api/handlers.download.go @@ -0,0 +1,73 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/go-chi/chi/v5" +) + +// handleGetDropMetadata returns info about the file (name, size, salt) +// The CLI needs this *before* it starts downloading to set up decryption. +func (s *Server) handleGetDropMetadata() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + dropID := chi.URLParam(r, "id") + + var resp GetDropMetadataResponse + var expiresAt time.Time + + // 1. Fetch metadata from Postgres + query := ` + SELECT file_name, file_size, encryption_salt, expires_at + FROM drops WHERE id = $1` + + err := s.DB.QueryRow(query, dropID).Scan( + &resp.FileName, &resp.FileSize, &resp.EncryptionSalt, &expiresAt, + ) + + if err != nil { + http.Error(w, "Drop not found", http.StatusNotFound) + return + } + + // 2. Check Expiry + if time.Now().After(expiresAt) { + http.Error(w, "Drop has expired", http.StatusGone) + return + } + + // 3. Get chunk count (to know how many pieces to expect) + err = s.DB.QueryRow("SELECT COUNT(*) FROM chunks WHERE drop_id = $1", dropID).Scan(&resp.ChunkCount) + if err != nil { + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + } +} + +// handleDownloadChunk retrieves a specific piece of binary data +func (s *Server) handleDownloadChunk() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + dropID := chi.URLParam(r, "id") + chunkIndex := chi.URLParam(r, "chunkIndex") // We'll add this param to router + + // 1. Construct S3 Key + key := fmt.Sprintf("drops/%s/%s", dropID, chunkIndex) + + // 2. Fetch from S3 + data, err := s.Store.DownloadChunk(key) + if err != nil { + http.Error(w, "Chunk not found or storage error", http.StatusNotFound) + return + } + + // 3. Stream binary back + w.Header().Set("Content-Type", "application/octet-stream") + w.Write(data) + } +} \ No newline at end of file diff --git a/internal/api/handler_upload.go b/internal/api/handlers_upload.go similarity index 100% rename from internal/api/handler_upload.go rename to internal/api/handlers_upload.go diff --git a/internal/api/models.go b/internal/api/models.go index e4389cc..35713ab 100644 --- a/internal/api/models.go +++ b/internal/api/models.go @@ -28,3 +28,11 @@ type ChunkUploadResponse struct { // 2. Server responds with CreateDropResponse (drop ID and expiration time) // 3. CLI uploads file chunks to /api/v1/drops/{drop_id}/chunks (POST) with ChunkUploadResponse confirming each chunk +// GetDropMetadataResponse is what the server sends back when the CLI requests metadata about a drop +type GetDropMetadataResponse struct { + FileName string `json:"file_name"` + FileSize int64 `json:"file_size"` + EncryptionSalt string `json:"encryption_salt"` + ChunkCount int `json:"chunk_count"` +} + diff --git a/internal/api/router.go b/internal/api/router.go index 0770d3e..90c0a60 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -52,5 +52,9 @@ func (s *Server) routes() { // Upload Endpoints r.Post("/drop", s.handleCreateDrop()) r.Post("/drop/{id}/chunk", s.handleUploadChunk()) + + // Download Endpoints + r.Get("/drop/{id}", s.handleGetDropMetadata()) + r.Get("/drop/{id}/chunk/{chunkIndex}", s.handleDownloadChunk()) }) } \ No newline at end of file