diff --git a/internal/app/service.go b/internal/app/service.go index c7ea14a..e00efe0 100644 --- a/internal/app/service.go +++ b/internal/app/service.go @@ -39,6 +39,7 @@ type Config struct { type AssetURLSigner interface { SignedGetObjectURL(ctx context.Context, objectKey string, expiry time.Duration) (string, error) PutObject(ctx context.Context, objectKey, contentType string, body io.Reader, size int64) error + GetObject(ctx context.Context, objectKey string) (io.ReadCloser, string, int64, error) } type Service struct { @@ -551,3 +552,17 @@ func (s *Service) SignedDownloadURL(requesterKey, assetID string) (string, error } return url, nil } + +func (s *Service) OpenAssetDownload(requesterKey, assetID string) (io.ReadCloser, string, int64, error) { + if s.assetSigner == nil { + return nil, "", 0, ErrStorageNotConfigured + } + asset, err := s.store.GetAsset(assetID) + if err != nil { + return nil, "", 0, ErrAssetMiss + } + if asset.OwnerKey != requesterKey && !asset.IsPublic { + return nil, "", 0, ErrForbidden + } + return s.assetSigner.GetObject(context.Background(), asset.ObjectKey) +} diff --git a/internal/http/api_test.go b/internal/http/api_test.go index 222e8bd..4c527f9 100644 --- a/internal/http/api_test.go +++ b/internal/http/api_test.go @@ -41,6 +41,11 @@ func (fakeSigner) PutObject(_ context.Context, _ string, _ string, _ io.Reader, return nil } +func (fakeSigner) GetObject(_ context.Context, objectKey string) (io.ReadCloser, string, int64, error) { + payload := []byte("fake-download:" + objectKey) + return io.NopCloser(bytes.NewReader(payload)), "application/octet-stream", int64(len(payload)), nil +} + func mustJSON(t *testing.T, value interface{}) []byte { t.Helper() b, err := json.Marshal(value) @@ -314,7 +319,6 @@ func TestAssetLifecycleAndVisibility(t *testing.T) { server := newTestServer(adminPubB64) defer server.Close() client := server.Client() - client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse } adminToken := loginUser(t, client, server.URL, adminPubB64, adminPriv) @@ -418,12 +422,17 @@ func TestAssetLifecycleAndVisibility(t *testing.T) { if err != nil { t.Fatalf("download public request failed: %v", err) } - if downloadPublicResp.StatusCode != http.StatusFound { - t.Fatalf("expected public asset redirect status, got %d", downloadPublicResp.StatusCode) + defer downloadPublicResp.Body.Close() + if downloadPublicResp.StatusCode != http.StatusOK { + t.Fatalf("expected public asset stream status, got %d", downloadPublicResp.StatusCode) } - expectedLocation := fmt.Sprintf("http://files.local/download/%s/%s.%s", user1PubB64, "abcdef1234", "glb") - if downloadPublicResp.Header.Get("Location") != expectedLocation { - t.Fatalf("unexpected redirect location: %s", downloadPublicResp.Header.Get("Location")) + body, err := io.ReadAll(downloadPublicResp.Body) + if err != nil { + t.Fatalf("read public download body: %v", err) + } + expectedBody := fmt.Sprintf("fake-download:%s/%s.%s", user1PubB64, "abcdef1234", "glb") + if string(body) != expectedBody { + t.Fatalf("unexpected download body: %q", string(body)) } patchResp, patchData := patchJSON(t, client, server.URL+"/v1/assets/"+assetID, map[string]interface{}{ diff --git a/internal/http/handlers.go b/internal/http/handlers.go index 9b78b59..2546195 100644 --- a/internal/http/handlers.go +++ b/internal/http/handlers.go @@ -3,6 +3,8 @@ package httpapi import ( "encoding/json" "errors" + "fmt" + "io" "net/http" "strings" "time" @@ -492,10 +494,20 @@ func (a *API) downloadAsset(w http.ResponseWriter, r *http.Request) { return } assetID := r.PathValue("id") - url, err := a.service.SignedDownloadURL(user, assetID) + reader, contentType, size, err := a.service.OpenAssetDownload(user, assetID) if err != nil { writeErr(w, err) return } - http.Redirect(w, r, url, http.StatusFound) + defer reader.Close() + if contentType != "" { + w.Header().Set("Content-Type", contentType) + } + if size >= 0 { + w.Header().Set("Content-Length", fmt.Sprintf("%d", size)) + } + if _, err := io.Copy(w, reader); err != nil { + // Response stream may be interrupted by client disconnects; ignore write errors here. + return + } } diff --git a/internal/storage/s3_signer.go b/internal/storage/s3_signer.go index 5759cbf..5ed4ba5 100644 --- a/internal/storage/s3_signer.go +++ b/internal/storage/s3_signer.go @@ -62,3 +62,20 @@ func (s *S3Signer) PutObject(ctx context.Context, objectKey, contentType string, }) return err } + +func (s *S3Signer) GetObject(ctx context.Context, objectKey string) (io.ReadCloser, string, int64, error) { + obj, err := s.client.GetObject(ctx, s.bucket, objectKey, minio.GetObjectOptions{}) + if err != nil { + return nil, "", 0, err + } + info, err := obj.Stat() + if err != nil { + _ = obj.Close() + return nil, "", 0, err + } + contentType := info.ContentType + if contentType == "" { + contentType = "application/octet-stream" + } + return obj, contentType, info.Size, nil +}