@@ -15,6 +15,7 @@ import (
1515 larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
1616
1717 "github.com/larksuite/cli/errs"
18+ "github.com/larksuite/cli/internal/client"
1819 "github.com/larksuite/cli/internal/output"
1920)
2021
@@ -57,6 +58,7 @@ type DriveMediaMultipartUploadConfig struct {
5758 Reader io.Reader
5859}
5960
61+ // Deprecated: use UploadDriveMediaAllTyped for typed error envelopes.
6062func UploadDriveMediaAll (runtime * RuntimeContext , cfg DriveMediaUploadAllConfig ) (string , error ) {
6163 var fileReader io.Reader
6264 if cfg .Reader != nil {
@@ -98,6 +100,52 @@ func UploadDriveMediaAll(runtime *RuntimeContext, cfg DriveMediaUploadAllConfig)
98100 return ExtractDriveMediaUploadFileToken (data , driveMediaUploadAllAction )
99101}
100102
103+ // UploadDriveMediaAllTyped is the typed-error counterpart of
104+ // UploadDriveMediaAll: file-open failures surface as typed validation errors,
105+ // transport failures as typed network errors, and API failures are classified
106+ // via ClassifyAPIResponse so subtype / code / log_id survive on the error.
107+ func UploadDriveMediaAllTyped (runtime * RuntimeContext , cfg DriveMediaUploadAllConfig ) (string , error ) {
108+ var fileReader io.Reader
109+ if cfg .Reader != nil {
110+ fileReader = cfg .Reader
111+ } else {
112+ f , err := runtime .FileIO ().Open (cfg .FilePath )
113+ if err != nil {
114+ return "" , WrapInputStatErrorTyped (err )
115+ }
116+ defer f .Close ()
117+ fileReader = f
118+ }
119+
120+ fd := larkcore .NewFormdata ()
121+ fd .AddField ("file_name" , cfg .FileName )
122+ fd .AddField ("parent_type" , cfg .ParentType )
123+ fd .AddField ("size" , fmt .Sprintf ("%d" , cfg .FileSize ))
124+ if cfg .ParentNode != nil {
125+ fd .AddField ("parent_node" , * cfg .ParentNode )
126+ }
127+ if cfg .Extra != "" {
128+ fd .AddField ("extra" , cfg .Extra )
129+ }
130+ fd .AddFile ("file" , fileReader )
131+
132+ apiResp , err := runtime .DoAPI (& larkcore.ApiReq {
133+ HttpMethod : http .MethodPost ,
134+ ApiPath : "/open-apis/drive/v1/medias/upload_all" ,
135+ Body : fd ,
136+ }, larkcore .WithFileUpload ())
137+ if err != nil {
138+ return "" , prefixDriveMediaUploadProblem (client .WrapDoAPIError (err ), driveMediaUploadAllAction )
139+ }
140+
141+ data , err := runtime .ClassifyAPIResponse (apiResp )
142+ if err != nil {
143+ return "" , prefixDriveMediaUploadProblem (err , driveMediaUploadAllAction )
144+ }
145+ return extractDriveMediaUploadFileTokenTyped (data , driveMediaUploadAllAction )
146+ }
147+
148+ // Deprecated: use UploadDriveMediaMultipartTyped for typed error envelopes.
101149func UploadDriveMediaMultipart (runtime * RuntimeContext , cfg DriveMediaMultipartUploadConfig ) (string , error ) {
102150 // upload_prepare expects parent_node to be present even when the caller wants
103151 // the service default/root behavior, so multipart callers pass an explicit
@@ -130,6 +178,43 @@ func UploadDriveMediaMultipart(runtime *RuntimeContext, cfg DriveMediaMultipartU
130178 return finishDriveMediaMultipartUpload (runtime , session .UploadID , session .BlockNum )
131179}
132180
181+ // UploadDriveMediaMultipartTyped is the typed-error counterpart of
182+ // UploadDriveMediaMultipart: prepare/finish failures come back typed from
183+ // CallAPITyped, malformed session plans surface as invalid-response internal
184+ // errors, and per-part transport/API failures are classified the same way as
185+ // UploadDriveMediaAllTyped.
186+ func UploadDriveMediaMultipartTyped (runtime * RuntimeContext , cfg DriveMediaMultipartUploadConfig ) (string , error ) {
187+ // upload_prepare expects parent_node to be present even when the caller wants
188+ // the service default/root behavior, so multipart callers pass an explicit
189+ // string instead of relying on field omission like upload_all does.
190+ prepareBody := map [string ]interface {}{
191+ "file_name" : cfg .FileName ,
192+ "parent_type" : cfg .ParentType ,
193+ "parent_node" : cfg .ParentNode ,
194+ "size" : cfg .FileSize ,
195+ }
196+ if cfg .Extra != "" {
197+ prepareBody ["extra" ] = cfg .Extra
198+ }
199+
200+ data , err := runtime .CallAPITyped ("POST" , "/open-apis/drive/v1/medias/upload_prepare" , nil , prepareBody )
201+ if err != nil {
202+ return "" , err
203+ }
204+
205+ session , err := parseDriveMediaMultipartUploadSessionTyped (data )
206+ if err != nil {
207+ return "" , err
208+ }
209+ fmt .Fprintf (runtime .IO ().ErrOut , "Multipart upload initialized: %d chunks x %s\n " , session .BlockNum , FormatSize (session .BlockSize ))
210+
211+ if err = uploadDriveMediaMultipartPartsTyped (runtime , cfg , session ); err != nil {
212+ return "" , err
213+ }
214+
215+ return finishDriveMediaMultipartUploadTyped (runtime , session .UploadID , session .BlockNum )
216+ }
217+
133218func ParseDriveMediaMultipartUploadSession (data map [string ]interface {}) (DriveMediaMultipartUploadSession , error ) {
134219 // The backend chooses both chunk size and chunk count. Validate them once so
135220 // the streaming loop can follow the returned plan without re-checking shape.
@@ -280,3 +365,122 @@ func finishDriveMediaMultipartUpload(runtime *RuntimeContext, uploadID string, b
280365 }
281366 return ExtractDriveMediaUploadFileToken (data , driveMediaUploadFinishAction )
282367}
368+
369+ // prefixDriveMediaUploadProblem prepends the upload action to a typed error's
370+ // message so callers see which upload step failed. Non-typed errors are
371+ // returned unchanged.
372+ func prefixDriveMediaUploadProblem (err error , action string ) error {
373+ if p , ok := errs .ProblemOf (err ); ok {
374+ p .Message = action + ": " + p .Message
375+ }
376+ return err
377+ }
378+
379+ // parseDriveMediaMultipartUploadSessionTyped validates the upload_prepare
380+ // session plan like ParseDriveMediaMultipartUploadSession, but reports a
381+ // malformed plan as a typed invalid-response internal error.
382+ func parseDriveMediaMultipartUploadSessionTyped (data map [string ]interface {}) (DriveMediaMultipartUploadSession , error ) {
383+ session := DriveMediaMultipartUploadSession {
384+ UploadID : GetString (data , "upload_id" ),
385+ BlockSize : int64 (GetFloat (data , "block_size" )),
386+ BlockNum : int (GetFloat (data , "block_num" )),
387+ }
388+ if session .UploadID == "" {
389+ return DriveMediaMultipartUploadSession {}, errs .NewInternalError (errs .SubtypeInvalidResponse , "upload prepare failed: no upload_id returned" )
390+ }
391+ if session .BlockSize <= 0 {
392+ return DriveMediaMultipartUploadSession {}, errs .NewInternalError (errs .SubtypeInvalidResponse , "upload prepare failed: invalid block_size returned" )
393+ }
394+ if session .BlockNum <= 0 {
395+ return DriveMediaMultipartUploadSession {}, errs .NewInternalError (errs .SubtypeInvalidResponse , "upload prepare failed: invalid block_num returned" )
396+ }
397+ return session , nil
398+ }
399+
400+ // extractDriveMediaUploadFileTokenTyped mirrors ExtractDriveMediaUploadFileToken
401+ // with a typed invalid-response internal error for a missing file_token.
402+ func extractDriveMediaUploadFileTokenTyped (data map [string ]interface {}, action string ) (string , error ) {
403+ fileToken := GetString (data , "file_token" )
404+ if fileToken == "" {
405+ return "" , errs .NewInternalError (errs .SubtypeInvalidResponse , "%s: no file_token returned" , action )
406+ }
407+ return fileToken , nil
408+ }
409+
410+ // uploadDriveMediaMultipartPartsTyped mirrors uploadDriveMediaMultipartParts
411+ // with typed errors for file-open, file-read, and per-part upload failures.
412+ func uploadDriveMediaMultipartPartsTyped (runtime * RuntimeContext , cfg DriveMediaMultipartUploadConfig , session DriveMediaMultipartUploadSession ) error {
413+ var r io.Reader
414+ if cfg .Reader != nil {
415+ r = cfg .Reader
416+ } else {
417+ f , err := runtime .FileIO ().Open (cfg .FilePath )
418+ if err != nil {
419+ return WrapInputStatErrorTyped (err )
420+ }
421+ defer f .Close ()
422+ r = f
423+ }
424+
425+ maxInt := int64 (^ uint (0 ) >> 1 )
426+ bufferSize := session .BlockSize
427+ if bufferSize <= 0 || bufferSize > maxInt {
428+ return errs .NewInternalError (errs .SubtypeInvalidResponse , "upload prepare failed: invalid block_size returned" )
429+ }
430+ buffer := make ([]byte , int (bufferSize ))
431+ remaining := cfg .FileSize
432+ // Follow the server-declared block plan exactly; upload_finish expects the
433+ // same block count returned by upload_prepare.
434+ for seq := 0 ; seq < session .BlockNum ; seq ++ {
435+ chunkSize := session .BlockSize
436+ if remaining > 0 && chunkSize > remaining {
437+ chunkSize = remaining
438+ }
439+
440+ n , readErr := io .ReadFull (r , buffer [:int (chunkSize )])
441+ if readErr != nil {
442+ return WrapInputStatErrorTyped (readErr )
443+ }
444+
445+ if err := uploadDriveMediaMultipartPartTyped (runtime , session .UploadID , seq , buffer [:n ]); err != nil {
446+ return err
447+ }
448+ fmt .Fprintf (runtime .IO ().ErrOut , " Block %d/%d uploaded (%s)\n " , seq + 1 , session .BlockNum , FormatSize (int64 (n )))
449+ remaining -= int64 (n )
450+ }
451+
452+ return nil
453+ }
454+
455+ func uploadDriveMediaMultipartPartTyped (runtime * RuntimeContext , uploadID string , seq int , chunk []byte ) error {
456+ fd := larkcore .NewFormdata ()
457+ fd .AddField ("upload_id" , uploadID )
458+ fd .AddField ("seq" , fmt .Sprintf ("%d" , seq ))
459+ fd .AddField ("size" , fmt .Sprintf ("%d" , len (chunk )))
460+ fd .AddFile ("file" , bytes .NewReader (chunk ))
461+
462+ apiResp , err := runtime .DoAPI (& larkcore.ApiReq {
463+ HttpMethod : http .MethodPost ,
464+ ApiPath : "/open-apis/drive/v1/medias/upload_part" ,
465+ Body : fd ,
466+ }, larkcore .WithFileUpload ())
467+ if err != nil {
468+ return prefixDriveMediaUploadProblem (client .WrapDoAPIError (err ), driveMediaUploadPartAction )
469+ }
470+
471+ if _ , err := runtime .ClassifyAPIResponse (apiResp ); err != nil {
472+ return prefixDriveMediaUploadProblem (err , driveMediaUploadPartAction )
473+ }
474+ return nil
475+ }
476+
477+ func finishDriveMediaMultipartUploadTyped (runtime * RuntimeContext , uploadID string , blockNum int ) (string , error ) {
478+ data , err := runtime .CallAPITyped ("POST" , "/open-apis/drive/v1/medias/upload_finish" , nil , map [string ]interface {}{
479+ "upload_id" : uploadID ,
480+ "block_num" : blockNum ,
481+ })
482+ if err != nil {
483+ return "" , err
484+ }
485+ return extractDriveMediaUploadFileTokenTyped (data , driveMediaUploadFinishAction )
486+ }
0 commit comments