Skip to content

Commit ed968ff

Browse files
committed
chore(volumes): add filer client
1 parent 99bda55 commit ed968ff

7 files changed

Lines changed: 331 additions & 21 deletions

File tree

api/filer.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package api
2+
3+
type FilerDirEntry struct {
4+
Name string `json:"name,omitempty"`
5+
Size string `json:"size,omitempty"`
6+
Type string `json:"type,omitempty"`
7+
Timestamp string `json:"timestamp,omitempty"`
8+
}
9+
10+
type FilerDirEntries []FilerDirEntry

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/drycc/controller-sdk-go
33
go 1.22
44

55
require (
6+
github.com/google/uuid v1.6.0
67
github.com/stretchr/testify v1.9.0
78
golang.org/x/net v0.23.0
89
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
22
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
4+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
35
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
46
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
57
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=

http.go

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,9 @@ func createHTTPClient(sslVerify bool) *http.Client {
2121
return &http.Client{Transport: tr}
2222
}
2323

24-
// Request makes a HTTP request with the given method, relative URL, and body on the controller.
25-
// It also sets the Authorization and Content-Type headers to properly authenticate and communicate
26-
// API. This is primarily intended to use be used by the SDK itself, but could potentially be used elsewhere.
27-
func (c *Client) Request(method string, path string, body []byte) (*http.Response, error) {
28-
url := *c.ControllerURL
29-
30-
if strings.Contains(path, "?") {
31-
parts := strings.Split(path, "?")
32-
url.Path = parts[0]
33-
url.RawQuery = parts[1]
34-
} else {
35-
url.Path = path
36-
}
37-
38-
req, err := http.NewRequest(method, url.String(), bytes.NewBuffer(body))
39-
40-
if err != nil {
41-
return nil, err
42-
}
43-
44-
req.Header.Add("Content-Type", "application/json")
24+
// Do sends an HTTP request and returns an HTTP response,
25+
// following policy (such as redirects, cookies, auth) as configured on the client.
26+
func (c *Client) Do(req *http.Request) (*http.Response, error) {
4527

4628
if c.Token != "" {
4729
req.Header.Add("Authorization", "token "+c.Token)
@@ -50,6 +32,9 @@ func (c *Client) Request(method string, path string, body []byte) (*http.Respons
5032
if c.HooksToken != "" {
5133
req.Header.Add("X-Drycc-Builder-Auth", c.HooksToken)
5234
}
35+
if req.Header.Get("Content-Type") == "" {
36+
req.Header.Add("Content-Type", "application/json")
37+
}
5338

5439
addUserAgent(&req.Header, c.UserAgent)
5540

@@ -73,6 +58,31 @@ func (c *Client) Request(method string, path string, body []byte) (*http.Respons
7358
return res, checkAPICompatibility(apiVersion, APIVersion)
7459
}
7560

61+
// NewRequest wraps [NewRequestWithContext] using [context.Background].
62+
func (c *Client) NewRequest(method string, path string, body io.Reader) (*http.Request, error) {
63+
url := *c.ControllerURL
64+
65+
if strings.Contains(path, "?") {
66+
parts := strings.Split(path, "?")
67+
url.Path = parts[0]
68+
url.RawQuery = parts[1]
69+
} else {
70+
url.Path = path
71+
}
72+
return http.NewRequest(method, url.String(), body)
73+
}
74+
75+
// Request makes a HTTP request with the given method, relative URL, and body on the controller.
76+
// It also sets the Authorization and Content-Type headers to properly authenticate and communicate
77+
// API. This is primarily intended to use be used by the SDK itself, but could potentially be used elsewhere.
78+
func (c *Client) Request(method string, path string, body []byte) (*http.Response, error) {
79+
req, err := c.NewRequest(method, path, bytes.NewBuffer(body))
80+
if err != nil {
81+
return nil, err
82+
}
83+
return c.Do(req)
84+
}
85+
7686
// LimitedRequest allows limiting the number of responses in a request.
7787
func (c *Client) LimitedRequest(path string, results int) (string, int, error) {
7888
res, reqErr := c.Request("GET", path+"?limit="+strconv.Itoa(results), nil)

volumes/filer.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Package config provides methods for managing configuration of apps.
2+
package volumes
3+
4+
import (
5+
"bytes"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"mime/multipart"
10+
"net/http"
11+
"os"
12+
13+
drycc "github.com/drycc/controller-sdk-go"
14+
"github.com/drycc/controller-sdk-go/api"
15+
)
16+
17+
// ListDir to an app's volume.
18+
func ListDir(c *drycc.Client, appID, volumeID, path string, results int) (api.FilerDirEntries, int, error) {
19+
u := fmt.Sprintf("/v2/apps/%s/volumes/%s/client/?path=%s", appID, volumeID, path)
20+
21+
body, count, reqErr := c.LimitedRequest(u, results)
22+
23+
if reqErr != nil && !drycc.IsErrAPIMismatch(reqErr) {
24+
return []api.FilerDirEntry{}, -1, reqErr
25+
}
26+
27+
var filerDirEntries []api.FilerDirEntry
28+
if err := json.Unmarshal([]byte(body), &filerDirEntries); err != nil {
29+
return []api.FilerDirEntry{}, -1, err
30+
}
31+
32+
return filerDirEntries, count, reqErr
33+
}
34+
35+
// Getfile to an app's volume.
36+
func GetFile(c *drycc.Client, appID, volumeID, path string) (*http.Response, error) {
37+
u := fmt.Sprintf("/v2/apps/%s/volumes/%s/client/%s", appID, volumeID, path)
38+
req, err := c.NewRequest("GET", u, nil)
39+
if err != nil {
40+
return nil, err
41+
}
42+
return c.Do(req)
43+
}
44+
45+
// Put file to an app's volume.
46+
func PostFile(c *drycc.Client, appID, volumeID, path string, files ...string) (*http.Response, error) {
47+
buffer := new(bytes.Buffer)
48+
writer := multipart.NewWriter(buffer)
49+
for _, file := range files {
50+
if f, err := os.Open(file); err != nil {
51+
return nil, err
52+
} else if part, err := writer.CreateFormFile("file", f.Name()); err != nil {
53+
return nil, err
54+
} else if _, err = io.Copy(part, f); err != nil {
55+
return nil, err
56+
} else {
57+
defer f.Close()
58+
}
59+
}
60+
61+
if err := writer.WriteField("path", path); err != nil {
62+
return nil, err
63+
}
64+
writer.Close()
65+
66+
u := fmt.Sprintf("/v2/apps/%s/volumes/%s/client/", appID, volumeID)
67+
r, err := c.NewRequest("POST", u, buffer)
68+
if err != nil {
69+
return nil, err
70+
}
71+
r.Header.Add("Content-Type", writer.FormDataContentType())
72+
return c.Do(r)
73+
}
74+
75+
// Get file to an app's volume.
76+
func DeleteFile(c *drycc.Client, appID, volumeID, path string) (*http.Response, error) {
77+
u := fmt.Sprintf("/v2/apps/%s/volumes/%s/client/%s", appID, volumeID, path)
78+
return c.Request("DELETE", u, nil)
79+
}

volumes/filer_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package volumes
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
8+
"os"
9+
"reflect"
10+
"strings"
11+
"testing"
12+
13+
drycc "github.com/drycc/controller-sdk-go"
14+
)
15+
16+
const volumeFileContentExpected string = `hello world`
17+
18+
type fakeFilerServer struct{}
19+
20+
func (f *fakeFilerServer) ServeHTTP(res http.ResponseWriter, req *http.Request) {
21+
res.Header().Add("DRYCC_API_VERSION", drycc.APIVersion)
22+
// get/delete file
23+
if strings.Contains(req.URL.Path, "/v2/apps/example-go/volumes/myvolume/client/tmp/helloword.txt") {
24+
if req.Method == "GET" {
25+
res.Header().Add("Content-Type", "application/octet-stream")
26+
res.Write([]byte(volumeFileContentExpected))
27+
return
28+
} else if req.Method == "DELETE" {
29+
res.WriteHeader(http.StatusNoContent)
30+
res.Write(nil)
31+
return
32+
}
33+
}
34+
// post file or list dir
35+
if strings.Contains(req.URL.Path, "/v2/apps/example-go/volumes/myvolume/client/") {
36+
if req.Method == "GET" {
37+
res.Header().Add("Content-Type", "application/json")
38+
res.Write([]byte(`{"results":[], "count": 0}`))
39+
return
40+
} else if req.Method == "POST" {
41+
if err := req.ParseMultipartForm(1024 * 1024); err != nil {
42+
http.Error(res, fmt.Sprintf("Parse multipart form error: %v", err), http.StatusBadRequest)
43+
return
44+
}
45+
for _, tmpFiles := range req.MultipartForm.File {
46+
for _, tmpFile := range tmpFiles {
47+
srcFile, err := tmpFile.Open()
48+
if err != nil {
49+
return
50+
}
51+
body, err := io.ReadAll(srcFile)
52+
if err != nil {
53+
return
54+
}
55+
if string(body) != volumeFileContentExpected {
56+
fmt.Printf("Expected '%s', Got '%s'\n", volumeFileContentExpected, body)
57+
res.WriteHeader(http.StatusInternalServerError)
58+
res.Write(nil)
59+
return
60+
}
61+
}
62+
}
63+
return
64+
}
65+
}
66+
67+
fmt.Printf("Unrecognized URL %s\n", req.URL)
68+
res.WriteHeader(http.StatusNotFound)
69+
res.Write(nil)
70+
}
71+
72+
func TestVolumesListDir(t *testing.T) {
73+
t.Parallel()
74+
75+
handler := fakeFilerServer{}
76+
server := httptest.NewServer(&handler)
77+
defer server.Close()
78+
79+
drycc, err := drycc.New(false, server.URL, "abc")
80+
if err != nil {
81+
t.Fatal(err)
82+
}
83+
84+
_, counts, err := ListDir(drycc, "example-go", "myvolume", "tmp", 3000)
85+
if err != nil {
86+
t.Fatal(err)
87+
}
88+
if counts != 0 {
89+
t.Error(fmt.Errorf("Expected %v, Got %v", 0, counts))
90+
}
91+
}
92+
93+
func TestVolumesGetFile(t *testing.T) {
94+
t.Parallel()
95+
96+
handler := fakeFilerServer{}
97+
server := httptest.NewServer(&handler)
98+
defer server.Close()
99+
100+
drycc, err := drycc.New(false, server.URL, "abc")
101+
if err != nil {
102+
t.Fatal(err)
103+
}
104+
105+
res, err := GetFile(drycc, "example-go", "myvolume", "tmp/helloword.txt")
106+
if err != nil {
107+
t.Fatal(err)
108+
}
109+
110+
body, err := io.ReadAll(res.Body)
111+
if err != nil {
112+
t.Fatal(err)
113+
}
114+
115+
if !reflect.DeepEqual(volumeFileContentExpected, string(body)) {
116+
t.Error(fmt.Errorf("Expected %v, Got %v", volumeFileContentExpected, string(body)))
117+
}
118+
}
119+
120+
func TestVolumesPostFile(t *testing.T) {
121+
t.Parallel()
122+
123+
handler := fakeFilerServer{}
124+
server := httptest.NewServer(&handler)
125+
defer server.Close()
126+
127+
testFile := "helloword.txt"
128+
129+
err := os.WriteFile(testFile, []byte(volumeFileContentExpected), 0644)
130+
if err != nil {
131+
t.Fatal(err)
132+
}
133+
defer func() {
134+
if err := os.Remove(testFile); err != nil {
135+
t.Fatal(err)
136+
}
137+
}()
138+
139+
drycc, err := drycc.New(false, server.URL, "abc")
140+
if err != nil {
141+
t.Fatal(err)
142+
}
143+
144+
if _, err := PostFile(drycc, "example-go", "myvolume", "tmp/", testFile); err != nil {
145+
t.Fatal(err)
146+
}
147+
148+
}
149+
150+
func TestVolumesDeleteFile(t *testing.T) {
151+
t.Parallel()
152+
153+
handler := fakeFilerServer{}
154+
server := httptest.NewServer(&handler)
155+
defer server.Close()
156+
157+
drycc, err := drycc.New(false, server.URL, "abc")
158+
if err != nil {
159+
t.Fatal(err)
160+
}
161+
162+
_, err = DeleteFile(drycc, "example-go", "myvolume", "tmp/helloword.txt")
163+
if err != nil {
164+
t.Fatal(err)
165+
}
166+
}

0 commit comments

Comments
 (0)