Skip to content

Commit

Permalink
Add new methods to artifact interface: Read, write, parse url (#92)
Browse files Browse the repository at this point in the history
* Add new methods to artifact interface: Read, write, parse url

* Fix typo

* Address review
  • Loading branch information
Arief Rahmansyah authored Dec 8, 2023
1 parent b3d74d1 commit 1755960
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 20 deletions.
127 changes: 108 additions & 19 deletions api/pkg/artifact/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,121 @@ package artifact

import (
"context"
"fmt"
"io"
"net/url"
"strings"

"cloud.google.com/go/storage"
"google.golang.org/api/iterator"
)

// URL contains the information needed to identify the location of an object
// located in Google Cloud Storage.
type URL struct {
// Bucket is the name of the Google Cloud Storage bucket where the object
// is located.
Bucket string

// Object is the name and or path of the object stored in the bucket. It
// should not start with a forward slash.
Object string
}

type Service interface {
ParseURL(gsURL string) (*URL, error)

ReadArtifact(ctx context.Context, url string) ([]byte, error)
WriteArtifact(ctx context.Context, url string, content []byte) error
DeleteArtifact(ctx context.Context, url string) error
}

type GcsArtifactClient struct {
API *storage.Client
}

func NewGcsArtifactClient(api *storage.Client) Service {
return &GcsArtifactClient{
API: api,
}
}

// Parse parses a Google Cloud Storage string into a URL struct. The expected
// format of the string is gs://[bucket-name]/[object-path]. If the provided
// URL is formatted incorrectly an error will be returned.
func (gac *GcsArtifactClient) ParseURL(gsURL string) (*URL, error) {
u, err := url.Parse(gsURL)
if err != nil {
return nil, err
}
if u.Scheme != "gs" {
return nil, err
}

bucket, object := u.Host, strings.TrimLeft(u.Path, "/")

if bucket == "" {
return nil, err
}

if object == "" {
return nil, err
}

return &URL{
Bucket: bucket,
Object: object,
}, nil
}

func (gac *GcsArtifactClient) ReadArtifact(ctx context.Context, url string) ([]byte, error) {
u, err := gac.ParseURL(url)
if err != nil {
return nil, err
}

reader, err := gac.API.Bucket(u.Bucket).Object(u.Object).NewReader(ctx)
if err != nil {
return nil, err
}
defer reader.Close() //nolint:errcheck

bytes, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return bytes, nil
}

func (gac *GcsArtifactClient) WriteArtifact(ctx context.Context, url string, content []byte) error {
u, err := gac.ParseURL(url)
if err != nil {
return err
}
w := gac.API.Bucket(u.Bucket).Object(u.Object).NewWriter(ctx)

if _, err := fmt.Fprintf(w, "%s", content); err != nil {
return err
}

if err := w.Close(); err != nil {
return err
}

return nil
}

func (gac *GcsArtifactClient) DeleteArtifact(ctx context.Context, url string) error {
// Get bucket name and gcsPrefix
// the [5:] is to remove the "gs://" on the artifact uri
// ex : gs://bucketName/path → bucketName/path
gcsBucket, gcsLocation := gac.getGcsBucketAndLocation(url[5:])
u, err := gac.ParseURL(url)
if err != nil {
return err
}

// Sets the name for the bucket.
bucket := gac.API.Bucket(gcsBucket)
bucket := gac.API.Bucket(u.Bucket)

it := bucket.Objects(ctx, &storage.Query{
Prefix: gcsLocation,
Prefix: u.Object,
})
for {
attrs, err := it.Next()
Expand All @@ -43,25 +133,24 @@ func (gac *GcsArtifactClient) DeleteArtifact(ctx context.Context, url string) er
return nil
}

func (gac *GcsArtifactClient) getGcsBucketAndLocation(str string) (string, string) {
// Split string using delimiter
// ex : bucketName/path/path1/item → (bucketName , path/path1/item)
splitStr := strings.SplitN(str, "/", 2)
return splitStr[0], splitStr[1]
type NopArtifactClient struct{}

func NewNopArtifactClient() Service {
return &NopArtifactClient{}
}

func NewGcsArtifactClient(api *storage.Client) Service {
return &GcsArtifactClient{
API: api,
}
func (nac *NopArtifactClient) ParseURL(gsURL string) (*URL, error) {
return nil, nil
}

type NopArtifactClient struct{}
func (nac *NopArtifactClient) ReadArtifact(ctx context.Context, url string) ([]byte, error) {
return nil, nil
}

func (nac *NopArtifactClient) DeleteArtifact(ctx context.Context, url string) error {
func (nac *NopArtifactClient) WriteArtifact(ctx context.Context, url string, content []byte) error {
return nil
}

func NewNopArtifactClient() Service {
return &NopArtifactClient{}
func (nac *NopArtifactClient) DeleteArtifact(ctx context.Context, url string) error {
return nil
}
68 changes: 68 additions & 0 deletions api/pkg/artifact/artifact_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package artifact

import (
"reflect"
"testing"

"cloud.google.com/go/storage"
)

func TestGcsArtifactClient_ParseURL(t *testing.T) {
type fields struct {
API *storage.Client
}
type args struct {
gsURL string
}
tests := []struct {
name string
fields fields
args args
want *URL
wantErr bool
}{
{
name: "valid short url",
fields: fields{
API: nil,
},
args: args{
gsURL: "gs://bucket-name/object-path",
},
want: &URL{
Bucket: "bucket-name",
Object: "object-path",
},
wantErr: false,
},
{
name: "valid url",
fields: fields{
API: nil,
},
args: args{
gsURL: "gs://bucket-name/object-path/object-path-2/object-path-3/file-1.txt",
},
want: &URL{
Bucket: "bucket-name",
Object: "object-path/object-path-2/object-path-3/file-1.txt",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gac := &GcsArtifactClient{
API: tt.fields.API,
}
got, err := gac.ParseURL(tt.args.gsURL)
if (err != nil) != tt.wantErr {
t.Errorf("GcsArtifactClient.ParseURL() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GcsArtifactClient.ParseURL() = %v, want %v", got, tt.want)
}
})
}
}
70 changes: 69 additions & 1 deletion api/pkg/artifact/mocks/artifact.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 1755960

Please sign in to comment.