From 51f2d5acbc365edf498d16df6cd9cdca4392316f Mon Sep 17 00:00:00 2001 From: Steve Date: Mon, 15 Jan 2024 20:24:13 -0500 Subject: [PATCH] init --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 6 ++ .goreleaser.yaml | 100 ++++++++++++++++++ Delete.go | 39 +++++++ ListFiles.go | 61 +++++++++++ PinByCID.go | 64 ++++++++++++ README.md | 107 +++++++++++++++++++ Requests.go | 64 ++++++++++++ SaveJWT.go | 59 +++++++++++ Upload.go | 266 +++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 19 ++++ go.sum | 35 +++++++ install.sh | 39 +++++++ main.go | 237 +++++++++++++++++++++++++++++++++++++++++ 14 files changed, 1096 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 .goreleaser.yaml create mode 100644 Delete.go create mode 100644 ListFiles.go create mode 100644 PinByCID.go create mode 100644 README.md create mode 100644 Requests.go create mode 100644 SaveJWT.go create mode 100644 Upload.go create mode 100644 go.mod create mode 100644 go.sum create mode 100755 install.sh create mode 100644 main.go diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f691b82ef673601b15c38482352d1adbb1c27741 GIT binary patch literal 6148 zcmeHK&5qMB5FWQ->rz$f0i+%yajitlb{EiILbm$?1i=ANNl4X3o5oR-l0#J~XZA_< z4fX~43i~LWnXz5eAASxsyf6QOz1BdO^T znq>5ipvcl776JC}j=ar)VZbm@H3sPHS`b168RWa?H%Q|s%lrL%R;ybNP8z(yTl}Lp z6NQ)i`7(3;#e3?#5h4z@wjaEWCdIh(=u#xPA0?B842Z%Bx_o#SC7~!>u}s2D#(HXk zH+gg1>8@5Up7ne7^TFA=XRlt8Jv<+-*G>NT$w#F^opz$N0 zFWw@FMKZ-P@;`wT^Gv{p9Q=ypdis~S?&TCu2i|8fPq$F6pMkpScIf@T`gQ%kA7pxl0mHz5#Q?KL-pIw2^xJxr9KE$R>L*kZ@+%d}5Nz~w hEDd^!yQord&L#uV(O4?P5ft+yAZaj-Vc?%K@EsdNrwIT6 literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1227ac9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +dist/ +pinata-go-cli +pinata +.idea +.vscode +gon.hcl diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..43c7a42 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,100 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + # you may remove this if you don't need go generate + - go generate ./... +builds: + + - binary: pinata + + id: pinata + + env: + - CGO_ENABLED=0 + goos: + - linux + - windows + goarch: + - amd64 + + - binary: pinata + + id: pinata-macos + + goos: + - darwin + + goarch: + - amd64 + + hooks: + post: gon gon.hcl + +universal_binaries: + - id: pinata + + - ids: + - build1 + - build2 + # Universal binary name. + # + # You will want to change this if you have multiple builds! + # + # Default: '{{ .ProjectName }}' + # Templates: allowed + name_template: "{{.ProjectName}}_{{.Version}}" + + # Whether to remove the previous single-arch binaries from the artifact list. + # If left as false, your end release might have both several macOS archives: + # amd64, arm64 and all. + replace: true + + # Set the modified timestamp on the output binary, typically + # you would do this to ensure a build was reproducible. Pass + # empty string to skip modifying the output. + # + # Templates: allowed. + # Since: v1.20. + mod_timestamp: "{{ .CommitTimestamp }}" + + # Hooks can be used to customize the final binary, + # for example, to run generators. + # + # Templates: allowed + # hooks: + # pre: rice embed-go + # post: ./script.sh {{ .Path }} + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of uname. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip + files: + - install.sh + - README.md + +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" +# The lines beneath this are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj diff --git a/Delete.go b/Delete.go new file mode 100644 index 0000000..10286a9 --- /dev/null +++ b/Delete.go @@ -0,0 +1,39 @@ +package main + +import ( + "errors" + "fmt" + "net/http" +) + +func Delete(cid string) error { + jwt, err := findToken() + if err != nil { + return err + } + host := GetHost() + url := fmt.Sprintf("https://%s/pinning/unpin/%s", host, cid) + + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return errors.Join(err, errors.New("failed to create the request")) + } + req.Header.Set("Authorization", "Bearer "+string(jwt)) + req.Header.Set("content-type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return errors.Join(err, errors.New("failed to send the request")) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("server Returned an error %d, check CID", resp.StatusCode) + } + + fmt.Println("File Deleted") + + return nil + +} diff --git a/ListFiles.go b/ListFiles.go new file mode 100644 index 0000000..4cc754c --- /dev/null +++ b/ListFiles.go @@ -0,0 +1,61 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" +) + +func ListFiles(amount string, cid string, name string, status string, offset string) (ListResponse, error) { + jwt, err := findToken() + if err != nil { + return ListResponse{}, err + } + host := GetHost() + url := fmt.Sprintf("https://%s/data/pinList?includesCount=false&pageLimit=%s&status=%s", host, amount, status) + + if cid != "null" { + url += "&hashContains=" + cid + } + if name != "null" { + url += "&metadata[name]=" + name + } + if offset != "null" { + url += "&pageOffset=" + offset + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return ListResponse{}, errors.Join(err, errors.New("failed to create the request")) + } + req.Header.Set("Authorization", "Bearer "+string(jwt)) + req.Header.Set("content-type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return ListResponse{}, errors.Join(err, errors.New("failed to send the request")) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return ListResponse{}, fmt.Errorf("server Returned an error %d", resp.StatusCode) + } + + var response ListResponse + + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return ListResponse{}, err + } + formattedJSON, err := json.MarshalIndent(response.Rows, "", " ") + if err != nil { + return ListResponse{}, errors.New("failed to format JSON") + } + + fmt.Println(string(formattedJSON)) + + return response, nil + +} diff --git a/PinByCID.go b/PinByCID.go new file mode 100644 index 0000000..55f194d --- /dev/null +++ b/PinByCID.go @@ -0,0 +1,64 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" +) + +func PinByCID(cid string, name string) (PinByCIDResponse, error) { + jwt, err := findToken() + if err != nil { + return PinByCIDResponse{}, err + } + host := GetHost() + url := fmt.Sprintf("https://%s/pinning/pinByHash", host) + + // Create the request body + requestBody := map[string]interface{}{ + "hashToPin": cid, + "pinataMetadata": map[string]string{ + "name": name, + }, + } + jsonBody, err := json.Marshal(requestBody) + if err != nil { + return PinByCIDResponse{}, errors.New("failed to create JSON body") + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody)) + if err != nil { + return PinByCIDResponse{}, errors.New("failed to create the request") + } + req.Header.Set("Authorization", "Bearer "+string(jwt)) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return PinByCIDResponse{}, errors.New("failed to send the request") + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return PinByCIDResponse{}, fmt.Errorf("server returned an error %s", resp.Status) + } + + var response PinByCIDResponse + + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return PinByCIDResponse{}, err + } + + fmt.Println("Pin by CID Request Started") + fmt.Println("Request ID:", response.Id) + fmt.Println("CID:", response.CID) + fmt.Println("Status:", response.Status) + fmt.Println("Name:", response.Name) + + return response, nil +} + diff --git a/README.md b/README.md new file mode 100644 index 0000000..f9e619d --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# Pinata Go CLI + +Welcome to the Pinata Go CLI! This is still in active development so please let us know if you have any questions! :) + +## Installation + +> Note - If you are on Windows please use WSL when installing, as the current implementation will not work natively on Windows OS. + +We are currently working on the build flow for binaries to make installation easier, but for now we recommend building from source. + +To do this make sure you have [Go](https://go.dev/) installed on your computer and the following command returns a version: + +```shell +go version +``` + +Then paste and run the following into your terminal: + +```shell +git clone https://github.com/stevedylandev/pinata-go-cli && cd pinata-go-cli && go install . +``` + +## Usage + +The Pinata CLI is equipped with the majortiry of features on the Pinata API. + +### `auth` - Authentication + +With the CLI installed you will first need to authenticate it with your [Pinata JWT](https://docs.pinata.cloud/docs/api-keys) + +```shell +pinata auth +``` + +### `upload` - Uploads + +After authentication you can now upload using the `upload` command or `u` for short, then pass in the path to the file or folder you want to upload. + +```shell +pinata upload ~/Pictures/somefolder/image.png +``` + +The following flags are also available to set the name or CID version of the upload. + +```shell +--version value, -v value Set desired CID version to either 0 or 1. Default is 1. (default: 1) +--name value, -n value Add a name for the file you are uploading. By default it will use the filename on your system. (default: "nil") + +``` + +### `list` - List Files + +You can list files with the `list` command or the alias `l`. The results are printed in raw JSON to help increase composability. + +```shell +pinata list +``` + +By default it will retrieve the 10 latest files, but with the flags below you can get more results or fine tune your search. + +```shell +--cid value, -c value Search files by CID (default: "null") +--amount value, -a value The number of files you would like to return, default 10 max 1000 (default: "10") +--name value, -n value The name of the file (default: "null") +--status value, -s value Status of the file. Options are 'pinned', 'unpinned', or 'all'. Default: 'pinned' (default: "pinned") +--pageOffset value, -p value Allows you to paginate through files. If your file amount is 10, then you could set the pageOffset to '10' to see the next 10 files. (default: "null") +``` + +### `delete` - Delete Files + +If you ever need to you can delete a file by CID using the `delete` command or alias `d` followed by the file CID. + +```shell +pinata delete QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng +``` + +### `pin` - Pin by CID + +Separate from the `upload` command which uploads files from your machine to Pinata, you can also pin a file already on the IPFS network by using the `pin` command or alias `p` followed by the CID. This will start a pin by CID request which will go into a queue. + +```shell +pinata pin QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng +``` + +To check the queue use the `request` command. + +### `requests` - Pin by CID Requests + +As mentioned in the `pin` command, when you submit an existing CID on IPFS to be pinned to your Pinata account, it goes into a request queue. From here it will go through multiple status'. For more info on these please consult the [documentation](https://docs.pinata.cloud/reference/get_pinning-pinjobs). + +```shell +pinata requests +``` + +You can use flags to help filter requests as well. + +```shell +--cid value, -c value Search pin by CID requests by CID (default: "null") +--status value, -s value Search by status for pin by CID requests. See https://docs.pinata.cloud/reference/get_pinning-pinjobs for more info. (default: "null") +--pageOffset value, -p value Allows you to paginate through requests by number of requests. (default: "null") +``` + +## Contact + +If you have any questions please feel free to reach out to us! + +[team@pinata.cloud](mailto:team@pinata.cloud) diff --git a/Requests.go b/Requests.go new file mode 100644 index 0000000..7a7bf79 --- /dev/null +++ b/Requests.go @@ -0,0 +1,64 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" +) + +func Requests(cid string, status string, offset string) (RequestsResponse, error) { + jwt, err := findToken() + if err != nil { + return RequestsResponse{}, err + } + host := GetHost() + url := fmt.Sprintf("https://%s/pinning/pinJobs?sort=DESC", host) + + if cid != "null" { + url += "&ipfs_pin_hash=" + cid + } + if offset != "null" { + url += "&offset=" + offset + } + if status != "null" { + url += "&status=" + status + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return RequestsResponse{}, errors.Join(err, errors.New("failed to create the request")) + } + req.Header.Set("Authorization", "Bearer "+string(jwt)) + req.Header.Set("content-type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return RequestsResponse{}, errors.Join(err, errors.New("failed to send the request")) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return RequestsResponse{}, fmt.Errorf("server Returned an error %d", resp.StatusCode) + } + + var response RequestsResponse + + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return RequestsResponse{}, err + } + + for i := 0; i < len(response.Rows); i++ { + fmt.Println("Request ID:", response.Rows[i].Id) + fmt.Println("CID:", response.Rows[i].CID) + fmt.Println("Date Started:", response.Rows[i].StartDate) + fmt.Println("Name:", response.Rows[i].Name) + fmt.Println("Status:", response.Rows[i].Status) + fmt.Println() + } + + return response, nil + +} diff --git a/SaveJWT.go b/SaveJWT.go new file mode 100644 index 0000000..6f41b1d --- /dev/null +++ b/SaveJWT.go @@ -0,0 +1,59 @@ +package main + +import ( + "errors" + "fmt" + "net/http" + "os" + "path/filepath" + "time" +) + +func SaveJWT(jwt string) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + p := filepath.Join(home, ".pinata-go-cli") + err = os.WriteFile(p, []byte(jwt), 0600) + if err != nil { + return err + } + host := GetHost() + url := fmt.Sprintf("https://%s/data/testAuthentication", host) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + + req.Header.Set("Authorization", "Bearer "+jwt) + + client := &http.Client{ + Timeout: time.Duration(time.Second * 3), + } + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + status := resp.StatusCode + if status != 200 { + return errors.New("Authentication failed, make sure you are using the Pinata JWT") + } + + fmt.Println("Authentication Successful!") + return nil +} + +func GetHost() string { + return GetEnv("PINATA_HOST", "api.pinata.cloud") +} + +func GetEnv(key, defaultValue string) string { + value := os.Getenv(key) + if len(value) == 0 { + return defaultValue + } + return value +} diff --git a/Upload.go b/Upload.go new file mode 100644 index 0000000..36d6b3f --- /dev/null +++ b/Upload.go @@ -0,0 +1,266 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/schollz/progressbar/v3" + "io" + "log" + "mime/multipart" + "net/http" + "os" + "path/filepath" +) + +func Upload(filePath string, version int, name string, cidOnly bool) (UploadResponse, error) { + jwt, err := findToken() + if err != nil { + return UploadResponse{}, err + } + + stats, err := os.Stat(filePath) + if os.IsNotExist(err) { + fmt.Println("File or folder does not exist") + return UploadResponse{}, errors.Join(err, errors.New("file or folder does not exist")) + } + + files, err := pathsFinder(filePath, stats) + if err != nil { + return UploadResponse{}, err + } + + body := &bytes.Buffer{} + contentType, err := createMultipartRequest(filePath, files, body, stats, version, name) + if err != nil { + return UploadResponse{}, err + } + + totalSize := int64(body.Len()) + fmt.Printf("Uploading %s (%s)\n", stats.Name(), formatSize(int(totalSize))) + + progressBody := newProgressReader(body, totalSize) + + host := GetHost() + url := fmt.Sprintf("https://%s/pinning/pinFileToIPFS", host) + req, err := http.NewRequest("POST", url, progressBody) + if err != nil { + return UploadResponse{}, errors.Join(err, errors.New("failed to create the request")) + } + req.Header.Set("Authorization", "Bearer "+string(jwt)) + req.Header.Set("content-type", contentType) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return UploadResponse{}, errors.Join(err, errors.New("failed to send the request")) + } + if resp.StatusCode != 200 { + return UploadResponse{}, fmt.Errorf("server Returned an error %d", resp.StatusCode) + } + err = progressBody.bar.Set(int(totalSize)) + if err != nil { + return UploadResponse{}, err + } + fmt.Println() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + log.Fatal("could not close request body") + } + }(resp.Body) + + var response UploadResponse + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return UploadResponse{}, err + } + if cidOnly { + fmt.Println(response.IpfsHash) + } else { + fmt.Println("Success!") + fmt.Println("CID:", response.IpfsHash) + fmt.Println("Size:", formatSize(response.PinSize)) + fmt.Println("Date:", response.Timestamp) + if response.IsDuplicate { + fmt.Println("Already Pinned: true") + } + } + return response, nil +} + +func findToken() ([]byte, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + dotFilePath := filepath.Join(homeDir, ".pinata-go-cli") + JWT, err := os.ReadFile(dotFilePath) + if err != nil { + if os.IsNotExist(err) { + return nil, errors.New("JWT not found. Please authorize first using the 'auth' command") + } else { + return nil, err + } + } + return JWT, err +} + +type progressReader struct { + r io.Reader + bar *progressbar.ProgressBar +} + +func cmpl() { + fmt.Println() + fmt.Println("Upload complete, pinning...") +} + +func newProgressReader(r io.Reader, size int64) *progressReader { + bar := progressbar.NewOptions64( + size, + progressbar.OptionEnableColorCodes(true), + progressbar.OptionShowBytes(true), + progressbar.OptionSetDescription("Uploading..."), + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "█", + SaucerPadding: " ", + BarStart: "|", + BarEnd: "|", + }), + progressbar.OptionOnCompletion(cmpl), + ) + return &progressReader{r: r, bar: bar} +} + +func (pr *progressReader) Read(p []byte) (n int, err error) { + n, err = pr.r.Read(p) + if err != nil { + return 0, err + } + err = pr.bar.Add(n) + if err != nil { + return 0, err + } + return +} + +func formatSize(bytes int) string { + const ( + KB = 1000 + MB = KB * KB + GB = MB * KB + ) + + var formattedSize string + + switch { + case bytes < KB: + formattedSize = fmt.Sprintf("%d bytes", bytes) + case bytes < MB: + formattedSize = fmt.Sprintf("%.2f KB", float64(bytes)/KB) + case bytes < GB: + formattedSize = fmt.Sprintf("%.2f MB", float64(bytes)/MB) + default: + formattedSize = fmt.Sprintf("%.2f GB", float64(bytes)/GB) + } + + return formattedSize +} + +func createMultipartRequest(filePath string, files []string, body io.Writer, stats os.FileInfo, version int, name string) (string, error) { + contentType := "" + writer := multipart.NewWriter(body) + + fileIsASingleFile := !stats.IsDir() + for _, f := range files { + file, err := os.Open(f) + if err != nil { + return contentType, err + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + log.Fatal("could not close file") + } + }(file) + + var part io.Writer + if fileIsASingleFile { + part, err = writer.CreateFormFile("file", filepath.Base(f)) + } else { + relPath, _ := filepath.Rel(filePath, f) + part, err = writer.CreateFormFile("file", filepath.Join(stats.Name(), relPath)) + } + if err != nil { + return contentType, err + } + _, err = io.Copy(part, file) + if err != nil { + return contentType, err + } + } + + pinataOptions := Options{ + CidVersion: version, + } + + optionsBytes, err := json.Marshal(pinataOptions) + if err != nil { + return contentType, err + } + err = writer.WriteField("pinataOptions", string(optionsBytes)) + + if err != nil { + return contentType, err + } + + pinataMetadata := Metadata{ + Name: func() string { + if name != "nil" { + return name + } + return stats.Name() + }(), + } + metadataBytes, err := json.Marshal(pinataMetadata) + if err != nil { + return contentType, err + } + _ = writer.WriteField("pinataMetadata", string(metadataBytes)) + err = writer.Close() + if err != nil { + return contentType, err + } + + contentType = writer.FormDataContentType() + + return contentType, nil +} + +func pathsFinder(filePath string, stats os.FileInfo) ([]string, error) { + var err error + files := make([]string, 0) + fileIsASingleFile := !stats.IsDir() + if fileIsASingleFile { + files = append(files, filePath) + return files, err + } + err = filepath.Walk(filePath, + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + files = append(files, path) + } + return nil + }) + + if err != nil { + return nil, err + } + + return files, err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..63a0357 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module pinata + +go 1.21 + +require ( + github.com/schollz/progressbar/v3 v3.13.1 + github.com/urfave/cli/v2 v2.25.7 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/term v0.12.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..78b78f8 --- /dev/null +++ b/go.sum @@ -0,0 +1,35 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE= +github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..3759beb --- /dev/null +++ b/install.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# +# Installation script for ipfs. It tries to move $bin in one of the +# directories stored in $binpaths. + +INSTALL_DIR=$(dirname $0) + +bin="$INSTALL_DIR/pinata" +binpaths='/usr/local/bin /usr/bin $HOME/.local/bin' + +# This variable contains a nonzero length string in case the script fails +# because of missing write permissions. +is_write_perm_missing="" + +for raw in $binpaths; do + # Expand the $HOME variable. + binpath=$(eval echo "$raw") + mkdir -p "$binpath" + if mv "$bin" "$binpath/pinata" ; then + echo "Moved $bin to $binpath" + exit 0 + else + if [ -d "$binpath" ] && [ ! -w "$binpath" ]; then + is_write_perm_missing=1 + fi + fi +done + +echo "We cannot install $bin in one of the directories $binpaths" + +if [ -n "$is_write_perm_missing" ]; then + echo "It seems that we do not have the necessary write permissions." + echo "Perhaps try running this script as a privileged user:" + echo + echo " sudo $0" + echo +fi + +exit 1 diff --git a/main.go b/main.go new file mode 100644 index 0000000..8d4b88f --- /dev/null +++ b/main.go @@ -0,0 +1,237 @@ +package main + +import ( + "errors" + "github.com/urfave/cli/v2" + "log" + "os" +) + +type UploadResponse struct { + IpfsHash string `json:"IpfsHash"` + PinSize int `json:"PinSize"` + Timestamp string `json:"Timestamp"` + IsDuplicate bool `json:"isDuplicate"` +} + +type PinByCIDResponse struct { + Id string `json:"id"` + CID string `json:"ipfsHash"` + Status string `json:"status"` + Name string `json:"name"` +} + +type Options struct { + CidVersion int `json:"cidVersion"` +} + +type Metadata struct { + Name string `json:"name"` + KeyValues map[string]interface{} `json:"keyvalues"` +} + +type Pin struct { + Id string `json:"id"` + IPFSPinHash string `json:"ipfs_pin_hash"` + Size int `json:"size"` + UserId string `json:"user_id"` + DatePinned string `json:"date_pinned"` + DateUnpinned *string `json:"date_unpinned"` + Metadata Metadata `json:"metadata"` + MimeType string `json:"mime_type"` + NumberOfFiles int `json:"number_of_files"` +} + +type ListResponse struct { + Rows []Pin `json:"rows"` +} + +type Request struct { + Id string `json:"id"` + CID string `json:"ipfs_pin_hash"` + StartDate string `json:"date_queued"` + Name string `json:"name"` + Status string `json:"status"` +} + +type RequestsResponse struct { + Rows []Request `json:"rows"` +} + +func main() { + exeName := "pinata" + app := &cli.App{ + Name: "pinata", + Usage: "A CLI for uploading files to Pinata! To get started make an API key at https://app.pinata.cloud/keys, then authorize the CLI with the auth command with your JWT", + Commands: []*cli.Command{ + { + Name: "auth", + Aliases: []string{"a"}, + Usage: "Authorize the CLI with your Pinata JWT", + ArgsUsage: "[your Pinata JWT]", + Action: func(ctx *cli.Context) error { + jwt := ctx.Args().First() + if jwt == "" { + return errors.New("no jwt supplied") + } + err := SaveJWT(jwt) + return err + }, + }, + { + Name: "upload", + Aliases: []string{"u"}, + Usage: "Upload a file or folder to Pinata", + ArgsUsage: "[path to file]", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "version", + Aliases: []string{"v"}, + Value: 1, + Usage: "Set desired CID version to either 0 or 1. Default is 1.", + }, + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Value: "nil", + Usage: "Add a name for the file you are uploading. By default it will use the filename on your system.", + }, + &cli.BoolFlag{ + Name: "cid-only", + Usage: "Use if you only want the CID returned after an upload", + }, + }, + Action: func(ctx *cli.Context) error { + filePath := ctx.Args().First() + version := ctx.Int("version") + name := ctx.String("name") + cidOnly := ctx.Bool("cid-only") + if filePath == "" { + return errors.New("no file path provided") + } + _, err := Upload(filePath, version, name, cidOnly) + return err + }, + }, + { + Name: "pin", + Aliases: []string{"p"}, + Usage: "Pin an existing CID on IPFS to Pinata", + ArgsUsage: "[CID of file on IPFS]", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Value: "null", + Usage: "Add a name for the file you are trying to pin.", + }, + }, + Action: func(ctx *cli.Context) error { + cid := ctx.Args().First() + name := ctx.String("name") + if cid == "" { + return errors.New("no cid provided") + } + _, err := PinByCID(cid, name) + return err + }, + }, + { + Name: "requests", + Aliases: []string{"r"}, + Usage: "List pin by CID requests.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cid", + Aliases: []string{"c"}, + Value: "null", + Usage: "Search pin by CID requests by CID", + }, + &cli.StringFlag{ + Name: "status", + Aliases: []string{"s"}, + Value: "null", + Usage: "Search by status for pin by CID requests. See https://docs.pinata.cloud/reference/get_pinning-pinjobs for more info.", + }, + &cli.StringFlag{ + Name: "pageOffset", + Aliases: []string{"p"}, + Value: "null", + Usage: "Allows you to paginate through requests by number of requests.", + }, + }, + Action: func(ctx *cli.Context) error { + cid := ctx.String("cid") + status := ctx.String("status") + offset := ctx.String("pageOffset") + _, err := Requests(cid, status, offset) + return err + }, + }, + { + Name: "delete", + Aliases: []string{"d"}, + Usage: "Delete a file by CID", + ArgsUsage: "[CID of file]", + Action: func(ctx *cli.Context) error { + cid := ctx.Args().First() + if cid == "" { + return errors.New("no CID provided") + } + err := Delete(cid) + return err + }, + }, + { + Name: "list", + Aliases: []string{"l"}, + Usage: "List most recent files", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cid", + Aliases: []string{"c"}, + Value: "null", + Usage: "Search files by CID", + }, + &cli.StringFlag{ + Name: "amount", + Aliases: []string{"a"}, + Value: "10", + Usage: "The number of files you would like to return, default 10 max 1000", + }, + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Value: "null", + Usage: "The name of the file", + }, + &cli.StringFlag{ + Name: "status", + Aliases: []string{"s"}, + Value: "pinned", + Usage: "Status of the file. Options are 'pinned', 'unpinned', or 'all'. Default: 'pinned'", + }, + &cli.StringFlag{ + Name: "pageOffset", + Aliases: []string{"p"}, + Value: "null", + Usage: "Allows you to paginate through files. If your file amount is 10, then you could set the pageOffset to '10' to see the next 10 files.", + }, + }, + Action: func(ctx *cli.Context) error { + cid := ctx.String("cid") + amount := ctx.String("amount") + name := ctx.String("name") + status := ctx.String("status") + offset := ctx.String("pageOffset") + _, err := ListFiles(amount, cid, name, status, offset) + return err + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +}