Skip to content

Commit 99954a8

Browse files
committed
chore: Finished upload updates
1 parent 90c5c04 commit 99954a8

4 files changed

Lines changed: 228 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ pinata
66
gon.hcl
77
.DS_Store
88
Makefile
9+
main
10+
test

internal/types/types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,13 @@ type KeyItem struct {
173173
CreatedAt string `json:"createdAt"`
174174
UpdatedAt string `json:"updatedAt"`
175175
}
176+
177+
type PinataOptions struct {
178+
CidVersion int `json:"cidVersion"`
179+
GroupId string `json:"groupId,omitempty"`
180+
}
181+
182+
type PinataMetadata struct {
183+
Name string `json:"name"`
184+
KeyValues map[string]string `json:"keyvalues,omitempty"`
185+
}

internal/upload/uploads.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
299431
func 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+
365581
func pathsFinder(filePath string, stats os.FileInfo) ([]string, error) {
366582
var err error
367583
files := make([]string, 0)

main

-13 MB
Binary file not shown.

0 commit comments

Comments
 (0)