@@ -34,6 +34,21 @@ func Upload(filePath string, groupId string, name string, verbose bool, network
3434 return types.UploadResponse {}, err
3535 }
3636
37+ if stats .IsDir () {
38+ // Check if network is private and return error if so
39+ networkParam , err := config .GetNetworkParam (network )
40+ if err != nil {
41+ return types.UploadResponse {}, err
42+ }
43+
44+ if networkParam == "private" {
45+ return types.UploadResponse {}, errors .New ("folders are not supported on the private network" )
46+ }
47+
48+ // For folders, we use a different API endpoint
49+ return folderUpload (filePath , groupId , name , verbose )
50+ }
51+
3752 if stats .Size () > MAX_SIZE_REGULAR_UPLOAD {
3853 return uploadWithTUS (filePath , groupId , name , verbose , stats , network )
3954 }
@@ -296,6 +311,123 @@ func uploadWithTUS(filePath string, groupId string, name string, verbose bool, s
296311 return response , nil
297312}
298313
314+ func folderUpload (filePath string , groupId string , name string , verbose bool ) (types.UploadResponse , error ) {
315+ jwt , err := common .FindToken ()
316+ if err != nil {
317+ return types.UploadResponse {}, err
318+ }
319+
320+ stats , err := os .Stat (filePath )
321+ if os .IsNotExist (err ) {
322+ return types.UploadResponse {}, errors .Join (err , errors .New ("folder does not exist" ))
323+ }
324+
325+ files , err := pathsFinder (filePath , stats )
326+ if err != nil {
327+ return types.UploadResponse {}, err
328+ }
329+
330+ body := & bytes.Buffer {}
331+ contentType , err := createPinataMultipartRequest (filePath , files , body , stats , groupId , name )
332+ if err != nil {
333+ return types.UploadResponse {}, err
334+ }
335+
336+ var requestBody io.Reader
337+ if ! verbose {
338+ requestBody = body
339+ } else {
340+ totalSize := int64 (body .Len ())
341+ fmt .Printf ("Uploading folder %s (%s)\n " , stats .Name (), formatSize (int (totalSize )))
342+ requestBody = newProgressReader (body , totalSize )
343+ }
344+
345+ // Use the pinning endpoint for folders
346+ url := fmt .Sprintf ("https://%s/pinning/pinFileToIPFS" , cliConfig .GetAPIHost ())
347+ req , err := http .NewRequest ("POST" , url , requestBody )
348+ if err != nil {
349+ return types.UploadResponse {}, errors .Join (err , errors .New ("failed to create the request" ))
350+ }
351+ req .Header .Set ("Authorization" , "Bearer " + string (jwt ))
352+ req .Header .Set ("content-type" , contentType )
353+
354+ client := & http.Client {}
355+ resp , err := client .Do (req )
356+ if err != nil {
357+ return types.UploadResponse {}, errors .Join (err , errors .New ("failed to send the request" ))
358+ }
359+ if resp .StatusCode != 200 {
360+ respBody , _ := io .ReadAll (resp .Body )
361+ return types.UploadResponse {}, fmt .Errorf ("server returned an error %d: %s" , resp .StatusCode , string (respBody ))
362+ }
363+
364+ defer resp .Body .Close ()
365+
366+ // Parse the pinning API response
367+ var pinningResponse struct {
368+ ID string `json:"ID"`
369+ Name string `json:"Name"`
370+ IpfsHash string `json:"IpfsHash"`
371+ PinSize int `json:"PinSize"`
372+ Timestamp string `json:"Timestamp"`
373+ NumberOfFiles int `json:"NumberOfFiles"`
374+ MimeType string `json:"MimeType"`
375+ GroupId * string `json:"GroupId"`
376+ Keyvalues map [string ]string `json:"Keyvalues"`
377+ IsDuplicate bool `json:"isDuplicate"`
378+ }
379+
380+ err = json .NewDecoder (resp .Body ).Decode (& pinningResponse )
381+ if err != nil {
382+ return types.UploadResponse {}, err
383+ }
384+
385+ // Map the pinning API response to our UploadResponse format following the TypeScript mapping
386+ response := types.UploadResponse {
387+ Data : struct {
388+ Id string `json:"id"`
389+ Name string `json:"name"`
390+ Cid string `json:"cid"`
391+ Size int `json:"size"`
392+ CreatedAt string `json:"created_at"`
393+ NumberOfFiles int `json:"number_of_files"`
394+ MimeType string `json:"mime_type"`
395+ GroupId * string `json:"group_id"`
396+ KeyValues map [string ]string `json:"keyvalues"`
397+ Vectorized bool `json:"vectorized"`
398+ Network string `json:"network"`
399+ IsDuplicate bool `json:"is_duplicate,omitempty"`
400+ }{
401+ Id : pinningResponse .ID ,
402+ Name : pinningResponse .Name ,
403+ Cid : pinningResponse .IpfsHash ,
404+ Size : pinningResponse .PinSize ,
405+ CreatedAt : pinningResponse .Timestamp ,
406+ NumberOfFiles : pinningResponse .NumberOfFiles ,
407+ MimeType : pinningResponse .MimeType ,
408+ GroupId : pinningResponse .GroupId ,
409+ KeyValues : pinningResponse .Keyvalues ,
410+ Vectorized : false ,
411+ Network : "public" ,
412+ IsDuplicate : pinningResponse .IsDuplicate ,
413+ },
414+ }
415+
416+ // If groupId is specified, set it in the response
417+ if groupId != "" {
418+ response .Data .GroupId = & groupId
419+ }
420+
421+ formattedJSON , err := json .MarshalIndent (response .Data , "" , " " )
422+ if err != nil {
423+ return types.UploadResponse {}, errors .New ("failed to format JSON" )
424+ }
425+
426+ fmt .Println (string (formattedJSON ))
427+
428+ return response , nil
429+ }
430+
299431func createMultipartRequest (filePath string , files []string , body io.Writer , stats os.FileInfo , groupId string , name string , network string ) (string , error ) {
300432 contentType := ""
301433 writer := multipart .NewWriter (body )
@@ -362,6 +494,90 @@ func createMultipartRequest(filePath string, files []string, body io.Writer, sta
362494 return contentType , nil
363495}
364496
497+ func createPinataMultipartRequest (filePath string , files []string , body io.Writer , stats os.FileInfo , groupId string , name string ) (string , error ) {
498+ contentType := ""
499+ writer := multipart .NewWriter (body )
500+
501+ // Add files to the multipart request
502+ fileIsASingleFile := ! stats .IsDir ()
503+ for _ , f := range files {
504+ file , err := os .Open (f )
505+ if err != nil {
506+ return contentType , err
507+ }
508+ defer func (file * os.File ) {
509+ err := file .Close ()
510+ if err != nil {
511+ log .Fatal ("could not close file" )
512+ }
513+ }(file )
514+
515+ var part io.Writer
516+ if fileIsASingleFile {
517+ part , err = writer .CreateFormFile ("file" , filepath .Base (f ))
518+ } else {
519+ relPath , _ := filepath .Rel (filePath , f )
520+ part , err = writer .CreateFormFile ("file" , filepath .Join (stats .Name (), relPath ))
521+ }
522+ if err != nil {
523+ return contentType , err
524+ }
525+ _ , err = io .Copy (part , file )
526+ if err != nil {
527+ return contentType , err
528+ }
529+ }
530+
531+ // Create and add PinataOptions
532+ pinataOptions := types.PinataOptions {
533+ CidVersion : 1 , // Default CID version
534+ }
535+
536+ // Add groupId to options if provided
537+ if groupId != "" {
538+ pinataOptions .GroupId = groupId
539+ }
540+
541+ optionsBytes , err := json .Marshal (pinataOptions )
542+ if err != nil {
543+ return contentType , err
544+ }
545+
546+ err = writer .WriteField ("pinataOptions" , string (optionsBytes ))
547+ if err != nil {
548+ return contentType , err
549+ }
550+
551+ // Create and add PinataMetadata
552+ nameToUse := stats .Name ()
553+ if name != "nil" {
554+ nameToUse = name
555+ }
556+
557+ pinataMetadata := types.PinataMetadata {
558+ Name : nameToUse ,
559+ KeyValues : make (map [string ]string ), // Empty keyvalues for now
560+ }
561+
562+ metadataBytes , err := json .Marshal (pinataMetadata )
563+ if err != nil {
564+ return contentType , err
565+ }
566+
567+ err = writer .WriteField ("pinataMetadata" , string (metadataBytes ))
568+ if err != nil {
569+ return contentType , err
570+ }
571+
572+ err = writer .Close ()
573+ if err != nil {
574+ return contentType , err
575+ }
576+
577+ contentType = writer .FormDataContentType ()
578+ return contentType , nil
579+ }
580+
365581func pathsFinder (filePath string , stats os.FileInfo ) ([]string , error ) {
366582 var err error
367583 files := make ([]string , 0 )
0 commit comments