Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial implementation #1

Merged
merged 18 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/public
/bin
53 changes: 53 additions & 0 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Create and publish a Docker image

on:
push:
branches:
- '**'
pull_request:
branches:
- "**"


env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push-image:
runs-on: ubuntu-latest

permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}


- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/pina
/public
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM golang:1.21.3-alpine3.18

COPY . /app
WORKDIR /app

RUN mkdir /app/bin
RUN mkdir /app/public
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I wanted to persist the uploaded files between restarts, would i have to mount a volume here? maybe a VOLUME directive would be a good idea here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@boudra you should be able to do it anyway when you start the container right?
but I didn't set it as our standard way to do it just because we don't have a way to persist the full state of the system including the blockchain state until anvil doesn't dump events foundry-rs/foundry#5906


RUN CGO_ENABLED=0 GOOS=linux go build -o /app/bin/🍍

EXPOSE 8000

CMD /app/bin/🍍 -port 8000 -public /app/public
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.PHONY: docker-build docker-run docker-kill docker-stop docker-logs docker-deploy-contracts docker-all

IMAGE_NAME=pina
CONTAINER_NAME=pina

docker-all: docker-kill docker-build docker-run

docker-build:
docker build . -t $(IMAGE_NAME) --no-cache --progress=plain

docker-run:
docker run --name $(CONTAINER_NAME) --rm -d -p 127.0.0.1:8000:8000/tcp $(IMAGE_NAME)

docker-kill:
-docker kill $(CONTAINER_NAME)

docker-stop:
docker stop $(CONTAINER_NAME)

docker-logs:
docker logs -f $(CONTAINER_NAME)

32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Piña 🍍

Piña is a clone of the [Pinata](https://www.pinata.cloud/) API,
**exclusively designed for local development environments** to streamline your development process and minimize
the need for direct usage of Pinata's services during development.

## API

```
get /ipfs/{CID}
post /pinning/pinJSONToIPFS
post /pinning/pinFileToIPFS
```

### Test and Build

```
git clone [email protected]:gitcoinco/pina.git
go test
go build
```

### Run

`go build && ./pina -port 8000 -public ./public`

### Run in docker

```
make docker-build
make docker-run
```
25 changes: 25 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module github.com/gitcoinco/pina

go 1.19

require (
github.com/gravityblast/miniassert v0.0.0-20140522125902-bee63581261a
github.com/julienschmidt/httprouter v1.3.0
)

require (
github.com/ipfs/go-cid v0.4.1 // indirect
github.com/klauspost/cpuid/v2 v2.0.4 // indirect
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.0.3 // indirect
github.com/multiformats/go-base36 v0.1.0 // indirect
github.com/multiformats/go-multibase v0.0.3 // indirect
github.com/multiformats/go-multicodec v0.9.0 // indirect
github.com/multiformats/go-multihash v0.0.15 // indirect
github.com/multiformats/go-varint v0.0.6 // indirect
github.com/rs/cors v1.10.1 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/sys v0.1.0 // indirect
)
41 changes: 41 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
github.com/gravityblast/miniassert v0.0.0-20140522125902-bee63581261a h1:3xcty4gUzOHD4pqUZQDbS/Xb34susIRg9nmjKR3XM94=
github.com/gravityblast/miniassert v0.0.0-20140522125902-bee63581261a/go.mod h1:tVdF3zmaPraSn1jEeaVFIw4GYDX6fezNB3P9Gk4UpPA=
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/klauspost/cpuid/v2 v2.0.4 h1:g0I61F2K2DjRHz1cnxlkNSBIaePVoJIjjnHui8QHbiw=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g=
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI=
github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA=
github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4=
github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM=
github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk=
github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc=
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
github.com/multiformats/go-multihash v0.0.15 h1:hWOPdrNqDjwHDx82vsYGSDZNyktOJJ2dzZJzFkOV1jM=
github.com/multiformats/go-multihash v0.0.15/go.mod h1:D6aZrWNLFTV/ynMpKsNtB40mJzmCl4jb1alC0OvHiHg=
github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
158 changes: 158 additions & 0 deletions handlers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package main

import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/textproto"
"os"
"path/filepath"
"strings"
"testing"

assert "github.com/gravityblast/miniassert"
"github.com/julienschmidt/httprouter"
)

func newTestRouter(t *testing.T) (*httprouter.Router, string) {
publicPath := t.TempDir()
router, err := newRouter(publicPath)
if err != nil {
t.Error(err)
t.FailNow()
}

return router, filepath.Join(publicPath, "ipfs")
}

func TestIndexHandler(t *testing.T) {
router, _ := newTestRouter(t)

w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", nil)
router.ServeHTTP(w, req)

resp := w.Result()
body, _ := io.ReadAll(resp.Body)

assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
assert.Equal(t, "Hello World", string(body))
}

func TestStaticFiles(t *testing.T) {
router, ipfsPath := newTestRouter(t)
filePath := filepath.Join(ipfsPath, "test.txt")
err := os.WriteFile(filePath, []byte("Static file example"), 0755)
if err != nil {
t.Fatal(err)
}

w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/ipfs/test.txt", nil)
router.ServeHTTP(w, req)

resp := w.Result()
body, _ := io.ReadAll(resp.Body)

assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
assert.Equal(t, "Static file example", string(body))
}

func TestPinJSONHandler(t *testing.T) {
router, ipfsPath := newTestRouter(t)

w := httptest.NewRecorder()
reqBody := bytes.NewBufferString(`{"pinataContent": {"foo": {"bar": "baz"}}}`)
req := httptest.NewRequest(http.MethodPost, "/pinning/pinJSONToIPFS", reqBody)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", AUTH_TOKEN))
router.ServeHTTP(w, req)

resp := w.Result()

assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))

var responseBody PinJSONResponseBody
err := json.NewDecoder(resp.Body).Decode(&responseBody)
if err != nil {
log.Fatal(err)
}

cid := "bafkreihktyturq4bzrikdjylvvjbrgh5rfigzlydmjoyri3ip6fjbcqddu"
assert.Equal(t, cid, responseBody.IpfsHash)
assert.Equal(t, 10, responseBody.PinSize)
assert.NotEqual(t, "", responseBody.Timestamp)

fileName := filepath.Join(ipfsPath, cid)
f, err := os.Open(fileName)
if err != nil {
log.Fatal(err)
}

content, err := ioutil.ReadAll(f)
if err != nil {
log.Fatal(err)
}

assert.Equal(t, `{"foo":{"bar":"baz"}}`, strings.TrimSpace(string(content)))
}

func TestPinFileHandler(t *testing.T) {
router, _ := newTestRouter(t)

body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
// writer.WriteField("file", "Uploaded content")

// Create the file part with an invalid file extension
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "file", "uploaded.txt"))
h.Set("Content-Type", "text/plain")
part, _ := writer.CreatePart(h)
part.Write([]byte("Uploaded content"))
writer.Close()
// Prepare and send the HTTP request

w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/pinning/pinFileToIPFS", body)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", AUTH_TOKEN))
req.Header.Add("Content-Type", writer.FormDataContentType())
router.ServeHTTP(w, req)

resp := w.Result()

assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))

var responseBody PinJSONResponseBody
err := json.NewDecoder(resp.Body).Decode(&responseBody)
if err != nil {
log.Fatal(err)
}

cid := "bafkreifqrpvngn6k2q6qahrm2oawbtrsoucxi7xtxehydodbzgnky6eiem"
assert.Equal(t, cid, responseBody.IpfsHash)
assert.Equal(t, 10, responseBody.PinSize)
assert.NotEqual(t, "", responseBody.Timestamp)

fileName := filepath.Join(ipfsPath, cid)
f, err := os.Open(fileName)
if err != nil {
log.Fatal(err)
}

content, err := ioutil.ReadAll(f)
if err != nil {
log.Fatal(err)
}

assert.Equal(t, "Uploaded content", strings.TrimSpace(string(content)))
}
Loading