feat: Implement direct upload functionality for desktop client releases
Desktop Client Build / Resolve Build Metadata (push) Successful in 52s
Frontend CI / Frontend (push) Successful in 3m49s
Backend CI / Backend (push) Successful in 16m10s
Desktop Client Build / Build Desktop Client (push) Successful in 23m22s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 31s
Desktop Client Build / Resolve Build Metadata (push) Successful in 52s
Frontend CI / Frontend (push) Successful in 3m49s
Backend CI / Backend (push) Successful in 16m10s
Desktop Client Build / Build Desktop Client (push) Successful in 23m22s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 31s
- Added new endpoints for initiating and completing direct uploads of desktop client packages. - Introduced request and response structures for direct upload operations. - Enhanced the upload process to support SHA256 and Content-MD5 validation. - Updated the frontend to reflect changes in upload button states and progress indicators. - Modified object storage clients to support presigned PUT URLs with content type and MD5 headers. - Added tests for direct upload functionality and ensured existing upload processes remain intact.
This commit is contained in:
@@ -7,11 +7,13 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/ops/domain"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/objectstorage"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -311,11 +313,92 @@ func TestDesktopClientReleaseChunkedUploadCompletesOutOfOrder(t *testing.T) {
|
||||
require.True(t, errors.Is(err, os.ErrNotExist))
|
||||
}
|
||||
|
||||
func TestDesktopClientReleaseDirectUploadUsesPresignedPutURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
content := []byte("release-package")
|
||||
sum := sha256.Sum256(content)
|
||||
wantSHA := hex.EncodeToString(sum[:])
|
||||
storage := &desktopReleaseDownloadURLStorage{
|
||||
existsByKey: map[string]bool{},
|
||||
objects: map[string][]byte{},
|
||||
objectSizes: map[string]int64{},
|
||||
}
|
||||
svc := NewDesktopClientReleaseService(nil, storage, nil)
|
||||
|
||||
initResult, err := svc.InitiateDirectUpload(context.Background(), DesktopClientReleaseDirectUploadInitInput{
|
||||
Platform: "darwin",
|
||||
Arch: "arm64",
|
||||
Channel: "stable",
|
||||
Version: "1.2.3",
|
||||
Kind: DesktopClientAssetKindInstaller,
|
||||
FileName: "client.dmg",
|
||||
FileSizeBytes: int64(len(content)),
|
||||
SHA256: wantSHA,
|
||||
ContentMD5: "pt74NOtqjIwBJtPUYvJ8AA==",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, initResult.ObjectExists)
|
||||
require.Equal(t, http.MethodPut, initResult.Method)
|
||||
require.Equal(t, "desktop-client/releases/blobs/"+wantSHA+".dmg", initResult.OSSObjectKey)
|
||||
require.Equal(t, "https://oss.example.test/upload/"+initResult.OSSObjectKey, initResult.UploadURL)
|
||||
require.Equal(t, "application/x-apple-diskimage", initResult.Headers["Content-Type"])
|
||||
require.Equal(t, "pt74NOtqjIwBJtPUYvJ8AA==", initResult.Headers["Content-Md5"])
|
||||
require.Len(t, storage.presignedPutCalls, 1)
|
||||
require.Equal(t, initResult.OSSObjectKey, storage.presignedPutCalls[0])
|
||||
|
||||
storage.existsByKey[initResult.OSSObjectKey] = true
|
||||
storage.objectSizes[initResult.OSSObjectKey] = int64(len(content))
|
||||
completeResult, err := svc.CompleteDirectUpload(context.Background(), DesktopClientReleaseDirectUploadCompleteInput{
|
||||
Platform: "darwin",
|
||||
Arch: "arm64",
|
||||
Channel: "stable",
|
||||
Version: "1.2.3",
|
||||
Kind: DesktopClientAssetKindInstaller,
|
||||
OSSObjectKey: initResult.OSSObjectKey,
|
||||
FileName: initResult.FileName,
|
||||
FileSizeBytes: initResult.FileSizeBytes,
|
||||
SHA256: initResult.SHA256,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, initResult.OSSObjectKey, completeResult.OSSObjectKey)
|
||||
require.Equal(t, wantSHA, completeResult.SHA256)
|
||||
}
|
||||
|
||||
func TestDesktopClientReleaseDirectUploadRejectsSizeMismatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sum := sha256.Sum256([]byte("release-package"))
|
||||
sha256Hex := hex.EncodeToString(sum[:])
|
||||
objectKey := buildDesktopClientReleaseObjectKey(sha256Hex, "client.zip")
|
||||
storage := &desktopReleaseDownloadURLStorage{
|
||||
existsByKey: map[string]bool{objectKey: true},
|
||||
objectSizes: map[string]int64{objectKey: 10},
|
||||
}
|
||||
svc := NewDesktopClientReleaseService(nil, storage, nil)
|
||||
|
||||
_, err := svc.CompleteDirectUpload(context.Background(), DesktopClientReleaseDirectUploadCompleteInput{
|
||||
Platform: "darwin",
|
||||
Arch: "arm64",
|
||||
Channel: "stable",
|
||||
Version: "1.2.3",
|
||||
Kind: DesktopClientAssetKindUpdater,
|
||||
OSSObjectKey: objectKey,
|
||||
FileName: "client.zip",
|
||||
FileSizeBytes: 20,
|
||||
SHA256: sha256Hex,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "direct_upload_size_mismatch")
|
||||
}
|
||||
|
||||
type desktopReleaseDownloadURLStorage struct {
|
||||
publicURLCalls []string
|
||||
downloadURLCalls []string
|
||||
existsByKey map[string]bool
|
||||
objects map[string][]byte
|
||||
publicURLCalls []string
|
||||
downloadURLCalls []string
|
||||
existsByKey map[string]bool
|
||||
objects map[string][]byte
|
||||
objectSizes map[string]int64
|
||||
presignedPutCalls []string
|
||||
}
|
||||
|
||||
func (s *desktopReleaseDownloadURLStorage) Validate() error {
|
||||
@@ -335,6 +418,10 @@ func (s *desktopReleaseDownloadURLStorage) PutReader(_ context.Context, objectKe
|
||||
return err
|
||||
}
|
||||
s.objects[objectKey] = content
|
||||
if s.objectSizes == nil {
|
||||
s.objectSizes = map[string]int64{}
|
||||
}
|
||||
s.objectSizes[objectKey] = int64(len(content))
|
||||
if s.existsByKey == nil {
|
||||
s.existsByKey = map[string]bool{}
|
||||
}
|
||||
@@ -353,6 +440,26 @@ func (s *desktopReleaseDownloadURLStorage) Exists(_ context.Context, objectKey s
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *desktopReleaseDownloadURLStorage) PresignedPutURL(
|
||||
_ context.Context,
|
||||
objectKey string,
|
||||
contentType string,
|
||||
contentMD5Base64 string,
|
||||
ttl time.Duration,
|
||||
) (*objectstorage.PresignedPutResult, error) {
|
||||
s.presignedPutCalls = append(s.presignedPutCalls, objectKey)
|
||||
headers := http.Header{}
|
||||
headers.Set("Content-Type", contentType)
|
||||
if contentMD5Base64 != "" {
|
||||
headers.Set("Content-MD5", contentMD5Base64)
|
||||
}
|
||||
return &objectstorage.PresignedPutResult{
|
||||
URL: "https://oss.example.test/upload/" + objectKey,
|
||||
Headers: headers,
|
||||
ExpiresAt: time.Now().UTC().Add(ttl),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *desktopReleaseDownloadURLStorage) Delete(context.Context, string) error {
|
||||
return nil
|
||||
}
|
||||
@@ -366,3 +473,25 @@ func (s *desktopReleaseDownloadURLStorage) DownloadURL(objectKey string) (string
|
||||
s.downloadURLCalls = append(s.downloadURLCalls, objectKey)
|
||||
return "https://oss.example.test/signed/client.zip?Expires=123", nil
|
||||
}
|
||||
|
||||
func (s *desktopReleaseDownloadURLStorage) List(context.Context, objectstorage.ListInput) (*objectstorage.ListResult, error) {
|
||||
return &objectstorage.ListResult{}, nil
|
||||
}
|
||||
|
||||
func (s *desktopReleaseDownloadURLStorage) Stat(_ context.Context, objectKey string) (*objectstorage.ObjectInfo, error) {
|
||||
exists, _ := s.Exists(context.Background(), objectKey)
|
||||
if !exists {
|
||||
return nil, objectstorage.ErrObjectNotFound
|
||||
}
|
||||
if s.objectSizes != nil {
|
||||
return &objectstorage.ObjectInfo{Key: objectKey, Size: s.objectSizes[objectKey], LastModified: time.Now()}, nil
|
||||
}
|
||||
if s.objects != nil {
|
||||
return &objectstorage.ObjectInfo{Key: objectKey, Size: int64(len(s.objects[objectKey])), LastModified: time.Now()}, nil
|
||||
}
|
||||
return &objectstorage.ObjectInfo{Key: objectKey, LastModified: time.Now()}, nil
|
||||
}
|
||||
|
||||
func (s *desktopReleaseDownloadURLStorage) Copy(context.Context, string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user