{"id":51612271,"url":"https://github.com/pyaethu-aung/image-server","last_synced_at":"2026-07-12T09:31:03.774Z","repository":{"id":369718986,"uuid":"1291120240","full_name":"pyaethu-aung/image-server","owner":"pyaethu-aung","description":"Image upload and transformation server in Go (multipart/URL uploads, on-the-fly resize/format/quality/fit transforms, cached derivatives). Under active development — core upload/transform/cache endpoints not yet implemented.","archived":false,"fork":false,"pushed_at":"2026-07-06T16:45:56.000Z","size":56,"stargazers_count":0,"open_issues_count":4,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-06T17:06:27.245Z","etag":null,"topics":["chi-router","docker","go","golang","image-processing","image-server","libvips","openapi","postgresql","redis","rest-api"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pyaethu-aung.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-06T14:21:11.000Z","updated_at":"2026-07-06T16:46:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/pyaethu-aung/image-server","commit_stats":null,"previous_names":["pyaethu-aung/image-server"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/pyaethu-aung/image-server","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyaethu-aung%2Fimage-server","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyaethu-aung%2Fimage-server/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyaethu-aung%2Fimage-server/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyaethu-aung%2Fimage-server/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pyaethu-aung","download_url":"https://codeload.github.com/pyaethu-aung/image-server/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pyaethu-aung%2Fimage-server/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35388394,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-12T02:00:06.386Z","response_time":87,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chi-router","docker","go","golang","image-processing","image-server","libvips","openapi","postgresql","redis","rest-api"],"created_at":"2026-07-12T09:31:03.360Z","updated_at":"2026-07-12T09:31:03.768Z","avatar_url":"https://github.com/pyaethu-aung.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# image-server\n\n[![Version](https://img.shields.io/badge/version-0.1.0-blue)](docs/openapi/image-server.yaml)\n[![CI](https://github.com/pyaethu-aung/image-server/actions/workflows/ci.yml/badge.svg)](https://github.com/pyaethu-aung/image-server/actions/workflows/ci.yml)\n[![Docker](https://github.com/pyaethu-aung/image-server/actions/workflows/docker.yml/badge.svg)](https://github.com/pyaethu-aung/image-server/actions/workflows/docker.yml)\n[![Security](https://github.com/pyaethu-aung/image-server/actions/workflows/security.yml/badge.svg)](https://github.com/pyaethu-aung/image-server/actions/workflows/security.yml)\n\nAn image upload and transformation service in Go. Upload images via multipart form or by URL, store originals on local disk behind a pluggable storage interface, and serve them back with on-the-fly transforms (resize, format conversion, quality, fit) via URL query params. Generated derivatives are cached so repeated transforms are served from cache, not recomputed.\n\n\u003e **Status: core HTTP service complete.** Uploads (`POST /v1/images`, `POST /v1/images/from-url`), reads (`GET /v1/images/{id}`, original or transformed, plus `/meta`), derivative caching, and `DELETE` are all live and tested. See [CLAUDE.md](CLAUDE.md) for the architecture decisions. Next on the roadmap: an MCP server interface, then an S3 storage backend.\n\n## Features\n\n- **Upload** via multipart file or by URL (server fetches it, with SSRF protection). Accepts JPEG, PNG, GIF, WebP, TIFF, and the HEIF family (HEIC/HEIF/AVIF, i.e. iPhone and modern Android photos)\n- **Local filesystem storage** for originals, behind a `Storage` interface (S3 planned as a second backend)\n- **On-the-fly transforms** on read: width, height, format (jpeg/png/webp), quality, fit mode\n- **Derivative caching**: each unique transform is generated once, stored under a separate prefix, and tracked in Redis\n- **Deduplication**: identical uploads (same sha256) return the existing record\n- **Security built in**: magic-byte type validation, upload size limits, decompression-bomb guard, SSRF protection, path-traversal guard, API-key auth, per-key rate limiting\n\n## Tech Stack\n\n| Concern | Choice |\n|---|---|\n| Language / router | Go 1.26+, `chi` |\n| Image processing | `h2non/bimg` (libvips) |\n| Object storage | Local filesystem (S3 via AWS SDK v2 planned) |\n| Metadata | PostgreSQL via `pgx` + `sqlc` |\n| Cache / rate limiting | Redis |\n| Local stack | `docker-compose` (Postgres, Redis) |\n\n## Getting Started\n\n### Prerequisites\n\n- Docker and Docker Compose\n- Go 1.26+ and libvips (only for running the server outside Docker: `brew install vips` on macOS)\n\n### Run the stack\n\n```sh\ngit clone https://github.com/pyaethu-aung/image-server.git\ncd image-server\nmake setup             # wire git hooks (one-time; see Quality gates below)\ncp .env.example .env   # set API_KEY and adjust as needed\nmake up\n```\n\nThis builds the server image and boots it with Postgres and Redis, detached. Uploaded images are stored in a Docker volume mounted at `STORAGE_PATH`. Then apply migrations:\n\n```sh\nmake migrate\n```\n\n### Makefile targets\n\n```sh\nmake setup        # one-time: activate the committed git hooks\nmake up           # build and boot the full stack, detached\nmake down         # stop the stack (keeps volumes)\nmake run          # run the server locally (expects the compose services up)\nmake migrate      # apply SQL migrations (DATABASE_URL from env or .env)\nmake sqlc-gen     # regenerate database code from SQL queries\nmake openapi-gen  # regenerate server interfaces from the OpenAPI spec\nmake lint         # run golangci-lint\nmake test         # run all unit tests\nmake coverage     # print overall coverage\nmake test-api     # validate every endpoint against the OpenAPI spec\nmake test-e2e     # full-stack e2e tests against the real container (LOCAL-ONLY, not in CI)\n```\n\n### Quality gates (git hooks)\n\nThe repo ships hooks in `.githooks/`, activated by `make setup` (`git config core.hooksPath .githooks`):\n\n- **pre-commit**: `go mod tidy` cleanliness and generated-code sync checks\n- **commit-msg**: 50/72 subject-line rule\n- **pre-push**: golangci-lint, unit-test coverage gate (90% per layer and overall, generated code excluded), API spec tests (`make test-api`), and a full-stack e2e gate against the real Docker container (`make test-e2e`)\n\nCI re-runs three of these four gates on every PR (lint, coverage, generated-code drift, spec tests against real Postgres/Redis). **The e2e gate (`make test-e2e`) is local-only: CI does not re-run it.** `docker.yml` builds and pushes the server image but runs no tests against it, so a push made with `--no-verify`, or from a clone that skipped `make setup`, has no server-side backstop for container-level regressions. CI also runs `govulncheck` and validates PR titles as Conventional Commits. Dependabot keeps Go modules, Docker base images, and Actions current.\n\n### Claude Code plugins (contributors)\n\nThis repo's Claude Code workflows (commit messages, PRs, Go feature scaffolding) come from two plugins tracked in `.claude/settings.json`. If you use Claude Code on this repo, add the marketplace and install them once:\n\n```\n/plugin marketplace add pyaethu-aung/skills\n/plugin install git-workflow@pyaethu-aung-skills\n/plugin install go-dev@pyaethu-aung-skills\n```\n\nRun `/reload-plugins` after installing.\n\n### Configuration\n\nAll configuration is via environment variables. Never commit real credentials.\n\n| Variable | Description | Default |\n|---|---|---|\n| `API_KEY` | Key clients must send in `X-API-Key` | (required) |\n| `LISTEN_ADDR` | HTTP listen address | `:8080` |\n| `DATABASE_URL` | Postgres connection string | (required) |\n| `REDIS_ADDR` | Redis address | `localhost:6379` |\n| `STORAGE_PATH` | Root directory for stored images | `./data/images` |\n| `MAX_UPLOAD_BYTES` | Max upload size | `10485760` (10MB) |\n| `MAX_PIXELS` | Max decoded pixel count (bomb guard) | `50000000` |\n| `RATE_LIMIT_PER_MIN` | Requests per minute per API key | `120` |\n| `CACHE_CONTROL_MAX_AGE` | `Cache-Control` max-age (seconds) on served image bytes | `31536000` (1 year) |\n| `DERIVATIVE_CACHE_TTL` | TTL for Redis derivative-cache markers (Go duration) | `720h` (30 days) |\n\n## API\n\nThe OpenAPI 3.0.3 spec at [`docs/openapi/image-server.yaml`](docs/openapi/image-server.yaml) is the source of truth for this API; server interfaces are generated from it with `oapi-codegen`. The examples below are illustrative.\n\nAll `/v1` routes require an `X-API-Key` header. Examples assume the server on `localhost:8080` and `$API_KEY` exported.\n\n### Upload an image (multipart)\n\n```sh\ncurl -X POST http://localhost:8080/v1/images \\\n  -H \"X-API-Key: $API_KEY\" \\\n  -F \"file=@photo.jpg\"\n```\n\nResponse:\n\n```json\n{\n  \"id\": \"1b4e28ba-2fa1-4d3b-9558-b7f6f18e3c2a\",\n  \"original_filename\": \"photo.jpg\",\n  \"content_hash\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\n  \"mime_type\": \"image/jpeg\",\n  \"width\": 4032,\n  \"height\": 3024,\n  \"size_bytes\": 2843190,\n  \"created_at\": \"2026-07-06T12:00:00Z\"\n}\n```\n\nUploading a byte-identical file again returns the existing record (dedup by sha256).\n\n### Upload from a URL\n\n```sh\ncurl -X POST http://localhost:8080/v1/images/from-url \\\n  -H \"X-API-Key: $API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"https://example.com/photo.jpg\"}'\n```\n\nOnly public `http(s)` URLs are accepted. Requests resolving to private, loopback, link-local, or cloud-metadata addresses are rejected, including via redirects.\n\n### Get an image (original or transformed)\n\n```sh\n# Original\ncurl -H \"X-API-Key: $API_KEY\" \\\n  http://localhost:8080/v1/images/1b4e28ba-2fa1-4d3b-9558-b7f6f18e3c2a -o out.jpg\n\n# Resized to 800px wide, converted to webp at quality 80\ncurl -H \"X-API-Key: $API_KEY\" \\\n  \"http://localhost:8080/v1/images/1b4e28ba-2fa1-4d3b-9558-b7f6f18e3c2a?w=800\u0026fmt=webp\u0026q=80\" -o out.webp\n\n# 400x400 thumbnail, cropped to fill\ncurl -H \"X-API-Key: $API_KEY\" \\\n  \"http://localhost:8080/v1/images/1b4e28ba-2fa1-4d3b-9558-b7f6f18e3c2a?w=400\u0026h=400\u0026fit=cover\" -o thumb.jpg\n\n# Strip metadata (EXIF/GPS, XMP, IPTC) from the served image\ncurl -H \"X-API-Key: $API_KEY\" \\\n  \"http://localhost:8080/v1/images/1b4e28ba-2fa1-4d3b-9558-b7f6f18e3c2a?strip=true\" -o clean.jpg\n```\n\nTransform query params:\n\n| Param | Values | Description |\n|---|---|---|\n| `w` | positive int | target width in pixels |\n| `h` | positive int | target height in pixels |\n| `fmt` | `jpeg`, `png`, `webp` | output format |\n| `q` | 1 to 100 | output quality (lossy formats) |\n| `fit` | `cover`, `contain` | `cover` crops to fill w x h; `contain` fits within w x h |\n| `strip` | `true`, `false` | remove metadata (EXIF/GPS, XMP, IPTC, comments) from the served image |\n\nEach unique combination is generated once and cached; responses include `Cache-Control` headers.\n\nA HEIC, HEIF, AVIF, or TIFF original is served back unchanged when no transform\nis requested. Transforming one requires an explicit `fmt` (`jpeg`, `png`, or\n`webp`), since the server only encodes to those web formats; a transform without\n`fmt` on such a source returns `400`.\n\n`strip` never touches the stored original. On its own it is a lossless,\nbyte-preserving removal for JPEG and PNG; for other formats on their own it\nreturns `415` (add `fmt` to strip via a re-encode). Combined with a resize or\nformat change, the strip happens during that re-encode.\n\n### Get image metadata\n\n```sh\ncurl -H \"X-API-Key: $API_KEY\" \\\n  http://localhost:8080/v1/images/1b4e28ba-2fa1-4d3b-9558-b7f6f18e3c2a/meta\n```\n\n### Delete an image\n\n```sh\ncurl -X DELETE -H \"X-API-Key: $API_KEY\" \\\n  http://localhost:8080/v1/images/1b4e28ba-2fa1-4d3b-9558-b7f6f18e3c2a\n```\n\nDeletes the original, cached derivatives, and metadata.\n\n### Health check\n\n```sh\ncurl http://localhost:8080/healthz\n```\n\nNo auth required.\n\n## Project Structure\n\n```\ncmd/server/          entrypoint\ninternal/api/        handlers, middleware, router\ninternal/storage/    Storage interface + local filesystem implementation\ninternal/imageproc/  bimg transforms + validation\ninternal/db/         sqlc-generated code + queries\ninternal/config/     env parsing\nmigrations/          SQL migrations\ndocker-compose.yml\nMakefile\n```\n\nStorage is defined as an interface (`Put`, `Get`, `Delete`, `Exists`) with the local filesystem as the first implementation, so additional backends (S3, OneDrive, Dropbox) can be added without touching handler code.\n\n## Testing\n\n```sh\nmake test      # unit tests\nmake test-api  # spec-driven API tests (starts Postgres + Redis via compose)\nmake test-e2e  # full-stack e2e tests against the real container (local-only)\n```\n\nTable-driven unit tests cover transform param parsing, the SSRF guard, and storage key sanitization; the pre-push hook enforces 90% coverage. API tests boot the router in-process and validate every request/response against the OpenAPI spec with `kin-openapi`. `make test-e2e` goes one step further: it boots the real `server` Docker image (not an in-process router) and runs the same kind of spec-validated checks against it over the network — the only test path that exercises the Dockerfile, the libvips runtime lib, and container env-var wiring. It is a local-only pre-push gate; CI does not run it.\n\n## Roadmap\n\n- [x] Core HTTP service (scaffold, storage, upload, transform, caching)\n- [ ] MCP server interface exposing upload/transform as tools\n- [ ] S3 storage backend (AWS SDK for Go v2, Garage or LocalStack for local testing)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpyaethu-aung%2Fimage-server","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpyaethu-aung%2Fimage-server","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpyaethu-aung%2Fimage-server/lists"}