diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..f691b82 Binary files /dev/null and b/.DS_Store differ 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) + } +}