@@ -2,6 +2,7 @@ package kwlib
22
33import (
44 "bytes"
5+ "context"
56 "crypto/tls"
67 "encoding/json"
78 "errors"
@@ -18,6 +19,8 @@ import (
1819 "github.com/rs/zerolog"
1920)
2021
22+ const uploadChunkSize = 8 << 20 // 8 MiB
23+
2124func NewClientFactory (server , agentString string , insecure bool ) * APIClientFactory {
2225 transport := & http.Transport {
2326 Proxy : http .ProxyFromEnvironment ,
@@ -36,28 +39,35 @@ func NewClientFactory(server, agentString string, insecure bool) *APIClientFacto
3639 // #nosec
3740 transport .TLSClientConfig = & tls.Config {InsecureSkipVerify : true }
3841 }
42+
43+ uploadTransport := transport .Clone ()
44+ uploadTransport .ResponseHeaderTimeout = 30 * time .Second
45+
3946 return & APIClientFactory {
40- server : server ,
41- agentString : agentString ,
42- httpClient : & http.Client {Transport : transport , Timeout : 15 * time .Second },
47+ server : server ,
48+ agentString : agentString ,
49+ httpClient : & http.Client {Transport : transport , Timeout : 15 * time .Second },
50+ uploadClient : & http.Client {Transport : uploadTransport },
4351 }
4452}
4553
4654type APIClientFactory struct {
47- server string
48- agentString string
49- httpClient * http.Client
55+ server string
56+ agentString string
57+ httpClient * http.Client
58+ uploadClient * http.Client
5059}
5160
5261type APIClient struct {
53- server string
54- agentString string
55- logger * zerolog.Logger
56- host string
57- token string
58- requestId string
59- remoteAddr string
60- httpClient * http.Client
62+ server string
63+ agentString string
64+ logger * zerolog.Logger
65+ host string
66+ token string
67+ requestId string
68+ remoteAddr string
69+ httpClient * http.Client
70+ uploadClient * http.Client
6171}
6272
6373func decodeJSON (body io.ReadCloser , out any ) error {
@@ -67,13 +77,14 @@ func decodeJSON(body io.ReadCloser, out any) error {
6777
6878func (f * APIClientFactory ) Build (host , requestId , remoteAddr , token string , l * zerolog.Logger ) * APIClient {
6979 return & APIClient {
70- token : token ,
71- server : f .server ,
72- host : host ,
73- logger : l ,
74- requestId : requestId ,
75- remoteAddr : remoteAddr ,
76- httpClient : f .httpClient ,
80+ token : token ,
81+ server : f .server ,
82+ host : host ,
83+ logger : l ,
84+ requestId : requestId ,
85+ remoteAddr : remoteAddr ,
86+ httpClient : f .httpClient ,
87+ uploadClient : f .uploadClient ,
7788 }
7889}
7990
@@ -94,7 +105,7 @@ func (c *APIClient) GetTopFolders() (*DirectoryInfo, error) {
94105}
95106
96107func (c * APIClient ) GetFolderByID (id string ) (* FileInfo , error ) {
97- request , err := c .NewGetRequest (fmt .Sprintf ("/rest/folders/%s" , id ))
108+ request , err := c .NewGetRequest (fmt .Sprintf ("/rest/folders/%s?with=(permissions) " , id ))
98109 if err != nil {
99110 return nil , err
100111 }
@@ -148,7 +159,7 @@ func (c *APIClient) Search(path string) (*FileInfo, error) {
148159}
149160
150161func (c * APIClient ) GetFileByID (id string ) (* FileInfo , error ) {
151- request , err := c .NewGetRequest (fmt .Sprintf ("/rest/files/%s" , id ))
162+ request , err := c .NewGetRequest (fmt .Sprintf ("/rest/files/%s?with=(permissions,lockUser) " , id ))
152163 if err != nil {
153164 return nil , err
154165 }
@@ -194,16 +205,16 @@ func (c *APIClient) GetUser(id string) (*User, error) {
194205 return out , nil
195206}
196207
197- func (c * APIClient ) GetQuotaInfo ( ) (* QuotaInfo , error ) {
198- request , err := c .NewGetRequest ("/rest/quotas" )
208+ func (c * APIClient ) GetFolderQuota ( folderID string ) (* FolderQuota , error ) {
209+ request , err := c .NewGetRequest (fmt . Sprintf ( "/rest/folders/%s/quota" , folderID ) )
199210 if err != nil {
200211 return nil , err
201212 }
202213 response , err := c .SendRequest (request )
203214 if err != nil {
204215 return nil , err
205216 }
206- out := & QuotaInfo {}
217+ out := & FolderQuota {}
207218 if err := decodeJSON (response .Body , out ); err != nil {
208219 return nil , err
209220 }
@@ -251,12 +262,20 @@ func (c *APIClient) CreateFolder(id string, payload CreateDirRequest) (string, e
251262}
252263
253264func (c * APIClient ) InitializeUpload (parentID , name string , size int64 , numberOfChunks int ) (* UploadResult , error ) {
265+ return c .initializeUpload (fmt .Sprintf ("/rest/folders/%s/actions/initiateUpload" , parentID ), name , size , numberOfChunks )
266+ }
267+
268+ func (c * APIClient ) InitializeVersionUpload (fileID , name string , size int64 , numberOfChunks int ) (* UploadResult , error ) {
269+ return c .initializeUpload (fmt .Sprintf ("/rest/files/%s/actions/initiateUpload" , fileID ), name , size , numberOfChunks )
270+ }
271+
272+ func (c * APIClient ) initializeUpload (path , name string , size int64 , numberOfChunks int ) (* UploadResult , error ) {
254273 payload := InitializeUpload {
255274 FileName : name ,
256275 TotalSize : size ,
257276 TotalChunks : numberOfChunks ,
258277 }
259- request , err := c .NewPostRequest (fmt . Sprintf ( "/rest/folders/%s/actions/initiateUpload" , parentID ) , payload )
278+ request , err := c .NewPostRequest (path , payload )
260279 if err != nil {
261280 return nil , err
262281 }
@@ -271,7 +290,20 @@ func (c *APIClient) InitializeUpload(parentID, name string, size int64, numberOf
271290 return out , nil
272291}
273292
274- func (c * APIClient ) UploadChunk (uploadURI , name string , file io.Reader , chunkIndex int , chunk int64 , isLastChunk bool ) (* FileInfo , error ) {
293+ func (c * APIClient ) TerminateUpload (uploadID int64 ) error {
294+ request , err := c .newRequest ("DELETE" , fmt .Sprintf ("/rest/uploads/%d" , uploadID ), nil )
295+ if err != nil {
296+ return err
297+ }
298+ response , err := c .SendRequest (request )
299+ if err != nil {
300+ return err
301+ }
302+ response .Body .Close ()
303+ return nil
304+ }
305+
306+ func (c * APIClient ) UploadChunk (ctx context.Context , uploadURI , name string , file io.Reader , chunkIndex int , chunk int64 , isLastChunk bool ) (* FileInfo , error ) {
275307 body := new (bytes.Buffer )
276308 writer := multipart .NewWriter (body )
277309 part , err := writer .CreateFormFile ("content" , name )
@@ -296,13 +328,14 @@ func (c *APIClient) UploadChunk(uploadURI, name string, file io.Reader, chunkInd
296328 if err != nil {
297329 return nil , err
298330 }
331+ request = request .WithContext (ctx )
299332 request .Header .Set ("Content-Type" , writer .FormDataContentType ())
300333 if isLastChunk {
301334 q := request .URL .Query ()
302335 q .Add ("returnEntity" , "true" )
303336 request .URL .RawQuery = q .Encode ()
304337 }
305- response , err := c .SendRequest ( request )
338+ response , err := c .sendWith ( c . uploadClient , request )
306339 if err != nil {
307340 return nil , err
308341 }
@@ -313,10 +346,121 @@ func (c *APIClient) UploadChunk(uploadURI, name string, file io.Reader, chunkInd
313346 }
314347 return out , nil
315348 }
349+ // drained so the connection can be reused across chunks
350+ _ , _ = io .Copy (io .Discard , response .Body )
316351 response .Body .Close ()
317352 return nil , nil
318353}
319354
355+ func (c * APIClient ) MoveFolder (id , destinationFolderID string ) error {
356+ request , err := c .NewPostRequest (
357+ fmt .Sprintf ("/rest/folders/%s/actions/move" , id ),
358+ MoveFolderRequest {DestinationFolderID : destinationFolderID },
359+ )
360+ if err != nil {
361+ return err
362+ }
363+ _ , err = c .SendRequest (request )
364+ return err
365+ }
366+
367+ // name must be the file's current name: the server requires it even for a version upload.
368+ func (c * APIClient ) UploadFileVersion (ctx context.Context , fileID , name string , body io.Reader , length int64 ) error {
369+ chunks := chunkCount (length )
370+ session , err := c .InitializeVersionUpload (fileID , name , length , chunks )
371+ if err != nil {
372+ return err
373+ }
374+ for i := 0 ; i < chunks ; i ++ {
375+ size := int64 (uploadChunkSize )
376+ if remaining := length - int64 (i )* uploadChunkSize ; remaining < size {
377+ size = remaining
378+ }
379+ if _ , err := c .UploadChunk (ctx , session .URI , name , body , i , size , i == chunks - 1 ); err != nil {
380+ if termErr := c .TerminateUpload (session .ID ); termErr != nil {
381+ c .logger .Warn ().Err (termErr ).Int64 ("uploadID" , session .ID ).Msg ("could not terminate kiteworks upload session" )
382+ }
383+ return err
384+ }
385+ }
386+ return nil
387+ }
388+
389+ func chunkCount (length int64 ) int {
390+ if length <= 0 {
391+ return 1
392+ }
393+ return int ((length + uploadChunkSize - 1 ) / uploadChunkSize )
394+ }
395+
396+ func (c * APIClient ) GetFileVersions (fileID string ) ([]Version , error ) {
397+ req , err := c .NewGetRequest (fmt .Sprintf ("/rest/files/%s/versions" , fileID ))
398+ if err != nil {
399+ return nil , err
400+ }
401+ resp , err := c .SendRequest (req )
402+ if err != nil {
403+ return nil , err
404+ }
405+ out := & VersionList {}
406+ if err := decodeJSON (resp .Body , out ); err != nil {
407+ return nil , err
408+ }
409+ return out .Data , nil
410+ }
411+
412+ func (c * APIClient ) DeleteFileVersion (fileID , versionID string ) error {
413+ req , err := c .newRequest ("DELETE" , fmt .Sprintf ("/rest/files/%s/versions/%s" , fileID , versionID ), nil )
414+ if err != nil {
415+ return err
416+ }
417+ _ , err = c .SendRequest (req )
418+ return err
419+ }
420+
421+ func (c * APIClient ) PromoteFileVersion (fileID , versionID string ) error {
422+ req , err := c .newRequest ("POST" , fmt .Sprintf ("/rest/files/%s/versions/%s/actions/promote" , fileID , versionID ), nil )
423+ if err != nil {
424+ return err
425+ }
426+ _ , err = c .SendRequest (req )
427+ return err
428+ }
429+
430+ func (c * APIClient ) GetVersionContents (fileID , versionID string ) (* http.Response , error ) {
431+ req , err := c .NewGetRequest (fmt .Sprintf ("/rest/files/%s/versions/%s/content" , fileID , versionID ))
432+ if err != nil {
433+ return nil , err
434+ }
435+ return c .SendRequest (req )
436+ }
437+
438+ func (c * APIClient ) LockFile (fileID string ) error {
439+ req , err := c .newRequest ("PATCH" , fmt .Sprintf ("/rest/files/%s/actions/lock" , fileID ), nil )
440+ if err != nil {
441+ return err
442+ }
443+ resp , err := c .SendRequest (req )
444+ if err != nil {
445+ return err
446+ }
447+ resp .Body .Close ()
448+ return nil
449+ }
450+
451+ func (c * APIClient ) UnlockFile (fileID string ) error {
452+ req , err := c .newRequest ("PATCH" , fmt .Sprintf ("/rest/files/%s/actions/unlock" , fileID ), nil )
453+ if err != nil {
454+ return err
455+ }
456+ resp , err := c .SendRequest (req )
457+ if err != nil {
458+ return err
459+ }
460+ resp .Body .Close ()
461+ return nil
462+ }
463+
320464func (c * APIClient ) DeleteFolder (id string ) error {
321465 request , err := c .newRequest ("DELETE" , fmt .Sprintf ("/rest/folders/%s" , id ), nil )
322466 if err != nil {
@@ -432,7 +576,11 @@ func (c *APIClient) newRequest(method, path string, body io.Reader) (*http.Reque
432576}
433577
434578func (c * APIClient ) SendRequest (req * http.Request ) (* http.Response , error ) {
435- response , err := c .httpClient .Do (req )
579+ return c .sendWith (c .httpClient , req )
580+ }
581+
582+ func (c * APIClient ) sendWith (client * http.Client , req * http.Request ) (* http.Response , error ) {
583+ response , err := client .Do (req )
436584 if err != nil {
437585 c .logger .Debug ().Str ("method" , req .Method ).Str ("path" , req .URL .String ()).Err (err ).Msg ("kiteworks API call errored" )
438586 return nil , err
0 commit comments