Skip to content

Commit 395f49e

Browse files
committed
Add client support for chunking
1 parent e48ad40 commit 395f49e

File tree

3 files changed

+120
-43
lines changed

3 files changed

+120
-43
lines changed

src/SDK/Language/Go.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ public function getFiles(): array
8888
'destination' => '{{ spec.title | caseLower}}/client.go',
8989
'template' => 'go/client.go.twig',
9090
],
91+
[
92+
'scope' => 'default',
93+
'destination' => '{{ spec.title | caseLower}}/inputFile.go',
94+
'template' => 'go/inputFile.go.twig',
95+
],
9196
[
9297
'scope' => 'service',
9398
'destination' => '{{ spec.title | caseLower}}/{{service.name | caseDash}}.go',

templates/go/client.go.twig

Lines changed: 103 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"net/http/cookiejar"
1414
"net/url"
1515
"os"
16-
"path/filepath"
1716
"reflect"
1817
"strconv"
1918
"strings"
@@ -23,6 +22,7 @@ import (
2322
const (
2423
fileNameKey = "file"
2524
defaultTimeout = 10 * time.Second
25+
defaultChunkSize = 5 * 1024 * 1024
2626
)
2727

2828
// {{ spec.title | caseUcfirst }}Error represents an error of a client request
@@ -58,6 +58,7 @@ type Client struct {
5858
endpoint string
5959
timeout time.Duration
6060
selfSigned bool
61+
chunkSize int64
6162
}
6263

6364
// NewClient initializes a new {{ spec.title | caseUcfirst }} client with a given timeout
@@ -78,6 +79,7 @@ func NewClient() Client {
7879
client: httpClient,
7980
timeout: defaultTimeout,
8081
headers: headers,
82+
chunkSize: defaultChunkSize,
8183
}
8284
}
8385

@@ -87,8 +89,8 @@ func (clt *Client) String() string {
8789

8890
func getDefaultClient(timeout time.Duration) (*http.Client, error) {
8991
jar, err := cookiejar.New(nil)
90-
if err != nil {
91-
return nil, err
92+
if err != nil {
93+
return nil, err
9294
}
9395
return &http.Client {
9496
Jar: jar,
@@ -114,6 +116,11 @@ func (clt *Client) SetSelfSigned(status bool) {
114116
clt.selfSigned = status
115117
}
116118

119+
// SetChunkSize sets the chunk size for file upload
120+
func (clt *Client) SetChunkSize(size int64) {
121+
clt.chunkSize = size
122+
}
123+
117124
// AddHeader add a new custom header that the Client should send on each request
118125
func (clt *Client) AddHeader(key string, value string) {
119126
clt.headers[key] = value
@@ -136,59 +143,69 @@ func isFileUpload(headers map[string]interface{}) bool {
136143
return false
137144
}
138145

139-
func (clt *Client) newfileUploadRequest(uri string, params map[string]interface{}, paramName string) (*http.Request, error) {
140-
path, ok := params[paramName].(string)
146+
func (clt *Client) FileUpload(url string, headers map[string]interface{}, params map[string]interface{}, paramName string, uploadId string) (*ClientResponse, error) {
147+
inputFile, ok := params[paramName].(InputFile)
141148
if !ok {
142-
return nil, os.ErrNotExist
143-
}
144-
if _, err := os.Stat(path); err != nil {
145-
return nil, err
149+
msg := fmt.Sprintf("invalid input file. params[%s] must be of type InputFile", paramName)
150+
return nil, errors.New(msg)
146151
}
147-
file, err := os.Open(path)
152+
153+
file, err := os.Open(inputFile.Path)
148154
if err != nil {
149155
return nil, err
150156
}
151157
defer file.Close()
152158

153-
body := &bytes.Buffer{}
154-
writer := multipart.NewWriter(body)
155-
part, err := writer.CreateFormFile(paramName, filepath.Base(path))
156-
if err != nil {
157-
return nil, err
158-
}
159-
_, err = io.Copy(part, file)
159+
fileInfo, err := file.Stat()
160160
if err != nil {
161161
return nil, err
162162
}
163163

164-
for key, val := range params {
165-
rt := reflect.TypeOf(val)
166-
switch rt.Kind() {
167-
case reflect.Array:
168-
case reflect.Slice:
169-
arr := reflect.ValueOf(val)
170-
for i := 0; i < arr.Len(); i++ {
171-
err = writer.WriteField(fmt.Sprintf("%s[]", key), toString(arr.Index(i)))
172-
if err != nil {
173-
return nil, err
174-
}
175-
}
176-
default:
177-
err = writer.WriteField(key, toString(val))
178-
if err != nil {
179-
return nil, err
180-
}
164+
inputFile.Data = make([]byte, clt.chunkSize)
165+
166+
var result *ClientResponse
167+
168+
numChunks := fileInfo.Size() / clt.chunkSize
169+
if fileInfo.Size()%clt.chunkSize != 0 {
170+
numChunks++
171+
}
172+
var currentChunk int64 = 0
173+
if uploadId != "" && uploadId != "unique()" {
174+
resp, err := clt.Call("GET", url + "/" + uploadId, nil, nil)
175+
if err == nil {
176+
currentChunk = int64(resp.Result.(map[string]interface{})["chunksUploaded"].(float64))
181177
}
182178
}
179+
for i := currentChunk; i < numChunks; i++ {
180+
chunkSize := clt.chunkSize
181+
offset := int64(i) * chunkSize
182+
if i == numChunks-1 {
183+
chunkSize = fileInfo.Size() - offset
184+
inputFile.Data = make([]byte, chunkSize)
185+
}
186+
_, err := file.ReadAt(inputFile.Data, offset)
187+
if err != nil && err != io.EOF {
188+
return nil, err
189+
}
190+
params[paramName] = inputFile
191+
if uploadId != "" {
192+
headers["x-appwrite-id"] = uploadId
193+
}
194+
totalSize := fileInfo.Size()
195+
start := offset
196+
end := offset + clt.chunkSize - 1
197+
if end > totalSize {
198+
end = totalSize
199+
}
200+
headers["content-range"] = fmt.Sprintf("bytes %d-%d/%d", start, end, totalSize)
201+
result, err = clt.Call("POST", url, headers, params)
202+
if err != nil {
203+
return nil, err
204+
}
183205

184-
err = writer.Close()
185-
if err != nil {
186-
return nil, err
206+
uploadId, _ = result.Result.(map[string]interface{})["$id"].(string)
187207
}
188-
189-
req, err := http.NewRequest("POST", uri, body)
190-
clt.AddHeader("content-type", writer.FormDataContentType())
191-
return req, err
208+
return result, nil
192209
}
193210

194211
// Call an API using Client
@@ -218,10 +235,53 @@ func (clt *Client) Call(method string, path string, headers map[string]interface
218235
if !isPost {
219236
return nil, errors.New("fileupload needs POST Request")
220237
}
221-
req, err = clt.newfileUploadRequest(urlPath, params, fileNameKey)
238+
var body bytes.Buffer
239+
writer := multipart.NewWriter(&body)
240+
if val, ok := params[fileNameKey]; ok {
241+
if file, ok := val.(InputFile); ok {
242+
fileName := file.Name
243+
fileData := file.Data
244+
fw, err := writer.CreateFormFile(fileNameKey, fileName)
245+
if err != nil {
246+
return nil, err
247+
}
248+
_, err = io.Copy(fw, bytes.NewReader(fileData))
249+
if err != nil {
250+
return nil, err
251+
}
252+
delete(params, fileNameKey)
253+
} else {
254+
return nil, errors.New("invalid input file")
255+
}
256+
}
257+
for key, val := range params {
258+
rt := reflect.TypeOf(val)
259+
switch rt.Kind() {
260+
case reflect.Array:
261+
case reflect.Slice:
262+
arr := reflect.ValueOf(val)
263+
for i := 0; i < arr.Len(); i++ {
264+
err = writer.WriteField(fmt.Sprintf("%s[]", key), toString(arr.Index(i)))
265+
if err != nil {
266+
return nil, err
267+
}
268+
}
269+
default:
270+
err = writer.WriteField(key, toString(val))
271+
if err != nil {
272+
return nil, err
273+
}
274+
}
275+
}
276+
err = writer.Close()
277+
if err != nil {
278+
return nil, err
279+
}
280+
req, err = http.NewRequest(method, urlPath, &body)
222281
if err != nil {
223282
return nil, err
224283
}
284+
headers["content-type"] = writer.FormDataContentType()
225285
} else {
226286
if !isGet {
227287
var reqBody *strings.Reader
@@ -385,4 +445,4 @@ func toString(arg interface{}) string {
385445
default:
386446
return fmt.Sprintf("%s", v)
387447
}
388-
}
448+
}

templates/go/inputFile.go.twig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package {{ spec.title | caseLower }}
2+
3+
type InputFile struct {
4+
Name string
5+
Path string
6+
Data []byte
7+
}
8+
9+
func (inp *InputFile) FromPath(path string, name string) {
10+
inp.Path = path
11+
inp.Name = name
12+
}

0 commit comments

Comments
 (0)