Skip to content

Commit

Permalink
feat(blob): implement GetUploadURL in service lib (#119)
Browse files Browse the repository at this point in the history
Because

the get-object-upload-url handler requires the foundational service
library,

This commit 

implements GetUploadURL.
  • Loading branch information
Yougigun authored Oct 18, 2024
1 parent 4107ad1 commit 931b1ca
Show file tree
Hide file tree
Showing 10 changed files with 358 additions and 20 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/frankban/quicktest v1.14.6
github.com/go-resty/resty/v2 v2.12.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/gogo/status v1.1.0
github.com/gojuno/minimock/v3 v3.3.6
github.com/golang-migrate/migrate/v4 v4.17.0
github.com/google/go-cmp v0.6.0
Expand Down Expand Up @@ -46,6 +47,7 @@ require (
github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect
github.com/getsentry/sentry-go v0.12.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.3 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,13 @@ github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0=
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gogo/status v1.1.0 h1:+eIkrewn5q6b30y+g/BJINVVdi2xH7je5MPJ3ZPK3JA=
github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM=
github.com/gojuno/minimock/v3 v3.3.6 h1:tZQQaDgKSxsKiVia9vt6zZ/qsKNGBw2D0ubHQPr+mHc=
github.com/gojuno/minimock/v3 v3.3.6/go.mod h1:kjvubEBVT8aUQ9e+g8x/hPfAhiOoqW7WinzzJgzr4ws=
Expand Down Expand Up @@ -342,8 +344,6 @@ github.com/influxdata/influxdb-client-go/v2 v2.12.3 h1:28nRlNMRIV4QbtIUvxhWqaxn0
github.com/influxdata/influxdb-client-go/v2 v2.12.3/go.mod h1:IrrLUbCjjfkmRuaCiGQg4m2GbkaeJDcuWoxiWdQEbA0=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
github.com/instill-ai/protogen-go v0.3.3-alpha.0.20241012090311-e872dc0b511d h1:jf2RQtRFNxnPMkjTD0AAqXDXO8lHYOrWU3Hrr+yGEzY=
github.com/instill-ai/protogen-go v0.3.3-alpha.0.20241012090311-e872dc0b511d/go.mod h1:rf0UY7VpEgpaLudYEcjx5rnbuwlBaaLyD4FQmWLtgAY=
github.com/instill-ai/protogen-go v0.3.3-alpha.0.20241018045010-dcc80f850d9d h1:6/5voyjeqeeeYszZ7XjifG2pekRDYdqWD0bgeKz9LHw=
github.com/instill-ai/protogen-go v0.3.3-alpha.0.20241018045010-dcc80f850d9d/go.mod h1:rf0UY7VpEgpaLudYEcjx5rnbuwlBaaLyD4FQmWLtgAY=
github.com/instill-ai/usage-client v0.3.0-alpha.0.20240319060111-4a3a39f2fd61 h1:smPTvmXDhn/QC7y/TPXyMTqbbRd0gvzmFgWBChwTfhE=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ BEGIN;
-- Create object table
CREATE TABLE IF NOT EXISTS object (
uid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(1040) NOT NULL,
size BIGINT NOT NULL,
content_type VARCHAR(255) NOT NULL,
name VARCHAR(1040),
size BIGINT ,
content_type VARCHAR(255),
namespace_uid UUID NOT NULL,
creator_uid UUID NOT NULL,
is_uploaded BOOLEAN NOT NULL DEFAULT FALSE,
Expand Down Expand Up @@ -36,7 +36,7 @@ CREATE TABLE IF NOT EXISTS object_url (
uid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
namespace_uid UUID NOT NULL,
object_uid UUID NOT NULL REFERENCES object(uid) ON DELETE CASCADE,
url_expire_at TIMESTAMP NOT NULL,
url_expire_at TIMESTAMP ,
minio_url_path TEXT NOT NULL,
encoded_url_path TEXT NOT NULL,
type VARCHAR(10) NOT NULL CHECK (type IN ('upload', 'download')),
Expand Down
1 change: 0 additions & 1 deletion pkg/minio/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package minio
// Port: "19000",
// RootUser: "minioadmin",
// RootPwd: "minioadmin",
// BucketName: "instill-ai-knowledge-bases",
// })
// if err != nil {
// log.Fatalf("Failed to initialize Minio client for testing: %v", err)
Expand Down
6 changes: 4 additions & 2 deletions pkg/minio/minio.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,18 @@ type MinioI interface {
GetFilesByPaths(ctx context.Context, bucket string, filePaths []string) ([]FileContent, error)
// KnowledgeBase
KnowledgeBaseI
// Object
ObjectI
}

type Minio struct {
client *minio.Client
client *minio.Client
}

const (
// Note: this bucket is for storing the blob of the file and not changeable
// in different environment so we dont put it in config
BlobBucketName = "instill-ai-blob"
BlobBucketName = "instill-ai-blob"
KnowledgeBaseBucketName = "instill-ai-knowledge-bases"
)

Expand Down
68 changes: 68 additions & 0 deletions pkg/minio/object.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package minio

import (
"context"
"errors"
"fmt"
"net/url"
"time"

"github.com/gofrs/uuid"
"github.com/instill-ai/artifact-backend/pkg/logger"
"go.uber.org/zap"
)

// ObjectI is the interface for object-related operations.
type ObjectI interface {
// MakePresignedURLForUpload creates a presigned URL for uploading an object.
MakePresignedURLForUpload(ctx context.Context, namespaceUUID uuid.UUID, objectUUID uuid.UUID, expiration time.Duration) (*url.URL, error)
// MakePresignedURLForDownload creates a presigned URL for downloading an object.
MakePresignedURLForDownload(ctx context.Context, namespaceUUID uuid.UUID, objectUUID uuid.UUID, expiration time.Duration) (*url.URL, error)
}

// MakePresignedURLForUpload creates a presigned URL for uploading an object.
func (m *Minio) MakePresignedURLForUpload(ctx context.Context, namespaceUUID uuid.UUID, objectUUID uuid.UUID, expiration time.Duration) (*url.URL, error) {
log, err := logger.GetZapLogger(ctx)
if err != nil {
return nil, err
}
// check if the expiration is within the range of 1sec to 7 days.
if expiration > time.Hour*24*7 {
return nil, errors.New("expiration time must be within 1sec to 7 days")
}
// Get presigned URL for uploading object
presignedURL, err := m.client.PresignedPutObject(BlobBucketName, GetBlobObjectPath(namespaceUUID, objectUUID), expiration)
if err != nil {
log.Error("Failed to make presigned URL for upload", zap.Error(err))
return nil, err
}

return presignedURL, nil
}

// MakePresignedURLForDownload creates a presigned URL for downloading an object.
func (m *Minio) MakePresignedURLForDownload(ctx context.Context, namespaceUUID uuid.UUID, objectUUID uuid.UUID, expiration time.Duration) (*url.URL, error) {
log, err := logger.GetZapLogger(ctx)
if err != nil {
return nil, err
}
// check if the expiration is within the range of 1sec to 7 days.
if expiration > time.Hour*24*7 {
return nil, errors.New("expiration time must be within 1sec to 7 days")
}

// Get presigned URL for downloading object
presignedURL, err := m.client.PresignedGetObject(BlobBucketName, GetBlobObjectPath(namespaceUUID, objectUUID), expiration, url.Values{})
if err != nil {
log.Error("Failed to make presigned URL for download", zap.Error(err))
return nil, err
}

return presignedURL, nil
}

// make object path from objectUUID.
// namespaceUUID / objectUUID
func GetBlobObjectPath(namespaceUUID uuid.UUID, objectUUID uuid.UUID) string {
return fmt.Sprintf("ns-%s/obj-%s", namespaceUUID.String(), objectUUID.String())
}
143 changes: 143 additions & 0 deletions pkg/minio/object_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package minio

// import (
// "context"
// "fmt"
// "io"
// "log"
// "net/http"
// "strings"
// "testing"
// "time"

// "github.com/gofrs/uuid"
// "github.com/instill-ai/artifact-backend/config"
// "github.com/minio/minio-go"
// "github.com/minio/minio-go/pkg/encrypt"
// )

// // Test presigned URL for upload
// func TestMinio_TestMakePresignedURLForUpload(t *testing.T) {
// log.Println("Setting up Minio client for testing")
// var err error
// testMinioClient, err := NewMinioClientAndInitBucket(config.MinioConfig{
// Host: "localhost",
// Port: "19000",
// RootUser: "minioadmin",
// RootPwd: "minioadmin",
// })
// if err != nil {
// log.Fatalf("Failed to initialize Minio client for testing: %v", err)
// }
// // create a namespaceUUID and objectUUID
// namespaceUUID, err := uuid.NewV4()
// if err != nil {
// t.Fatalf("failed to create namespaceUUID: %v", err)
// }
// objectUUID, err := uuid.NewV4()
// if err != nil {
// t.Fatalf("failed to create objectUUID: %v", err)
// }
// presignedURL, err := testMinioClient.MakePresignedURLForUpload(context.TODO(), namespaceUUID, objectUUID, 1*time.Hour)
// if err != nil {
// t.Fatalf("failed to make presigned URL for upload: %v", err)
// }
// // just get path and query from presignedURL
// // parsedURL, err := url.Parse(presignedURL)
// // if err != nil {
// // t.Fatalf("failed to parse presigned URL: %v", err)
// // }
// // fmt.Println("presignedURL: ", parsedURL.Path, parsedURL.Query())

// // upload a test file with content "test" to the presigned URL using http client
// client := &http.Client{}
// req, err := http.NewRequest("PUT", presignedURL.String(), strings.NewReader("test"))
// if err != nil {
// t.Fatalf("failed to create request: %v", err)
// }
// req.Header.Set("Content-Type", "text/plain")
// resp, err := client.Do(req)
// if err != nil {
// t.Fatalf("failed to upload file: %v", err)
// }
// fmt.Println("resp code: ", resp.StatusCode)
// // resp range in header
// fmt.Println("resp range: ", resp.Header.Get("Content-Range"))
// // read the body 100 bytes at a time until EOF
// buf := make([]byte, 100)
// for {
// n, err := resp.Body.Read(buf)
// if err != nil && err != io.EOF {
// t.Fatalf("failed to read response body: %v", err)
// }
// fmt.Println("read: ", string(buf[:n]))
// if err == io.EOF {
// break
// }
// }

// // check if the file is uploaded to the Minio bucket
// objectInfo, err := testMinioClient.client.StatObject(BlobBucketName, GetBlobObjectPath(namespaceUUID, objectUUID), minio.StatObjectOptions{})
// if err != nil {
// t.Fatalf("failed to stat object: %v", err)
// }
// // list objectsInfo field by field
// // print name of object
// fmt.Println("ObjectInfo:")
// fmt.Printf(" Key: %v\n", objectInfo.Key)
// fmt.Printf(" Size: %v\n", objectInfo.Size)
// fmt.Printf(" ContentType: %v\n", objectInfo.ContentType)
// // fmt.Printf("Metadata: %v\n", objectInfo.Metadata)
// // fmt.Printf("Owner: %v\n", objectInfo.Owner)
// // fmt.Printf("StorageClass: %v\n", objectInfo.StorageClass)

// // check if the content is "test"
// data, err := testMinioClient.GetFile(context.TODO(), BlobBucketName, GetBlobObjectPath(namespaceUUID, objectUUID))
// if err != nil {
// t.Fatalf("failed to get file: %v", err)
// }
// fmt.Println("data: ", string(data))

// // delete the test file from the Minio bucket
// err = testMinioClient.client.RemoveObject(BlobBucketName, GetBlobObjectPath(namespaceUUID, objectUUID))
// if err != nil {
// t.Fatalf("failed to delete object: %v", err)
// }
// }

// // TestMinio_TestMakePresignedURLForDownload
// func TestMinio_TestMakePresignedURLForDownload(t *testing.T) {
// }

// // TestMinio_TestMultiPartUpload
// func TestMinio_TestMultiPartUpload(t *testing.T) {
// log.Println("Setting up Minio client for testing")
// var err error
// testMinioClient, err := NewMinioClientAndInitBucket(config.MinioConfig{
// Host: "localhost",
// Port: "19000",
// RootUser: "minioadmin",
// RootPwd: "minioadmin",
// })
// if err != nil {
// log.Fatalf("Failed to initialize Minio client for testing: %v", err)
// }
// client := testMinioClient.GetClient()
// namespaceUUID, err := uuid.NewV4()
// if err != nil {
// t.Fatalf("failed to create namespaceUUID: %v", err)
// }
// objectUUID, err := uuid.NewV4()
// if err != nil {
// t.Fatalf("failed to create objectUUID: %v", err)
// }
// dest, err := minio.NewDestinationInfo(BlobBucketName, GetBlobObjectPath(namespaceUUID, objectUUID), encrypt.NewSSE(), nil)
// if err != nil {
// t.Fatalf("failed to create destination info: %v", err)
// }
// sources := []minio.SourceInfo{
// minio.NewSourceInfo(BlobBucketName, GetBlobObjectPath(namespaceUUID, objectUUID), nil),
// }
// client.ComposeObjectWithProgress(dest, sources, nil)

// }
12 changes: 6 additions & 6 deletions pkg/repository/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ type ObjectI interface {

type Object struct {
UID uuid.UUID `gorm:"column:uid;type:uuid;default:gen_random_uuid();primaryKey" json:"uid"`
Name string `gorm:"column:name;size:1040;not null" json:"name"`
Size int64 `gorm:"column:size;not null" json:"size"`
ContentType string `gorm:"column:content_type;size:255;not null" json:"content_type"`
NamespaceUID uuid.UUID `gorm:"column:namespace_uid;type:uuid;not null" json:"namespace_uid"`
CreatorUID uuid.UUID `gorm:"column:creator_uid;type:uuid;not null" json:"creator_uid"`
IsUploaded bool `gorm:"column:is_uploaded;not null;default:false" json:"is_uploaded"`
Name string `gorm:"column:name;size:1040" json:"name"`
Size int64 `gorm:"column:size;" json:"size"`
ContentType string `gorm:"column:content_type;size:255" json:"content_type"`
NamespaceUID uuid.UUID `gorm:"column:namespace_uid;type:uuid;not null" json:"namespace_uid"`
CreatorUID uuid.UUID `gorm:"column:creator_uid;type:uuid;not null" json:"creator_uid"`
IsUploaded bool `gorm:"column:is_uploaded;not null;default:false" json:"is_uploaded"`
// BucketName/ns:<nid>/obj:<uid>
Destination string `gorm:"column:destination;size:255" json:"destination"`
ObjectExpireDays *int `gorm:"column:object_expire_days" json:"object_expire_days"`
Expand Down
10 changes: 5 additions & 5 deletions pkg/repository/objectUrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type ObjectURL struct {
UID uuid.UUID `gorm:"column:uid;type:uuid;default:gen_random_uuid();primaryKey" json:"uid"`
NamespaceUID uuid.UUID `gorm:"column:namespace_uid;type:uuid;not null" json:"namespace_uid"`
ObjectUID uuid.UUID `gorm:"column:object_uid;type:uuid;not null" json:"object_uid"`
URLExpireAt time.Time `gorm:"column:url_expire_at;not null" json:"url_expire_at"`
URLExpireAt time.Time `gorm:"column:url_expire_at" json:"url_expire_at"`
MinioURLPath string `gorm:"column:minio_url_path;type:text;not null" json:"minio_url_path"`
EncodedURLPath string `gorm:"column:encoded_url_path;type:text;not null" json:"encoded_url_path"`
// download or upload
Expand Down Expand Up @@ -68,8 +68,8 @@ var ObjectURLColumn = ObjectURLColumns{
}

const (
objectURLTypeDownload = "download"
objectURLTypeUpload = "upload"
ObjectURLTypeDownload = "download"
ObjectURLTypeUpload = "upload"
)


Expand Down Expand Up @@ -151,7 +151,7 @@ func (r *Repository) GetObjectURLCountByObject(ctx context.Context, objectUID uu
func (r *Repository) GetObjectUploadURL(ctx context.Context, objectUID uuid.UUID) (*ObjectURL, error) {
var objectURL ObjectURL
whereString := fmt.Sprintf("%v = ? AND %v = ? AND %v IS NULL", ObjectURLColumn.ObjectUID, ObjectURLColumn.Type, ObjectURLColumn.DeleteTime)
if err := r.db.WithContext(ctx).Where(whereString, objectUID, objectURLTypeUpload).First(&objectURL).Error; err != nil {
if err := r.db.WithContext(ctx).Where(whereString, objectUID, ObjectURLTypeUpload).First(&objectURL).Error; err != nil {
return nil, err
}
return &objectURL, nil
Expand All @@ -161,7 +161,7 @@ func (r *Repository) GetObjectUploadURL(ctx context.Context, objectUID uuid.UUID
func (r *Repository) GetObjectDownloadURL(ctx context.Context, objectUID uuid.UUID) (*ObjectURL, error) {
var objectURL ObjectURL
whereString := fmt.Sprintf("%v = ? AND %v = ? AND %v IS NULL", ObjectURLColumn.ObjectUID, ObjectURLColumn.Type, ObjectURLColumn.DeleteTime)
if err := r.db.WithContext(ctx).Where(whereString, objectUID, objectURLTypeDownload).First(&objectURL).Error; err != nil {
if err := r.db.WithContext(ctx).Where(whereString, objectUID, ObjectURLTypeDownload).First(&objectURL).Error; err != nil {
return nil, err
}
return &objectURL, nil
Expand Down
Loading

0 comments on commit 931b1ca

Please sign in to comment.