Skip to content

Commit 1674dba

Browse files
committed
feat: support install module from Alibaba Cloud OSS bucket
1 parent 3713030 commit 1674dba

File tree

8 files changed

+653
-1405
lines changed

8 files changed

+653
-1405
lines changed

detect.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func init() {
3131
new(BitBucketDetector),
3232
new(S3Detector),
3333
new(GCSDetector),
34+
new(OSSDetector),
3435
new(FileDetector),
3536
}
3637
}

detect_oss.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package getter
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"strings"
7+
)
8+
9+
// OSSDetector implements Detector to detect OSS URLs and turn
10+
// them into URLs that the OSSGetter can understand.
11+
type OSSDetector struct{}
12+
13+
func (d *OSSDetector) Detect(src, _ string) (string, bool, error) {
14+
if len(src) == 0 {
15+
return "", false, nil
16+
}
17+
18+
if strings.Contains(src, ".aliyuncs.com/") {
19+
return d.detectHTTP(src)
20+
}
21+
22+
return "", false, nil
23+
}
24+
25+
func (d *OSSDetector) detectHTTP(src string) (string, bool, error) {
26+
parts := strings.Split(src, "/")
27+
if len(parts) < 2 {
28+
return "", false, fmt.Errorf(
29+
"URL is not a valid OSS URL")
30+
}
31+
32+
urlStr := fmt.Sprintf("https://%s", strings.Join(parts, "/"))
33+
url, err := url.Parse(urlStr)
34+
if err != nil {
35+
return "", true, fmt.Errorf("error parsing OSS URL: %s", err)
36+
}
37+
38+
return "oss::" + url.String(), true, nil
39+
}

detect_oss_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package getter
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestOSSDetector(t *testing.T) {
8+
cases := []struct {
9+
Input string
10+
Output string
11+
}{
12+
{
13+
"go-getter.oss-ap-southeast-1.aliyuncs.com/foo",
14+
"oss::https://go-getter.oss-ap-southeast-1.aliyuncs.com/foo",
15+
},
16+
{
17+
"go-getter.oss-ap-southeast-1.aliyuncs.com/foo/bar",
18+
"oss::https://go-getter.oss-ap-southeast-1.aliyuncs.com/foo/bar",
19+
},
20+
{
21+
"go-getter.oss-ap-southeast-1.aliyuncs.com/foo/bar.baz",
22+
"oss::https://go-getter.oss-ap-southeast-1.aliyuncs.com/foo/bar.baz",
23+
},
24+
}
25+
26+
pwd := "/pwd"
27+
f := new(OSSDetector)
28+
for i, tc := range cases {
29+
output, ok, err := f.Detect(tc.Input, pwd)
30+
if err != nil {
31+
t.Fatalf("err: %s", err)
32+
}
33+
if !ok {
34+
t.Fatal("not ok")
35+
}
36+
37+
if output != tc.Output {
38+
t.Fatalf("%d: bad: %#v", i, output)
39+
}
40+
}
41+
}

get.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func init() {
7373
"gcs": new(GCSGetter),
7474
"hg": new(HgGetter),
7575
"s3": new(S3Getter),
76+
"oss": new(OSSGetter),
7677
"http": httpGetter,
7778
"https": httpGetter,
7879
}

get_oss.go

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
package getter
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"time"
11+
12+
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
13+
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
14+
15+
openapicred "github.com/aliyun/credentials-go/credentials"
16+
)
17+
18+
// OSSGetter is a Getter implementation that will download a module from
19+
// an OSS bucket.
20+
type OSSGetter struct {
21+
getter
22+
23+
// Timeout sets a deadline which all OSS operations should
24+
// complete within.
25+
//
26+
// The zero value means timeout.
27+
Timeout time.Duration
28+
}
29+
30+
func (g *OSSGetter) ClientMode(u *url.URL) (ClientMode, error) {
31+
// Parse URL
32+
ctx := g.Context()
33+
34+
if g.Timeout > 0 {
35+
var cancel context.CancelFunc
36+
ctx, cancel = context.WithTimeout(ctx, g.Timeout)
37+
defer cancel()
38+
}
39+
40+
region, bucket, path, _, err := g.parseUrl(u)
41+
if err != nil {
42+
return 0, err
43+
}
44+
45+
// Create client config
46+
client, err := g.newOSSClient(region, u)
47+
if err != nil {
48+
return 0, err
49+
}
50+
51+
// List the object(s) at the given prefix
52+
request := &oss.ListObjectsV2Request{
53+
Bucket: oss.Ptr(bucket),
54+
Prefix: oss.Ptr(path),
55+
}
56+
57+
p := client.NewListObjectsV2Paginator(request)
58+
59+
var i int
60+
for p.HasNext() {
61+
i++
62+
page, err := p.NextPage(ctx)
63+
if err != nil {
64+
return 0, err
65+
}
66+
for _, obj := range page.Contents {
67+
// Use file mode on exact match.
68+
if *obj.Key == path {
69+
return ClientModeFile, nil
70+
}
71+
72+
// Use dir mode if child keys are found.
73+
if strings.HasPrefix(*obj.Key, path+"/") {
74+
return ClientModeDir, nil
75+
}
76+
}
77+
}
78+
79+
// There was no match, so just return file mode. The download is going
80+
// to fail but we will let OSS return the proper error later.
81+
return ClientModeFile, nil
82+
}
83+
84+
func (g *OSSGetter) Get(dst string, u *url.URL) error {
85+
ctx := g.Context()
86+
87+
if g.Timeout > 0 {
88+
var cancel context.CancelFunc
89+
ctx, cancel = context.WithTimeout(ctx, g.Timeout)
90+
defer cancel()
91+
}
92+
93+
region, bucket, path, version, err := g.parseUrl(u)
94+
if err != nil {
95+
return err
96+
}
97+
98+
// Remove destination if it already exists
99+
_, err = os.Stat(dst)
100+
if err != nil && !os.IsNotExist(err) {
101+
return err
102+
}
103+
104+
if err == nil {
105+
// Remove the destination
106+
if err := os.RemoveAll(dst); err != nil {
107+
return err
108+
}
109+
}
110+
111+
// Create all the parent directories
112+
if err := os.MkdirAll(filepath.Dir(dst), g.client.mode(0755)); err != nil {
113+
return err
114+
}
115+
116+
// Create client config
117+
client, err := g.newOSSClient(region, u)
118+
if err != nil {
119+
return err
120+
}
121+
122+
// List files in path, keep listing until no more objects are found
123+
request := &oss.ListObjectsV2Request{
124+
Bucket: oss.Ptr(bucket),
125+
Prefix: oss.Ptr(path),
126+
}
127+
128+
p := client.NewListObjectsV2Paginator(request)
129+
130+
var i int
131+
for p.HasNext() {
132+
i++
133+
page, err := p.NextPage(ctx)
134+
if err != nil {
135+
return err
136+
}
137+
for _, obj := range page.Contents {
138+
objPath := *obj.Key
139+
140+
// If the key ends with a backslash assume it is a directory and ignore
141+
if strings.HasSuffix(objPath, "/") {
142+
continue
143+
}
144+
145+
// Get the object destination path
146+
objDst, err := filepath.Rel(path, objPath)
147+
if err != nil {
148+
return err
149+
}
150+
objDst = filepath.Join(dst, objDst)
151+
152+
if err := g.getObject(ctx, client, objDst, bucket, objPath, version); err != nil {
153+
return err
154+
}
155+
156+
}
157+
}
158+
159+
return nil
160+
}
161+
162+
func (g *OSSGetter) GetFile(dst string, u *url.URL) error {
163+
ctx := g.Context()
164+
165+
if g.Timeout > 0 {
166+
var cancel context.CancelFunc
167+
ctx, cancel = context.WithTimeout(ctx, g.Timeout)
168+
defer cancel()
169+
}
170+
171+
region, bucket, path, version, err := g.parseUrl(u)
172+
if err != nil {
173+
return err
174+
}
175+
176+
client, err := g.newOSSClient(region, u)
177+
if err != nil {
178+
return err
179+
}
180+
181+
return g.getObject(ctx, client, dst, bucket, path, version)
182+
}
183+
184+
func (g *OSSGetter) getObject(ctx context.Context, client *oss.Client, dst, bucket, object, version string) error {
185+
request := &oss.GetObjectRequest{
186+
Bucket: oss.Ptr(bucket),
187+
Key: oss.Ptr(object),
188+
}
189+
190+
if version != "" {
191+
request.VersionId = oss.Ptr(version)
192+
}
193+
194+
result, err := client.GetObject(ctx, request)
195+
if err != nil {
196+
return err
197+
}
198+
199+
// Create all the parent directories
200+
if err := os.MkdirAll(filepath.Dir(dst), g.client.mode(0755)); err != nil {
201+
return err
202+
}
203+
204+
body := result.Body
205+
206+
// There is no limit set for the size of an object from OSS
207+
return copyReader(dst, body, 0666, g.client.umask(), 0)
208+
}
209+
210+
func (g *OSSGetter) parseUrl(u *url.URL) (region, bucket, path, version string, err error) {
211+
if strings.Contains(u.Host, "aliyuncs.com") {
212+
hostParts := strings.Split(u.Host, ".")
213+
214+
switch len(hostParts) {
215+
// path-style
216+
case 4:
217+
bucket = hostParts[0]
218+
region = strings.TrimPrefix(hostParts[1], "oss-")
219+
region = strings.TrimSuffix(region, "-internal")
220+
221+
case 5:
222+
bucket = hostParts[0]
223+
region = hostParts[1]
224+
}
225+
226+
pathParts := strings.SplitN(u.Path, "/", 2)
227+
if len(pathParts) != 2 {
228+
err = fmt.Errorf("URL is not a valid OSS URL")
229+
return
230+
}
231+
path = pathParts[1]
232+
233+
if len(hostParts) < 4 || len(hostParts) > 5 {
234+
err = fmt.Errorf("URL is not a valid OSS URL")
235+
return
236+
}
237+
238+
version = u.Query().Get("version")
239+
}
240+
return
241+
}
242+
243+
func (g *OSSGetter) newOSSClient(region string, url *url.URL) (*oss.Client, error) {
244+
245+
arnCredential, gerr := openapicred.NewCredential(nil)
246+
provider := credentials.CredentialsProviderFunc(func(ctx context.Context) (credentials.Credentials, error) {
247+
if gerr != nil {
248+
return credentials.Credentials{}, gerr
249+
}
250+
cred, err := arnCredential.GetCredential()
251+
if err != nil {
252+
return credentials.Credentials{}, err
253+
}
254+
return credentials.Credentials{
255+
AccessKeyID: *cred.AccessKeyId,
256+
AccessKeySecret: *cred.AccessKeySecret,
257+
SecurityToken: *cred.SecurityToken,
258+
}, nil
259+
})
260+
261+
cfg := oss.LoadDefaultConfig().
262+
WithCredentialsProvider(provider).
263+
WithRegion(region)
264+
265+
client := oss.NewClient(cfg)
266+
267+
return client, nil
268+
}

0 commit comments

Comments
 (0)