Skip to content

Commit 607fb44

Browse files
committed
Azure blob storage support
Adds a Detector and getter for Azure Blob Storage with dependencies on: github.com/Azure/go-autorest/autorest/azure github.com/Azure/azure-storage-go An access key is required for the SDK Client, public blobs should be able to make use of the http Getter anyway.
1 parent c3d66e7 commit 607fb44

File tree

7 files changed

+484
-6
lines changed

7 files changed

+484
-6
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ can be augmented at runtime by implementing the `Getter` interface.
7272
* Mercurial
7373
* HTTP
7474
* Amazon S3
75+
* Azure Blob Storage
7576

7677
In addition to the above protocols, go-getter has what are called "detectors."
7778
These take a URL and attempt to automatically choose the best protocol for
@@ -251,3 +252,7 @@ Some examples for these addressing schemes:
251252
- bucket.s3.amazonaws.com/foo
252253
- bucket.s3-eu-west-1.amazonaws.com/foo/bar
253254

255+
### Azure Blob Storage (`azureblob`)
256+
257+
Azure Blob Storage requires a valid access key for the storage account, this can be provided by the
258+
`ARM_ACCESS_KEY` environment variable, or the `access_key` query value which takes priority.

detect.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func init() {
2525
new(GitHubDetector),
2626
new(BitBucketDetector),
2727
new(S3Detector),
28+
new(AzureBlobDetector),
2829
new(FileDetector),
2930
}
3031
}

detect_azure_blob.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package getter
2+
3+
import (
4+
"net/url"
5+
"strings"
6+
7+
"fmt"
8+
9+
"github.com/Azure/go-autorest/autorest/azure"
10+
)
11+
12+
var azureStorageSuffixes = map[string]azure.Environment{
13+
azure.PublicCloud.StorageEndpointSuffix: azure.PublicCloud,
14+
azure.GermanCloud.StorageEndpointSuffix: azure.GermanCloud,
15+
azure.USGovernmentCloud.StorageEndpointSuffix: azure.USGovernmentCloud,
16+
azure.ChinaCloud.StorageEndpointSuffix: azure.GermanCloud,
17+
}
18+
19+
// AzureBlobDetector implements Detector to detect Azure URLs and turn
20+
// them into URLs that the Azure getter can understand.
21+
type AzureBlobDetector struct{}
22+
23+
func (d *AzureBlobDetector) Detect(src, _ string) (string, bool, error) {
24+
if len(src) == 0 {
25+
return "", false, nil
26+
}
27+
28+
for s := range azureStorageSuffixes {
29+
if strings.Contains(src, s) {
30+
return d.detectURL(src)
31+
}
32+
}
33+
34+
return "", false, nil
35+
}
36+
37+
func (d *AzureBlobDetector) detectURL(src string) (string, bool, error) {
38+
u, err := url.Parse(src)
39+
if err != nil {
40+
return "", false, err
41+
}
42+
43+
parts := strings.Split(u.Path, "/")
44+
if len(parts) < 2 {
45+
return "", false, fmt.Errorf("path to blob must not be empty")
46+
}
47+
48+
u.Scheme = "https"
49+
50+
return fmt.Sprintf("azureblob::%s", u.String()), true, nil
51+
}

detect_azure_blob_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package getter
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestAzureBlobDetector(t *testing.T) {
8+
cases := []struct {
9+
Input string
10+
Output string
11+
}{
12+
{
13+
"account.blob.core.windows.net/foo/bar",
14+
"azureblob::https://account.blob.core.windows.net/foo/bar",
15+
},
16+
{
17+
"account.blob.core.usgovcloudapi.net/foo/bar",
18+
"azureblob::https://account.blob.core.usgovcloudapi.net/foo/bar",
19+
},
20+
{
21+
"account.blob.core.chinacloudapi.cn/foo/bar",
22+
"azureblob::https://account.blob.core.chinacloudapi.cn/foo/bar",
23+
},
24+
{
25+
"account.blob.core.cloudapi.de/foo/bar",
26+
"azureblob::https://account.blob.core.cloudapi.de/foo/bar",
27+
},
28+
// Misc tests
29+
{
30+
"account.blob.core.windows.net/foo/bar?version=1234",
31+
"azureblob::https://account.blob.core.windows.net/foo/bar?version=1234",
32+
},
33+
}
34+
35+
pwd := "/pwd"
36+
f := new(AzureBlobDetector)
37+
for i, tc := range cases {
38+
output, ok, err := f.Detect(tc.Input, pwd)
39+
if err != nil {
40+
t.Fatalf("err: %s", err)
41+
}
42+
if !ok {
43+
t.Fatal("not ok")
44+
}
45+
46+
if output != tc.Output {
47+
t.Fatalf("%d: bad: %#v", i, output)
48+
}
49+
}
50+
}

get.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,13 @@ func init() {
5353
httpGetter := &HttpGetter{Netrc: true}
5454

5555
Getters = map[string]Getter{
56-
"file": new(FileGetter),
57-
"git": new(GitGetter),
58-
"hg": new(HgGetter),
59-
"s3": new(S3Getter),
60-
"http": httpGetter,
61-
"https": httpGetter,
56+
"file": new(FileGetter),
57+
"git": new(GitGetter),
58+
"hg": new(HgGetter),
59+
"s3": new(S3Getter),
60+
"azureblob": new(AzureBlobGetter),
61+
"http": httpGetter,
62+
"https": httpGetter,
6263
}
6364
}
6465

get_azure_blob.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package getter
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/url"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
storage "github.com/Azure/azure-storage-go"
12+
)
13+
14+
// AzureBlobGetter is a Getter implementation that will download a module from
15+
// an Azure Blob Storage Account.
16+
type AzureBlobGetter struct{}
17+
18+
func (g *AzureBlobGetter) ClientMode(u *url.URL) (ClientMode, error) {
19+
// Parse URL
20+
accountName, baseURL, containerName, blobPath, accessKey, err := g.parseUrl(u)
21+
if err != nil {
22+
return 0, err
23+
}
24+
25+
client, err := g.getBobClient(accountName, baseURL, accessKey)
26+
if err != nil {
27+
return 0, err
28+
}
29+
30+
container := client.GetContainerReference(containerName)
31+
32+
// List the object(s) at the given prefix
33+
params := storage.ListBlobsParameters{
34+
Prefix: blobPath,
35+
}
36+
resp, err := container.ListBlobs(params)
37+
if err != nil {
38+
return 0, err
39+
}
40+
41+
for _, b := range resp.Blobs {
42+
// Use file mode on exact match.
43+
if b.Name == blobPath {
44+
return ClientModeFile, nil
45+
}
46+
47+
// Use dir mode if child keys are found.
48+
if strings.HasPrefix(b.Name, blobPath+"/") {
49+
return ClientModeDir, nil
50+
}
51+
}
52+
53+
// There was no match, so just return file mode. The download is going
54+
// to fail but we will let Azure return the proper error later.
55+
return ClientModeFile, nil
56+
}
57+
58+
func (g *AzureBlobGetter) Get(dst string, u *url.URL) error {
59+
// Parse URL
60+
accountName, baseURL, containerName, blobPath, accessKey, err := g.parseUrl(u)
61+
if err != nil {
62+
return err
63+
}
64+
65+
// Remove destination if it already exists
66+
_, err = os.Stat(dst)
67+
if err != nil && !os.IsNotExist(err) {
68+
return err
69+
}
70+
71+
if err == nil {
72+
// Remove the destination
73+
if err := os.RemoveAll(dst); err != nil {
74+
return err
75+
}
76+
}
77+
78+
// Create all the parent directories
79+
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
80+
return err
81+
}
82+
83+
client, err := g.getBobClient(accountName, baseURL, accessKey)
84+
if err != nil {
85+
return err
86+
}
87+
88+
container := client.GetContainerReference(containerName)
89+
90+
// List files in path, keep listing until no more objects are found
91+
lastMarker := ""
92+
hasMore := true
93+
for hasMore {
94+
params := storage.ListBlobsParameters{
95+
Prefix: blobPath,
96+
}
97+
if lastMarker != "" {
98+
params.Marker = lastMarker
99+
}
100+
101+
resp, err := container.ListBlobs(params)
102+
if err != nil {
103+
return err
104+
}
105+
106+
hasMore = resp.NextMarker != ""
107+
lastMarker = resp.NextMarker
108+
109+
// Get each object storing each file relative to the destination path
110+
for _, object := range resp.Blobs {
111+
objPath := object.Name
112+
113+
// If the key ends with a backslash assume it is a directory and ignore
114+
if strings.HasSuffix(objPath, "/") {
115+
continue
116+
}
117+
118+
// Get the object destination path
119+
objDst, err := filepath.Rel(blobPath, objPath)
120+
if err != nil {
121+
return err
122+
}
123+
objDst = filepath.Join(dst, objDst)
124+
125+
if err := g.getObject(client, objDst, containerName, objPath); err != nil {
126+
return err
127+
}
128+
}
129+
}
130+
131+
return nil
132+
}
133+
134+
func (g *AzureBlobGetter) GetFile(dst string, u *url.URL) error {
135+
accountName, baseURL, containerName, blobPath, accessKey, err := g.parseUrl(u)
136+
if err != nil {
137+
return err
138+
}
139+
140+
client, err := g.getBobClient(accountName, baseURL, accessKey)
141+
if err != nil {
142+
return err
143+
}
144+
145+
return g.getObject(client, dst, containerName, blobPath)
146+
}
147+
148+
func (g *AzureBlobGetter) getObject(client storage.BlobStorageClient, dst, container, blobName string) error {
149+
r, err := client.GetBlob(container, blobName)
150+
if err != nil {
151+
return err
152+
}
153+
154+
// Create all the parent directories
155+
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
156+
return err
157+
}
158+
159+
f, err := os.Create(dst)
160+
if err != nil {
161+
return err
162+
}
163+
defer f.Close()
164+
165+
_, err = io.Copy(f, r)
166+
return err
167+
}
168+
169+
func (g *AzureBlobGetter) getBobClient(accountName string, baseURL string, accountKey string) (storage.BlobStorageClient, error) {
170+
var b storage.BlobStorageClient
171+
172+
if accountKey == "" {
173+
accountKey = os.Getenv("ARM_ACCESS_KEY")
174+
}
175+
176+
c, err := storage.NewClient(accountName, accountKey, baseURL, storage.DefaultAPIVersion, true)
177+
if err != nil {
178+
return b, err
179+
}
180+
181+
b = c.GetBlobService()
182+
183+
return b, nil
184+
}
185+
186+
func (g *AzureBlobGetter) parseUrl(u *url.URL) (accountName, baseURL, container, blobPath, accessKey string, err error) {
187+
// Expected host style: accountname.blob.core.windows.net.
188+
// The last 3 parts will be different across environments.
189+
hostParts := strings.SplitN(u.Host, ".", 3)
190+
if len(hostParts) != 3 {
191+
err = fmt.Errorf("URL is not a valid Azure Blob URL")
192+
return
193+
}
194+
195+
accountName = hostParts[0]
196+
baseURL = hostParts[2]
197+
198+
pathParts := strings.SplitN(strings.TrimPrefix(u.Path, "/"), "/", 2)
199+
if len(pathParts) != 2 {
200+
err = fmt.Errorf("URL is not a valid Azure Blob URL")
201+
return
202+
}
203+
204+
container = pathParts[0]
205+
blobPath = pathParts[1]
206+
207+
accessKey = u.Query().Get("access_key")
208+
209+
return
210+
}

0 commit comments

Comments
 (0)