diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..d4dccf4 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.27" + + - name: Vet + run: go vet ./... + + - name: Test + run: go test -v ./... + + docker: + # build & push image only on tags (releases) + if: startsWith(github.ref, 'refs/tags/') + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea registry + uses: docker/login-action@v3 + with: + registry: git.archfox.org + username: ${{ gitea.actor }} + password: ${{ secrets.GITEA_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + git.archfox.org/poslop/palette:${{ gitea.ref_name }} + git.archfox.org/poslop/palette:latest + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1512eb6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# syntax=docker/dockerfile:1 + +# ---- build stage ---- +FROM golang:1.27-alpine AS build +WORKDIR /src + +# cache deps first +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /palette . + +# ---- runtime stage ---- +FROM alpine:3.20 + +# non-root user +RUN adduser -D -u 10001 palette + +COPY --from=build /palette /usr/local/bin/palette + +USER palette +WORKDIR /data + +ENV PALETTE_ADDR=":8080" \ + PALETTE_DB="/data/palette.db" + +EXPOSE 8080 +VOLUME /data + +HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ + CMD wget -qO- http://127.0.0.1:8080/ >/dev/null 2>&1 || exit 1 + +ENTRYPOINT ["palette"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..be2435f --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# Palette + +Fast, self-hosted pastebin with paste cans, password lock, expiry, custom URLs, and an API-first design. + +## Quick start + +```bash +go build -o palette . +./palette +# UI at http://localhost:8080 +``` + +## Docker + +```bash +docker build -t palette . +docker run -p 8080:8080 -v palette-data:/data palette +``` + +## Configuration + +| Env var | Default | Description | +|---|---|---| +| `PALETTE_ADDR` | `:8080` | Listen address | +| `PALETTE_DB` | `palette.db` | SQLite database path | +| `PALETTE_MAX_TEXT` | `5242880` | Max paste size in bytes (5 MB) | +| `PALETTE_MAX_ITEM` | `26214400` | Max can item size in bytes (25 MB) | + +## API + +### Create paste +```bash +curl -X POST http://localhost:8080/api/pastes \ + -H "Content-Type: application/json" \ + -d '{ + "content": "print(hello)", + "title": "my snippet", + "language": "python", + "expires_in": "168h", + "password": "optional", + "custom_slug": "optional", + "burn_after_read": false, + "visibility": "public" + }' +``` + +Response includes `id`, `url`, `raw_url`, `api_url`, and a one-time `deletion_token`. + +### Get paste +```bash +curl http://localhost:8080/api/pastes/{id} +# password-protected pastes: +curl "http://localhost:8080/api/pastes/{id}?password=secret" +# or via header: X-Paste-Password: secret +``` + +### Raw content +```bash +curl http://localhost:8080/raw/{id} +``` + +### Soft delete +```bash +curl -X DELETE http://localhost:8080/api/pastes/{id} +``` + +### Hard delete (requires deletion token) +```bash +curl -X DELETE "http://localhost:8080/api/pastes/{id}/redeem?token=TOKEN" +``` + +### Public history +```bash +curl "http://localhost:8080/api/public?limit=25&offset=0" +``` + +### Create can (bundle of items) +```bash +curl -X POST http://localhost:8080/api/pastes/can \ + -F "title=My bundle" \ + -F "expires_in=48h" \ + -F 'json_items=[{"title":"notes.txt","content":"some notes"}]' \ + -F "files=@screenshot.png" \ + -F "files=@log.txt" +``` + +### Get can + items +```bash +curl http://localhost:8080/api/cans/{id} +curl http://localhost:8080/api/cans/{id}/items/{item_id} +``` + +## Expiry and deletion + +- Expired pastes are soft-deleted by a background sweeper (runs every minute). +- Soft-deleted pastes are hard-deleted after a 7-day grace period. +- Deletion tokens allow immediate hard delete. +- Burn-after-read pastes are soft-deleted on first read. + +## Web pages + +- `/new` — create a paste +- `/history` — public paste history +- `/{id}` — view a paste +- `/unlock/{id}` — password gate for protected pastes +- `/raw/{id}` — raw content with original content type + +## CI + +Gitea Actions workflow at `.gitea/workflows/ci.yml`: +- On push to main: `go vet` + `go test` +- On tags: build and push Docker image to `git.archfox.org/poslop/palette`