Skip to content

Commit 1755960

Browse files
author
Arief Rahmansyah
authored
Add new methods to artifact interface: Read, write, parse url (#92)
* Add new methods to artifact interface: Read, write, parse url * Fix typo * Address review
1 parent b3d74d1 commit 1755960

3 files changed

Lines changed: 245 additions & 20 deletions

File tree

api/pkg/artifact/artifact.go

Lines changed: 108 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,121 @@ package artifact
22

33
import (
44
"context"
5+
"fmt"
6+
"io"
7+
"net/url"
58
"strings"
69

710
"cloud.google.com/go/storage"
811
"google.golang.org/api/iterator"
912
)
1013

14+
// URL contains the information needed to identify the location of an object
15+
// located in Google Cloud Storage.
16+
type URL struct {
17+
// Bucket is the name of the Google Cloud Storage bucket where the object
18+
// is located.
19+
Bucket string
20+
21+
// Object is the name and or path of the object stored in the bucket. It
22+
// should not start with a forward slash.
23+
Object string
24+
}
25+
1126
type Service interface {
27+
ParseURL(gsURL string) (*URL, error)
28+
29+
ReadArtifact(ctx context.Context, url string) ([]byte, error)
30+
WriteArtifact(ctx context.Context, url string, content []byte) error
1231
DeleteArtifact(ctx context.Context, url string) error
1332
}
1433

1534
type GcsArtifactClient struct {
1635
API *storage.Client
1736
}
1837

38+
func NewGcsArtifactClient(api *storage.Client) Service {
39+
return &GcsArtifactClient{
40+
API: api,
41+
}
42+
}
43+
44+
// Parse parses a Google Cloud Storage string into a URL struct. The expected
45+
// format of the string is gs://[bucket-name]/[object-path]. If the provided
46+
// URL is formatted incorrectly an error will be returned.
47+
func (gac *GcsArtifactClient) ParseURL(gsURL string) (*URL, error) {
48+
u, err := url.Parse(gsURL)
49+
if err != nil {
50+
return nil, err
51+
}
52+
if u.Scheme != "gs" {
53+
return nil, err
54+
}
55+
56+
bucket, object := u.Host, strings.TrimLeft(u.Path, "/")
57+
58+
if bucket == "" {
59+
return nil, err
60+
}
61+
62+
if object == "" {
63+
return nil, err
64+
}
65+
66+
return &URL{
67+
Bucket: bucket,
68+
Object: object,
69+
}, nil
70+
}
71+
72+
func (gac *GcsArtifactClient) ReadArtifact(ctx context.Context, url string) ([]byte, error) {
73+
u, err := gac.ParseURL(url)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
reader, err := gac.API.Bucket(u.Bucket).Object(u.Object).NewReader(ctx)
79+
if err != nil {
80+
return nil, err
81+
}
82+
defer reader.Close() //nolint:errcheck
83+
84+
bytes, err := io.ReadAll(reader)
85+
if err != nil {
86+
return nil, err
87+
}
88+
return bytes, nil
89+
}
90+
91+
func (gac *GcsArtifactClient) WriteArtifact(ctx context.Context, url string, content []byte) error {
92+
u, err := gac.ParseURL(url)
93+
if err != nil {
94+
return err
95+
}
96+
w := gac.API.Bucket(u.Bucket).Object(u.Object).NewWriter(ctx)
97+
98+
if _, err := fmt.Fprintf(w, "%s", content); err != nil {
99+
return err
100+
}
101+
102+
if err := w.Close(); err != nil {
103+
return err
104+
}
105+
106+
return nil
107+
}
108+
19109
func (gac *GcsArtifactClient) DeleteArtifact(ctx context.Context, url string) error {
20-
// Get bucket name and gcsPrefix
21-
// the [5:] is to remove the "gs://" on the artifact uri
22-
// ex : gs://bucketName/path → bucketName/path
23-
gcsBucket, gcsLocation := gac.getGcsBucketAndLocation(url[5:])
110+
u, err := gac.ParseURL(url)
111+
if err != nil {
112+
return err
113+
}
24114

25115
// Sets the name for the bucket.
26-
bucket := gac.API.Bucket(gcsBucket)
116+
bucket := gac.API.Bucket(u.Bucket)
27117

28118
it := bucket.Objects(ctx, &storage.Query{
29-
Prefix: gcsLocation,
119+
Prefix: u.Object,
30120
})
31121
for {
32122
attrs, err := it.Next()
@@ -43,25 +133,24 @@ func (gac *GcsArtifactClient) DeleteArtifact(ctx context.Context, url string) er
43133
return nil
44134
}
45135

46-
func (gac *GcsArtifactClient) getGcsBucketAndLocation(str string) (string, string) {
47-
// Split string using delimiter
48-
// ex : bucketName/path/path1/item → (bucketName , path/path1/item)
49-
splitStr := strings.SplitN(str, "/", 2)
50-
return splitStr[0], splitStr[1]
136+
type NopArtifactClient struct{}
137+
138+
func NewNopArtifactClient() Service {
139+
return &NopArtifactClient{}
51140
}
52141

53-
func NewGcsArtifactClient(api *storage.Client) Service {
54-
return &GcsArtifactClient{
55-
API: api,
56-
}
142+
func (nac *NopArtifactClient) ParseURL(gsURL string) (*URL, error) {
143+
return nil, nil
57144
}
58145

59-
type NopArtifactClient struct{}
146+
func (nac *NopArtifactClient) ReadArtifact(ctx context.Context, url string) ([]byte, error) {
147+
return nil, nil
148+
}
60149

61-
func (nac *NopArtifactClient) DeleteArtifact(ctx context.Context, url string) error {
150+
func (nac *NopArtifactClient) WriteArtifact(ctx context.Context, url string, content []byte) error {
62151
return nil
63152
}
64153

65-
func NewNopArtifactClient() Service {
66-
return &NopArtifactClient{}
154+
func (nac *NopArtifactClient) DeleteArtifact(ctx context.Context, url string) error {
155+
return nil
67156
}

api/pkg/artifact/artifact_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package artifact
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"cloud.google.com/go/storage"
8+
)
9+
10+
func TestGcsArtifactClient_ParseURL(t *testing.T) {
11+
type fields struct {
12+
API *storage.Client
13+
}
14+
type args struct {
15+
gsURL string
16+
}
17+
tests := []struct {
18+
name string
19+
fields fields
20+
args args
21+
want *URL
22+
wantErr bool
23+
}{
24+
{
25+
name: "valid short url",
26+
fields: fields{
27+
API: nil,
28+
},
29+
args: args{
30+
gsURL: "gs://bucket-name/object-path",
31+
},
32+
want: &URL{
33+
Bucket: "bucket-name",
34+
Object: "object-path",
35+
},
36+
wantErr: false,
37+
},
38+
{
39+
name: "valid url",
40+
fields: fields{
41+
API: nil,
42+
},
43+
args: args{
44+
gsURL: "gs://bucket-name/object-path/object-path-2/object-path-3/file-1.txt",
45+
},
46+
want: &URL{
47+
Bucket: "bucket-name",
48+
Object: "object-path/object-path-2/object-path-3/file-1.txt",
49+
},
50+
wantErr: false,
51+
},
52+
}
53+
for _, tt := range tests {
54+
t.Run(tt.name, func(t *testing.T) {
55+
gac := &GcsArtifactClient{
56+
API: tt.fields.API,
57+
}
58+
got, err := gac.ParseURL(tt.args.gsURL)
59+
if (err != nil) != tt.wantErr {
60+
t.Errorf("GcsArtifactClient.ParseURL() error = %v, wantErr %v", err, tt.wantErr)
61+
return
62+
}
63+
if !reflect.DeepEqual(got, tt.want) {
64+
t.Errorf("GcsArtifactClient.ParseURL() = %v, want %v", got, tt.want)
65+
}
66+
})
67+
}
68+
}

api/pkg/artifact/mocks/artifact.go

Lines changed: 69 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)