├── .codeclimate.yml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .editorconfig ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── renovate.json └── workflows │ ├── build.yml │ ├── go.yml │ ├── release-please.yml │ └── scrape.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .prettierignore ├── .release-please-manifest.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── cmd └── manael │ ├── doc.go │ └── main.go ├── commitlint.config.js ├── decode.go ├── docsearch.json ├── encode.go ├── example_test.go ├── go.mod ├── go.sum ├── internal └── testutil │ └── testutil.go ├── lint-staged.config.js ├── manael.go ├── manael_test.go ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── release-please-config.json ├── testdata ├── empty.gif ├── empty.txt ├── gray.png ├── invalid.png ├── logo.png └── photo.jpeg └── website ├── docs ├── 0-introduction.md ├── 1-installation.md └── _category_.json ├── docusaurus.config.js ├── i18n ├── en │ ├── code.json │ ├── docusaurus-plugin-content-docs │ │ └── current.json │ └── docusaurus-theme-classic │ │ ├── footer.json │ │ └── navbar.json └── ja │ ├── code.json │ ├── docusaurus-plugin-content-docs │ ├── current.json │ └── current │ │ ├── 0-introduction.md │ │ ├── 1-installation.md │ │ └── _category_.json │ └── docusaurus-theme-classic │ ├── footer.json │ └── navbar.json ├── package.json ├── sidebars.js ├── src ├── css │ └── custom.css └── pages │ ├── index.jsx │ └── styles.module.css └── static ├── img ├── logo.png └── manael.png └── x ├── manael.html └── manael ├── cmd └── manael.html ├── internal └── testutil.html ├── v2.html └── v2 ├── cmd └── manael.html └── internal └── testutil.html /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | checks: 3 | argument-count: 4 | enabled: false 5 | method-complexity: 6 | enabled: false 7 | method-lines: 8 | enabled: false 9 | return-statements: 10 | enabled: false 11 | similar-code: 12 | enabled: false 13 | plugins: 14 | eslint: 15 | channel: eslint-7 16 | enabled: true 17 | gofmt: 18 | enabled: true 19 | golint: 20 | enabled: true 21 | govet: 22 | enabled: true 23 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/go:1-1.23-bookworm@sha256:ee28302232bca53c6cfacf0b00a427ebbda10b33731c78d3dcf9f59251b23c9c 2 | 3 | # [Optional] Uncomment this section to install additional OS packages. 4 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 5 | && apt-get -y install --no-install-recommends libaom-dev libwebp-dev 6 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/go 3 | { 4 | "name": "Manael", 5 | "build": { 6 | "dockerfile": "Dockerfile" 7 | }, 8 | 9 | // Features to add to the dev container. More info: https://containers.dev/features. 10 | "features": { 11 | "ghcr.io/devcontainers/features/node:1.6.2": { 12 | "version": "22.16" 13 | } 14 | }, 15 | 16 | // Configure tool-specific properties. 17 | "customizations": { 18 | "vscode": { 19 | "settings": {}, 20 | "extensions": ["EditorConfig.EditorConfig", "esbenp.prettier-vscode"] 21 | } 22 | }, 23 | 24 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 25 | "forwardPorts": [8080], 26 | 27 | // Use 'portsAttributes' to set default properties for specific forwarded ports. 28 | // More info: https://containers.dev/implementors/json_reference/#port-attributes 29 | "portsAttributes": { 30 | "8080": { 31 | "label": "Manael Dev Server", 32 | "onAutoForward": "notify" 33 | } 34 | }, 35 | 36 | // Use 'postCreateCommand' to run commands after the container is created. 37 | "postCreateCommand": "go mod download && pnpm install" 38 | 39 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 40 | // "remoteUser": "root" 41 | } 42 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [Dockerfile] 12 | indent_size = 4 13 | indent_style = tab 14 | 15 | [Makefile] 16 | indent_size = 8 17 | indent_style = tab 18 | 19 | [*.go] 20 | indent_size = 8 21 | indent_style = tab 22 | 23 | [*.md] 24 | indent_size = 4 25 | trim_trailing_whitespace = false 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/jsx-runtime", 10 | "plugin:react-hooks/recommended" 11 | ], 12 | "parserOptions": { 13 | "sourceType": "module" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: 2 | - https://www.paypal.me/ykzts 3 | github: ykzts 4 | patreon: ykzts 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | about: Create a report to help us improve 3 | name: Bug report 4 | --- 5 | 6 | ### Describe the bug 7 | 8 | 9 | 10 | ### To Reproduce 11 | 12 | 13 | 14 | ### Expected behavior 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | about: Suggest an idea for this project 3 | name: Feature request 4 | --- 5 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:best-practices", 5 | "customManagers:githubActionsVersions", 6 | "helpers:disableTypesNodeMajor", 7 | ":automergeMinor", 8 | ":automergeRequireAllStatusChecks", 9 | ":combinePatchMinorReleases", 10 | ":disableRateLimiting", 11 | ":enableVulnerabilityAlertsWithLabel(security)", 12 | ":label(dependencies)", 13 | ":maintainLockFilesWeekly", 14 | ":prImmediately", 15 | ":preserveSemverRanges", 16 | ":reviewer(ykzts)", 17 | ":renovatePrefix", 18 | ":semanticCommitTypeAll(build)", 19 | ":separateMultipleMajorReleases", 20 | ":timezone(Asia/Tokyo)", 21 | ":widenPeerDependencies" 22 | ], 23 | "dependencyDashboardLabels": ["dependencies"], 24 | "lockFileMaintenance": { 25 | "extends": [":semanticCommitType(build)"] 26 | }, 27 | "platformAutomerge": true, 28 | "postUpdateOptions": [ 29 | "gomodTidy", 30 | "gomodUpdateImportPaths", 31 | "pnpmDedupe" 32 | ], 33 | "rebaseWhen": "conflicted", 34 | "reviewers": ["ykzts"] 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | push: 7 | branches: [main] 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | docker: 13 | runs-on: ubuntu-24.04 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 21 | - name: Set up QEMU 22 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5 23 | with: 24 | go-version: '1.24' 25 | - id: meta 26 | name: Docker meta 27 | uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5 28 | with: 29 | images: | 30 | ghcr.io/manaelproxy/manael 31 | tags: | 32 | type=ref,event=branch 33 | type=ref,event=pr 34 | type=semver,pattern={{version}} 35 | type=semver,pattern={{major}}.{{minor}} 36 | - name: Set up QEMU 37 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3 38 | - name: Set up Docker Buildx 39 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 40 | - if: ${{ github.event_name == 'release' }} 41 | name: Login to GitHub Container Registry 42 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 43 | with: 44 | registry: ghcr.io 45 | username: ${{ github.actor }} 46 | password: ${{ secrets.GITHUB_TOKEN }} 47 | - env: 48 | CGO_CFLAGS: -I/tmp/libwebp/include -I/tmp/libaom/include 49 | CGO_LDFLAGS: -L/tmp/libwebp/lib -lwebp -L/tmp/libaom/lib -laom -lm 50 | name: Build and push 51 | uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6 52 | with: 53 | cache-from: type=gha 54 | cache-to: type=gha,mode=max 55 | context: . 56 | labels: ${{ steps.meta.outputs.labels }} 57 | tags: ${{ steps.meta.outputs.tags }} 58 | push: ${{ github.event_name == 'release' }} 59 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | tags: ['!*'] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-24.04 13 | 14 | strategy: 15 | matrix: 16 | go-version: 17 | - '1.21' 18 | - '1.22' 19 | - '1.23' 20 | 21 | steps: 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 23 | - name: Set up Go ${{ matrix.go-version }} 24 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5 25 | with: 26 | go-version: ${{ matrix.go-version }} 27 | 28 | - name: Get dependencies 29 | run: | 30 | sudo apt-get update 31 | sudo apt-get install -y libaom-dev libwebp-dev 32 | go mod download 33 | 34 | - name: Test 35 | env: 36 | MANAEL_ENABLE_AVIF: true 37 | run: | 38 | mkdir -p cover 39 | go test -race -coverprofile=coverage.txt -covermode=atomic -v 40 | 41 | - uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5 42 | with: 43 | token: ${{ secrets.CODECOV_TOKEN }} 44 | 45 | - name: Build 46 | run: make 47 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: release-please 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | release-please: 9 | runs-on: ubuntu-24.04 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | steps: 15 | - id: generate_token 16 | uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2 17 | with: 18 | app-id: ${{ secrets.RELEASE_PLEASE_APP_ID }} 19 | private-key: ${{ secrets.RELEASE_PLEASE_PRIVATE_KEY }} 20 | - uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4 21 | with: 22 | token: ${{ steps.generate_token.outputs.token }} 23 | -------------------------------------------------------------------------------- /.github/workflows/scrape.yml: -------------------------------------------------------------------------------- 1 | name: DocSearch Scraper 2 | 3 | on: 4 | schedule: 5 | - cron: '53 1 * * *' 6 | 7 | jobs: 8 | scrape: 9 | runs-on: ubuntu-24.04 10 | 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 13 | - name: Get DocSearch configuration 14 | id: docsearch-config 15 | run: echo "::set-output name=json::$(cat docsearch.json | jq -r tostring)" 16 | - name: Pull DocSearch Scraper 17 | run: docker pull algolia/docsearch-scraper:latest 18 | - name: Run DocSearch Scraper 19 | run: docker run -e TZ -e API_KEY -e APPLICATION_ID -e CONFIG algolia/docsearch-scraper:latest 20 | env: 21 | TZ: UTC 22 | API_KEY: ${{ secrets.ALGOLIA_API_KEY }} 23 | APPLICATION_ID: ${{ secrets.ALGOLIA_APPLICATION_ID }} 24 | CONFIG: ${{ steps.docsearch-config.outputs.json }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vercel/ 2 | /bin/ 3 | /cover/ 4 | /dist/ 5 | /node_modules/ 6 | /website/.docusaurus/ 7 | /website/build/ 8 | /website/node_modules/ 9 | 10 | /yarn-error.log 11 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | pnpm commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.vercel/ 2 | /bin/ 3 | /cover/ 4 | /dist/ 5 | /node_modules/ 6 | /website/.docusaurus/ 7 | /website/build/ 8 | /website/node_modules/ 9 | 10 | /yarn-error.log 11 | /pnpm-lock.yaml 12 | /pnpm-workspace.yaml 13 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "2.0.6" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.0.6](https://github.com/manaelproxy/manael/compare/v2.0.5...v2.0.6) (2024-12-08) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * **docker:** fix syntax for dockerfile ([#1354](https://github.com/manaelproxy/manael/issues/1354)) ([5638f9d](https://github.com/manaelproxy/manael/commit/5638f9d0f7f1a4714e00ba8466bbcd193fb4578a)) 9 | 10 | ## [2.0.5](https://github.com/manaelproxy/manael/compare/v2.0.4...v2.0.5) (2023-12-22) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * **docker:** add missing `main.go` ([#1080](https://github.com/manaelproxy/manael/issues/1080)) ([45b6dd6](https://github.com/manaelproxy/manael/commit/45b6dd6617663846b3de3005ebe8be122bb6a9df)) 16 | 17 | 18 | ### Reverts 19 | 20 | * **deps:** downgrade module github.com/harukasan/go-libwebp ([#1078](https://github.com/manaelproxy/manael/issues/1078)) ([b586e5f](https://github.com/manaelproxy/manael/commit/b586e5fa38a332100cd2e40e21aad801342c0cd2)) 21 | 22 | ## [2.0.4](https://github.com/manaelproxy/manael/compare/v2.0.3...v2.0.4) (2023-12-20) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * always set vary header ([#1075](https://github.com/manaelproxy/manael/issues/1075)) ([ef3d47f](https://github.com/manaelproxy/manael/commit/ef3d47f4cfc7e06143c073b6200a7017df067e52)) 28 | 29 | ## [2.0.3](https://github.com/manaelproxy/manael/compare/v2.0.2...v2.0.3) (2023-12-20) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * add build workflow ([#1072](https://github.com/manaelproxy/manael/issues/1072)) ([112232c](https://github.com/manaelproxy/manael/commit/112232c72a8c5826607c61104ecb16e406ca4255)) 35 | 36 | ## [2.0.2](https://github.com/manaelproxy/manael/compare/v2.0.1...v2.0.2) (2023-12-20) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * **docker:** add `cmake` ([#1070](https://github.com/manaelproxy/manael/issues/1070)) ([bc6dd34](https://github.com/manaelproxy/manael/commit/bc6dd344ce8b0de8e02a5e3e7802f8cc788bc382)) 42 | * **docker:** add `cmake` ([#1071](https://github.com/manaelproxy/manael/issues/1071)) ([aa12a63](https://github.com/manaelproxy/manael/commit/aa12a63bc20f023400e342d19a8a1474f757a1d7)) 43 | * **docker:** add missing `&` ([#1068](https://github.com/manaelproxy/manael/issues/1068)) ([9523d2d](https://github.com/manaelproxy/manael/commit/9523d2d2746d8a9d0f3a737326b1f651205395b6)) 44 | * **docker:** add missing `&` (2) ([#1069](https://github.com/manaelproxy/manael/issues/1069)) ([fc5025c](https://github.com/manaelproxy/manael/commit/fc5025c5258188c1eeae2d0239fb874eb58e876a)) 45 | * **docker:** build on docker ([#1065](https://github.com/manaelproxy/manael/issues/1065)) ([57fb700](https://github.com/manaelproxy/manael/commit/57fb700e8350ed7cbb96a15f5e116ec774841a09)) 46 | * **docker:** remove `sudo` ([#1067](https://github.com/manaelproxy/manael/issues/1067)) ([631c24e](https://github.com/manaelproxy/manael/commit/631c24e5a3ecd59daaebd6f3e3bcecdfac9dd152)) 47 | * **github-actions:** fix release flow ([#1064](https://github.com/manaelproxy/manael/issues/1064)) ([52c6010](https://github.com/manaelproxy/manael/commit/52c6010df710b18b5d796326439d536ed1de4039)) 48 | * **release-please:** fallback release_created ([#1066](https://github.com/manaelproxy/manael/issues/1066)) ([b316e59](https://github.com/manaelproxy/manael/commit/b316e595e88555e33531210d738ae874a198c884)) 49 | * remove goreleaser ([#1062](https://github.com/manaelproxy/manael/issues/1062)) ([33bf0b2](https://github.com/manaelproxy/manael/commit/33bf0b201b8dbf2a842bf730995113b327ea47fb)) 50 | 51 | ## [2.0.1](https://github.com/manaelproxy/manael/compare/v2.0.0...v2.0.1) (2023-12-20) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * **goreleaser:** fix invalid syntax ([#1060](https://github.com/manaelproxy/manael/issues/1060)) ([f78bff3](https://github.com/manaelproxy/manael/commit/f78bff38975965cc223f4cad1a5b6975e3c311b1)) 57 | 58 | ## [2.0.0](https://github.com/manaelproxy/manael/compare/v1.9.1...v2.0.0) (2023-12-20) 59 | 60 | 61 | ### ⚠ BREAKING CHANGES 62 | 63 | * replace to `httputil.ReverseProxy` ([#1059](https://github.com/manaelproxy/manael/issues/1059)) 64 | * **docker:** remove docker hub ([#1058](https://github.com/manaelproxy/manael/issues/1058)) 65 | * replace to pnpm ([#1047](https://github.com/manaelproxy/manael/issues/1047)) 66 | 67 | ### Features 68 | 69 | * **docker:** remove docker hub ([#1058](https://github.com/manaelproxy/manael/issues/1058)) ([50d85c8](https://github.com/manaelproxy/manael/commit/50d85c8ec507b16dec88cd0c2c38068122aacd0e)) 70 | * replace to `httputil.ReverseProxy` ([#1059](https://github.com/manaelproxy/manael/issues/1059)) ([62a86b6](https://github.com/manaelproxy/manael/commit/62a86b6cf44d1c5e34f613cc3c73be80c516d9bf)), closes [#1054](https://github.com/manaelproxy/manael/issues/1054) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * **release-please:** remove legacy property ([#1048](https://github.com/manaelproxy/manael/issues/1048)) ([515ca51](https://github.com/manaelproxy/manael/commit/515ca516b5e447126634bece4a34188fce71d53b)) 76 | 77 | 78 | ### Code Refactoring 79 | 80 | * replace to pnpm ([#1047](https://github.com/manaelproxy/manael/issues/1047)) ([0226430](https://github.com/manaelproxy/manael/commit/0226430a061f54e66db1b5e91d75ee4013d5a7fb)) 81 | 82 | ### [1.9.1](https://github.com/manaelproxy/manael/compare/v1.9.0...v1.9.1) (2022-04-17) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * **release:** fix path ([#695](https://github.com/manaelproxy/manael/issues/695)) ([1f3f36a](https://github.com/manaelproxy/manael/commit/1f3f36a8c962eb59f8fb891c17235e19a2c3e1aa)) 88 | 89 | ## [1.9.0](https://github.com/manaelproxy/manael/compare/v1.8.5...v1.9.0) (2022-04-17) 90 | 91 | 92 | ### Features 93 | 94 | * **deps:** update libwebp and libaom ([#693](https://github.com/manaelproxy/manael/issues/693)) ([cfbc541](https://github.com/manaelproxy/manael/commit/cfbc541604e3997eb6322d7e035c07cdeeff4aec)) 95 | 96 | 97 | ### Bug Fixes 98 | 99 | * **website:** disable trailing slash ([#681](https://github.com/manaelproxy/manael/issues/681)) ([5882d8a](https://github.com/manaelproxy/manael/commit/5882d8a5c7e6b2a086eddce2c684db8054501f1f)) 100 | * **website:** rename pkg url ([#684](https://github.com/manaelproxy/manael/issues/684)) ([24274a2](https://github.com/manaelproxy/manael/commit/24274a20bac64ecfa557f447fda5446abf0f563c)) 101 | 102 | 103 | ### [1.8.5](https://www.github.com/manaelproxy/manael/compare/v1.8.4...v1.8.5) (2021-05-20) 104 | 105 | 106 | ### Bug Fixes 107 | 108 | * **transport:** fix duplicate variables ([#464](https://www.github.com/manaelproxy/manael/issues/464)) ([dd1f3d5](https://www.github.com/manaelproxy/manael/commit/dd1f3d573e41d94653c1d1e9fbebdd177ce6c6ee)) 109 | 110 | ### [1.8.4](https://www.github.com/manaelproxy/manael/compare/v1.8.3...v1.8.4) (2021-05-20) 111 | 112 | 113 | ### Bug Fixes 114 | 115 | * **transport:** disable avif when png ([#462](https://www.github.com/manaelproxy/manael/issues/462)) ([c293444](https://www.github.com/manaelproxy/manael/commit/c293444dc83670a61d53f5c1f035ec9d649abaa2)) 116 | 117 | ### [1.8.3](https://www.github.com/manaelproxy/manael/compare/v1.8.2...v1.8.3) (2021-05-15) 118 | 119 | 120 | ### Bug Fixes 121 | 122 | * **release:** add -lm to ldflags ([#453](https://www.github.com/manaelproxy/manael/issues/453)) ([ae591af](https://www.github.com/manaelproxy/manael/commit/ae591afe12f97257dc18bd31030535451e8af760)) 123 | 124 | ### [1.8.2](https://www.github.com/manaelproxy/manael/compare/v1.8.1...v1.8.2) (2021-05-14) 125 | 126 | 127 | ### Bug Fixes 128 | 129 | * **transport:** change variable name ([#447](https://www.github.com/manaelproxy/manael/issues/447)) ([7b14d20](https://www.github.com/manaelproxy/manael/commit/7b14d203c38b3d9e1da98614efadadb2bed0c26e)) 130 | 131 | ### [1.8.1](https://www.github.com/manaelproxy/manael/compare/v1.8.0...v1.8.1) (2021-05-14) 132 | 133 | 134 | ### Bug Fixes 135 | 136 | * **release:** make directory for libaom ([#442](https://www.github.com/manaelproxy/manael/issues/442)) ([2790b2f](https://www.github.com/manaelproxy/manael/commit/2790b2f233d496eb21466329f3906e7b917add67)) 137 | 138 | ## [1.8.0](https://www.github.com/manaelproxy/manael/compare/v1.7.1...v1.8.0) (2021-05-14) 139 | 140 | 141 | ### Features 142 | 143 | * **avif:** add support avif ([#372](https://www.github.com/manaelproxy/manael/issues/372)) ([f2721d9](https://www.github.com/manaelproxy/manael/commit/f2721d99bb5f831237e49f8daa7994874e9efee6)) 144 | * **i18n:** add japanese translations ([#408](https://www.github.com/manaelproxy/manael/issues/408)) ([d4034b4](https://www.github.com/manaelproxy/manael/commit/d4034b4a4812d4fde952f4ffcef8900a28544e3b)) 145 | * **transprot:** add flag for avif ([#441](https://www.github.com/manaelproxy/manael/issues/441)) ([37cea0f](https://www.github.com/manaelproxy/manael/commit/37cea0fab3f45fb58fe90dbab103bc24e09aa3d8)) 146 | * **website:** enable docsearch ([#409](https://www.github.com/manaelproxy/manael/issues/409)) ([959d83a](https://www.github.com/manaelproxy/manael/commit/959d83a000458e0854c25666600bc23d823487b0)) 147 | 148 | 149 | ### Bug Fixes 150 | 151 | * **deps:** change release tag to latest ([#356](https://www.github.com/manaelproxy/manael/issues/356)) ([95b59cb](https://www.github.com/manaelproxy/manael/commit/95b59cb5426f7b0daee491ead9ad5a2eeb9e3c24)) 152 | * **website:** add missing scripts ([#407](https://www.github.com/manaelproxy/manael/issues/407)) ([d3c4bb1](https://www.github.com/manaelproxy/manael/commit/d3c4bb1f274ce5fd047106027bdc0ef354822bee)) 153 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@manael.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | Manael welcomes your contribution! 4 | 5 | ## Run test 6 | 7 | ```console 8 | $ go test -v ./... 9 | ``` 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Start by building the application. 2 | FROM golang:1.24.3-bookworm@sha256:89a04cc2e2fbafef82d4a45523d4d4ae4ecaf11a197689036df35fef3bde444a AS build 3 | 4 | ENV LIBAOM_VERSION=3.8.0 5 | ENV LIBWEBP_VERSION=1.2.4 6 | 7 | RUN \ 8 | apt-get update && \ 9 | apt-get install -y cmake yasm && \ 10 | \ 11 | mkdir -p /tmp/src && \ 12 | cd /tmp/src && \ 13 | wget -O libwebp.tar.gz https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-${LIBWEBP_VERSION}.tar.gz && \ 14 | tar -xzf libwebp.tar.gz -C /tmp/src && \ 15 | rm libwebp.tar.gz && \ 16 | cd /tmp/src/libwebp-${LIBWEBP_VERSION} && \ 17 | ./configure --prefix /tmp/libwebp && \ 18 | make -j4 && \ 19 | make install && \ 20 | \ 21 | mkdir -p /tmp/src && \ 22 | cd /tmp/src && \ 23 | wget -O libaom.tar.gz https://storage.googleapis.com/aom-releases/libaom-${LIBAOM_VERSION}.tar.gz && \ 24 | mkdir -p /tmp/src/libaom-${LIBAOM_VERSION} && \ 25 | tar -xzf libaom.tar.gz -C /tmp/src && \ 26 | rm libaom.tar.gz && \ 27 | mkdir -p /tmp/src/aom_build && \ 28 | cd /tmp/src/aom_build && \ 29 | cmake /tmp/src/libaom-${LIBAOM_VERSION} -DCMAKE_INSTALL_PREFIX=/tmp/libaom && \ 30 | make -j4 && \ 31 | make install 32 | 33 | WORKDIR /go/src/manael 34 | COPY . . 35 | 36 | ENV CGO_CFLAGS="-I/tmp/libwebp/include -I/tmp/libaom/include" 37 | ENV CGO_LDFLAGS="-L/tmp/libwebp/lib -lwebp -L/tmp/libaom/lib -laom -lm" 38 | 39 | RUN go mod download 40 | RUN go build -ldflags '-extldflags "-static"' -o /go/bin/manael ./cmd/manael 41 | 42 | # Now copy it into our base image. 43 | FROM gcr.io/distroless/base-debian12@sha256:27769871031f67460f1545a52dfacead6d18a9f197db77110cfc649ca2a91f44 44 | COPY --from=build /go/bin/manael / 45 | CMD ["/manael"] 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Yamagishi Kazutoshi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: bin/manael 3 | 4 | bin/%: cmd/%/main.go *.go internal/**/*.go 5 | @mkdir -p bin 6 | go build -o $@ $< 7 | 8 | .PHONY: clean 9 | clean: 10 | rm -r bin 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Manael 2 | 3 | [![GoDoc](https://godoc.org/manael.org/x/manael?status.svg)](https://godoc.org/manael.org/x/manael) 4 | [![Go](https://github.com/manaelproxy/manael/workflows/Go/badge.svg)](https://github.com/manaelproxy/manael/actions?query=workflow%3AGo) 5 | [![Codecov](https://codecov.io/gh/manaelproxy/manael/branch/main/graph/badge.svg)](https://codecov.io/gh/manaelproxy/manael) 6 | 7 | Manael is a simple HTTP proxy for processing images. 8 | 9 | ## Installation 10 | 11 | - [Download latest binary](https://github.com/manaelproxy/manael/releases/latest) 12 | 13 | ## Usage 14 | 15 | ```console 16 | $ manael -http=:8080 -upstream_url=http://localhost:9000 17 | ``` 18 | 19 | ## License 20 | 21 | [MIT](/LICENSE) 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Manael does not fix any vulnerabilities other than those in the latest major version. If you find a vulnerability, report to . 4 | -------------------------------------------------------------------------------- /cmd/manael/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Yamagishi Kazutoshi 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | // Manael is a proxy server for processing images. 22 | // 23 | // Usage: 24 | // 25 | // manael [arguments] 26 | // 27 | // Example: 28 | // 29 | // manael -http=:8080 -upstream_url=http://localhost:9000 30 | package main 31 | -------------------------------------------------------------------------------- /cmd/manael/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Yamagishi Kazutoshi 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "flag" 25 | "fmt" 26 | "log" 27 | "net/http" 28 | "net/url" 29 | "os" 30 | 31 | "github.com/gorilla/handlers" 32 | "manael.org/x/manael/v2" 33 | ) 34 | 35 | const ( 36 | // DefaultPort is returned by default port. 37 | DefaultPort = 8080 38 | 39 | // DefaultUpstreamURL is returned by default upstream URL. 40 | DefaultUpstreamURL = "http://localhost:9000" 41 | ) 42 | 43 | type config struct { 44 | httpAddr string 45 | upstreamURL string 46 | } 47 | 48 | func main() { 49 | fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 50 | 51 | conf := config{} 52 | 53 | fs.StringVar(&conf.httpAddr, "http", "", "HTTP server address") 54 | fs.StringVar(&conf.upstreamURL, "upstream_url", "", "Upstream URL for processing images") 55 | 56 | if err := fs.Parse(os.Args[1:]); err != nil { 57 | log.Fatalf("Error: %v", err) 58 | } 59 | 60 | if conf.httpAddr == "" { 61 | port := os.Getenv("PORT") 62 | 63 | if port != "" { 64 | conf.httpAddr = fmt.Sprintf(":%s", port) 65 | } else { 66 | conf.httpAddr = fmt.Sprintf(":%d", DefaultPort) 67 | } 68 | } 69 | 70 | if conf.upstreamURL == "" { 71 | u := os.Getenv("MANAEL_UPSTREAM_URL") 72 | 73 | if u != "" { 74 | conf.upstreamURL = u 75 | } else { 76 | conf.upstreamURL = DefaultUpstreamURL 77 | } 78 | } 79 | 80 | upstreamURL, err := url.Parse(conf.upstreamURL) 81 | if err != nil { 82 | log.Fatalf("Error: %v", err) 83 | } 84 | 85 | var handler http.Handler 86 | handler = manael.NewServeProxy(upstreamURL) 87 | handler = handlers.CombinedLoggingHandler(os.Stdout, handler) 88 | 89 | if err := http.ListenAndServe(conf.httpAddr, handler); err != nil { 90 | log.Fatalf("Error: %v", err) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@commitlint/config-conventional' 3 | } 4 | -------------------------------------------------------------------------------- /decode.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Yamagishi Kazutoshi 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package manael 22 | 23 | import ( 24 | "errors" 25 | "image" 26 | "io" 27 | 28 | // register jpeg 29 | _ "image/jpeg" 30 | // register png 31 | _ "image/png" 32 | ) 33 | 34 | // Decode returns an image.Image. 35 | func Decode(r io.Reader) (image.Image, error) { 36 | img, _, err := image.Decode(r) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | switch img.(type) { 42 | case *image.Gray, *image.NRGBA, *image.RGBA: 43 | return img, nil 44 | case *image.CMYK, *image.NRGBA64, *image.Paletted, *image.RGBA64, *image.YCbCr: 45 | bounds := img.Bounds() 46 | newImg := image.NewRGBA(bounds) 47 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 48 | for x := bounds.Min.X; x < bounds.Max.X; x++ { 49 | newImg.Set(x, y, img.At(x, y)) 50 | } 51 | } 52 | 53 | return newImg, nil 54 | } 55 | 56 | return nil, errors.New("Not supported image format") 57 | } 58 | -------------------------------------------------------------------------------- /docsearch.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_settings": { 3 | "separatorsToIndex": "_", 4 | "attributesForFaceting": ["language", "version", "type", "docusaurus_tag"], 5 | "attributesToRetrieve": [ 6 | "hierarchy", 7 | "content", 8 | "anchor", 9 | "url", 10 | "url_without_anchor", 11 | "type" 12 | ] 13 | }, 14 | "index_name": "docusaurus", 15 | "selectors": { 16 | "lvl0": { 17 | "selector": "(//ul[contains(@class,'menu__list')]//a[contains(@class, 'menu__link menu__link--sublist menu__link--active')]/text() | //nav[contains(@class, 'navbar')]//a[contains(@class, 'navbar__link--active')]/text())[last()]", 18 | "type": "xpath", 19 | "global": true, 20 | "default_value": "Documentation" 21 | }, 22 | "lvl1": "header h1", 23 | "lvl2": "article h2", 24 | "lvl3": "article h3", 25 | "lvl4": "article h4", 26 | "lvl5": "article h5, article td:first-child", 27 | "lvl6": "article h6", 28 | "text": "article p, article li, article td:last-child" 29 | }, 30 | "sitemap_alternate_links": true, 31 | "sitemap_urls": ["https://manael.org/sitemap.xml"], 32 | "start_urls": ["https://manael.org/"], 33 | "strip_chars": " .,;:#", 34 | "stop_urls": [] 35 | } 36 | -------------------------------------------------------------------------------- /encode.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Yamagishi Kazutoshi 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package manael 22 | 23 | import ( 24 | "errors" 25 | "image" 26 | "io" 27 | 28 | "github.com/Kagami/go-avif" 29 | "github.com/harukasan/go-libwebp/webp" 30 | ) 31 | 32 | // Encode writes the Image m to w in the format specified by Content-Type t. 33 | func Encode(w io.Writer, m image.Image, t string) error { 34 | switch t { 35 | case "image/avif": 36 | opts := avif.Options{ 37 | Quality: 20, 38 | Speed: 8, 39 | } 40 | 41 | err := avif.Encode(w, m, &opts) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | return nil 47 | case "image/webp": 48 | c, err := webp.ConfigPreset(webp.PresetDefault, 90) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | switch img := m.(type) { 54 | case *image.Gray: 55 | err = webp.EncodeGray(w, img, c) 56 | if err != nil { 57 | return err 58 | } 59 | case *image.RGBA, *image.NRGBA: 60 | err = webp.EncodeRGBA(w, img, c) 61 | if err != nil { 62 | return err 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | 69 | return errors.New("Not supported image type") 70 | } 71 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Yamagishi Kazutoshi 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package manael_test 22 | 23 | import ( 24 | "log" 25 | "net/http" 26 | "net/url" 27 | 28 | "manael.org/x/manael/v2" 29 | ) 30 | 31 | func ExampleNewServeProxy() { 32 | u, err := url.Parse("http://localhost:9000") 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | p := manael.NewServeProxy(u) 38 | 39 | if err := http.ListenAndServe(":8080", p); err != nil { 40 | log.Fatal(err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module manael.org/x/manael/v2 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/Kagami/go-avif v0.1.0 7 | github.com/gorilla/handlers v1.5.2 8 | github.com/harukasan/go-libwebp v0.0.0-20220408054828-61eedf90d768 9 | ) 10 | 11 | require github.com/felixge/httpsnoop v1.0.3 // indirect 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Kagami/go-avif v0.1.0 h1:8GHAGLxCdFfhpd4Zg8j1EqO7rtcQNenxIDerC/uu68w= 2 | github.com/Kagami/go-avif v0.1.0/go.mod h1:OPmPqzNdQq3+sXm0HqaUJQ9W/4k+Elbc3RSfJUemDKA= 3 | github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= 4 | github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 5 | github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= 6 | github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= 7 | github.com/harukasan/go-libwebp v0.0.0-20220408054828-61eedf90d768 h1:6kHMeZ8a/fyiHlsR4fwBWaVh3KSBgsMrupXAA5pChfc= 8 | github.com/harukasan/go-libwebp v0.0.0-20220408054828-61eedf90d768/go.mod h1:ldE44ycRKJi6dVHIWnbUlEJqHQUhK5gJ4TKIfAwFbCg= 9 | -------------------------------------------------------------------------------- /internal/testutil/testutil.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Yamagishi Kazutoshi 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package testutil 22 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // '*.go': 'go fmt', 3 | '*.{js,jsx,ts,tsx}': 'eslint --fix', 4 | '*.{json,yml}': 'prettier --write' 5 | } 6 | -------------------------------------------------------------------------------- /manael.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Yamagishi Kazutoshi 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | // Package manael provides HTTP handler for processing images. 22 | package manael 23 | 24 | import ( 25 | "bytes" 26 | "io" 27 | "log" 28 | "net/http" 29 | "net/http/httputil" 30 | "net/url" 31 | "os" 32 | "strconv" 33 | "strings" 34 | ) 35 | 36 | func setVaryHeader(res *http.Response) { 37 | keys := []string{"Accept"} 38 | for _, v := range strings.Split(res.Header.Get("Vary"), ",") { 39 | v = strings.TrimSpace(v) 40 | 41 | if v != "" && !strings.EqualFold(v, "Accept") { 42 | keys = append(keys, v) 43 | } 44 | } 45 | 46 | res.Header.Set("Vary", strings.Join(keys[:], ", ")) 47 | } 48 | 49 | func avifEnabled(res *http.Response) bool { 50 | contentType := res.Header.Get("Content-Type") 51 | 52 | return os.Getenv("MANAEL_ENABLE_AVIF") == "true" && contentType != "image/png" 53 | } 54 | 55 | func scanAcceptHeader(res *http.Response) string { 56 | accepts := res.Request.Header.Get("Accept") 57 | 58 | for _, v := range strings.Split(accepts, ",") { 59 | t := strings.TrimSpace(v) 60 | 61 | if avifEnabled(res) && strings.HasPrefix(t, "image/avif") { 62 | return "image/avif" 63 | } else if strings.HasPrefix(t, "image/webp") { 64 | return "image/webp" 65 | } 66 | } 67 | 68 | return "*/*" 69 | } 70 | 71 | func check(res *http.Response) string { 72 | if res.Request.Method != http.MethodGet && res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNotModified { 73 | return "*/*" 74 | } 75 | 76 | if s := res.Header.Get("Cache-Control"); s != "" { 77 | for _, v := range strings.Split(s, ",") { 78 | if strings.TrimSpace(v) == "no-transform" { 79 | return "*/*" 80 | } 81 | } 82 | } 83 | 84 | t := res.Header.Get("Content-Type") 85 | 86 | if t != "image/jpeg" && t != "image/png" { 87 | return "*/*" 88 | } 89 | 90 | return scanAcceptHeader(res) 91 | } 92 | 93 | func convert(src io.Reader, t string) (*bytes.Buffer, error) { 94 | img, err := Decode(src) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | buf := bytes.NewBuffer(nil) 100 | 101 | err = Encode(buf, img, t) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | return buf, nil 107 | } 108 | 109 | func modifyResponse(res *http.Response) error { 110 | res.Header.Set("Server", "Manael") 111 | 112 | setVaryHeader(res) 113 | 114 | typ := check(res) 115 | if typ == "*/*" { 116 | return nil 117 | } 118 | 119 | defer res.Body.Close() 120 | 121 | p := bytes.NewBuffer(nil) 122 | b := io.TeeReader(res.Body, p) 123 | 124 | buf, err := convert(b, typ) 125 | if err != nil { 126 | body := io.MultiReader(p, res.Body) 127 | 128 | res.Body = io.NopCloser(body) 129 | log.Printf("error: %v\n", err) 130 | 131 | return nil 132 | } 133 | 134 | res.Body = io.NopCloser(buf) 135 | 136 | res.Header.Set("Content-Type", typ) 137 | res.Header.Set("Content-Length", strconv.Itoa(buf.Len())) 138 | 139 | if res.Header.Get("Accept-Ranges") != "" { 140 | res.Header.Del("Accept-Ranges") 141 | } 142 | 143 | return nil 144 | } 145 | 146 | // NewServeProxy returns a new Proxy given a upstream URL 147 | func NewServeProxy(u *url.URL) http.Handler { 148 | return &httputil.ReverseProxy{ 149 | Rewrite: func(r *httputil.ProxyRequest) { 150 | r.Out.Header["X-Forwarded-For"] = r.In.Header["X-Forwarded-For"] 151 | 152 | r.SetXForwarded() 153 | r.SetURL(u) 154 | }, 155 | ModifyResponse: modifyResponse, 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /manael_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Yamagishi Kazutoshi 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | // Package manael provides HTTP handler for processing images. 22 | package manael_test 23 | 24 | import ( 25 | "crypto/sha256" 26 | "fmt" 27 | "io" 28 | "net" 29 | "net/http" 30 | "net/http/httptest" 31 | "net/url" 32 | "os" 33 | "testing" 34 | "time" 35 | 36 | "manael.org/x/manael/v2" 37 | ) 38 | 39 | var basicTests = []struct { 40 | path string 41 | statusCode int 42 | }{ 43 | { 44 | "/logo.png", 45 | 200, 46 | }, 47 | { 48 | "/404.html", 49 | 404, 50 | }, 51 | } 52 | 53 | func TestNewServeProxy(t *testing.T) { 54 | mux := http.NewServeMux() 55 | mux.HandleFunc("/logo.png", func(w http.ResponseWriter, r *http.Request) { 56 | http.ServeFile(w, r, "testdata/logo.png") 57 | }) 58 | 59 | ts := httptest.NewServer(mux) 60 | defer ts.Close() 61 | 62 | u, err := url.Parse(ts.URL) 63 | if err != nil { 64 | t.Error(err) 65 | } 66 | 67 | p := manael.NewServeProxy(u) 68 | 69 | for _, tc := range basicTests { 70 | req := httptest.NewRequest(http.MethodGet, "https://manael.test"+tc.path, nil) 71 | 72 | w := httptest.NewRecorder() 73 | 74 | p.ServeHTTP(w, req) 75 | 76 | resp := w.Result() 77 | defer resp.Body.Close() 78 | 79 | if got, want := resp.StatusCode, tc.statusCode; got != want { 80 | t.Errorf("Status code is %d, want %d", got, want) 81 | } 82 | } 83 | } 84 | 85 | var varyTests = []struct { 86 | path string 87 | accept string 88 | vary string 89 | }{ 90 | { 91 | "/logo.png", 92 | "image/webp", 93 | "Accept", 94 | }, 95 | { 96 | "/logo2.png", 97 | "image/webp", 98 | "Accept, Origin, Accept-Encoding", 99 | }, 100 | { 101 | "/logo2.png", 102 | "image/png", 103 | "Accept, Origin, Accept-Encoding", 104 | }, 105 | } 106 | 107 | func TestNewServeProxy_vary(t *testing.T) { 108 | mux := http.NewServeMux() 109 | mux.HandleFunc("/logo.png", func(w http.ResponseWriter, r *http.Request) { 110 | w.Header().Set("Server", "OriginServer") 111 | 112 | http.ServeFile(w, r, "testdata/logo.png") 113 | }) 114 | mux.HandleFunc("/logo2.png", func(w http.ResponseWriter, r *http.Request) { 115 | w.Header().Set("Vary", "Origin, Accept-Encoding") 116 | 117 | http.ServeFile(w, r, "testdata/logo.png") 118 | }) 119 | 120 | ts := httptest.NewServer(mux) 121 | defer ts.Close() 122 | 123 | u, err := url.Parse(ts.URL) 124 | if err != nil { 125 | t.Error(err) 126 | } 127 | 128 | p := manael.NewServeProxy(u) 129 | 130 | for _, tc := range varyTests { 131 | req := httptest.NewRequest(http.MethodGet, ts.URL+tc.path, nil) 132 | req.Header.Set("Accept", tc.accept) 133 | 134 | w := httptest.NewRecorder() 135 | 136 | p.ServeHTTP(w, req) 137 | 138 | resp := w.Result() 139 | defer resp.Body.Close() 140 | 141 | if got, want := resp.Header.Get("Server"), "Manael"; got != want { 142 | t.Errorf("Server is %s, want %s", got, want) 143 | } 144 | 145 | if got, want := resp.Header.Get("Vary"), tc.vary; got != want { 146 | t.Errorf(`Vary is "%s", want "%s"`, got, want) 147 | } 148 | } 149 | } 150 | 151 | func TestNewServeProxy_badGateway(t *testing.T) { 152 | u, err := url.Parse("http://missing.test") 153 | if err != nil { 154 | t.Error(err) 155 | } 156 | 157 | p := manael.NewServeProxy(u) 158 | 159 | req := httptest.NewRequest(http.MethodGet, "https://manael.invalid/test.png", nil) 160 | 161 | w := httptest.NewRecorder() 162 | 163 | p.ServeHTTP(w, req) 164 | 165 | resp := w.Result() 166 | defer resp.Body.Close() 167 | 168 | if got, want := resp.Header.Get("Server"), ""; got != want { 169 | t.Errorf(`Server is "%s", want "%s"`, got, want) 170 | } 171 | 172 | if got, want := resp.StatusCode, 502; got != want { 173 | t.Errorf("Status code is %d, want %d", got, want) 174 | } 175 | } 176 | 177 | var convertTests = []struct { 178 | accept string 179 | path string 180 | statusCode int 181 | contentType string 182 | format string 183 | }{ 184 | { 185 | "image/*,*/*;q=0.8", 186 | "/logo.png", 187 | http.StatusOK, 188 | "image/png", 189 | "image/png", 190 | }, 191 | { 192 | "image/webp,image/*,*/*;q=0.8", 193 | "/logo.png", 194 | http.StatusOK, 195 | "image/webp", 196 | "image/webp", 197 | }, 198 | { 199 | "image/*,*/*", 200 | "/photo.jpeg", 201 | http.StatusOK, 202 | "image/jpeg", 203 | "image/jpeg", 204 | }, 205 | { 206 | "image/webp,image/*,*/*;q=0.8", 207 | "/photo.jpeg", 208 | http.StatusOK, 209 | "image/webp", 210 | "image/webp", 211 | }, 212 | { 213 | "image/*,*/*;q=0.8", 214 | "/empty.gif", 215 | http.StatusOK, 216 | "image/gif", 217 | "image/gif", 218 | }, 219 | { 220 | "image/webp,image/*,*/*;q=0.8", 221 | "/empty.gif", 222 | http.StatusOK, 223 | "image/gif", 224 | "image/gif", 225 | }, 226 | { 227 | "image/webp,image/*,*/*", 228 | "/empty.txt", 229 | http.StatusOK, 230 | "text/plain; charset=utf-8", 231 | "text/plain; charset=utf-8", 232 | }, 233 | { 234 | "image/webp,image/*,*/*", 235 | "/invalid.png", 236 | http.StatusOK, 237 | "image/png", 238 | "text/plain; charset=utf-8", 239 | }, 240 | } 241 | 242 | func TestNewServeProxy_convert(t *testing.T) { 243 | mux := http.NewServeMux() 244 | mux.HandleFunc("/logo.png", func(w http.ResponseWriter, r *http.Request) { 245 | http.ServeFile(w, r, "testdata/logo.png") 246 | }) 247 | mux.HandleFunc("/photo.jpeg", func(w http.ResponseWriter, r *http.Request) { 248 | http.ServeFile(w, r, "testdata/photo.jpeg") 249 | }) 250 | mux.HandleFunc("/empty.gif", func(w http.ResponseWriter, r *http.Request) { 251 | http.ServeFile(w, r, "testdata/empty.gif") 252 | }) 253 | mux.HandleFunc("/empty.txt", func(w http.ResponseWriter, r *http.Request) { 254 | http.ServeFile(w, r, "testdata/empty.txt") 255 | }) 256 | mux.HandleFunc("/invalid.png", func(w http.ResponseWriter, r *http.Request) { 257 | http.ServeFile(w, r, "testdata/invalid.png") 258 | }) 259 | 260 | ts := httptest.NewServer(mux) 261 | defer ts.Close() 262 | 263 | u, err := url.Parse(ts.URL) 264 | if err != nil { 265 | t.Error(err) 266 | } 267 | 268 | p := manael.NewServeProxy(u) 269 | 270 | for _, tc := range convertTests { 271 | req := httptest.NewRequest(http.MethodGet, "https://manael.test"+tc.path, nil) 272 | req.Header.Set("Accept", tc.accept) 273 | 274 | w := httptest.NewRecorder() 275 | 276 | p.ServeHTTP(w, req) 277 | 278 | resp := w.Result() 279 | defer resp.Body.Close() 280 | 281 | if got, want := resp.StatusCode, tc.statusCode; got != want { 282 | t.Errorf("Status Code is %d, want %d", got, want) 283 | } 284 | 285 | if got, want := resp.Header.Get("Content-Type"), tc.contentType; got != want { 286 | t.Errorf("Content-Type is %s, want %s", got, want) 287 | } 288 | 289 | body, err := io.ReadAll(resp.Body) 290 | if err != nil { 291 | t.Error(err) 292 | } 293 | 294 | if got, want := http.DetectContentType(body), tc.format; got != want { 295 | t.Errorf("Detect format is %s, want %s", got, want) 296 | } 297 | } 298 | } 299 | 300 | var ifModifiedSinceTests = []struct { 301 | path string 302 | modtime time.Time 303 | statusCode int 304 | contentLength int 305 | }{ 306 | { 307 | "/logo.png", 308 | time.Date(2018, time.June, 30, 14, 4, 31, 0, time.UTC), 309 | http.StatusNotModified, 310 | 0, 311 | }, 312 | { 313 | "/logo.png", 314 | time.Time{}, 315 | http.StatusOK, 316 | 4090, 317 | }, 318 | { 319 | "/logo.png", 320 | time.Date(2018, time.June, 30, 14, 3, 31, 0, time.UTC), 321 | http.StatusOK, 322 | 4090, 323 | }, 324 | { 325 | "/logo.png", 326 | time.Date(2018, time.June, 30, 14, 5, 31, 0, time.UTC), 327 | http.StatusNotModified, 328 | 0, 329 | }, 330 | } 331 | 332 | func TestNewServeProxy_ifModifiedSince(t *testing.T) { 333 | mux := http.NewServeMux() 334 | mux.HandleFunc("/logo.png", func(w http.ResponseWriter, r *http.Request) { 335 | modtime := time.Date(2018, time.June, 30, 14, 4, 31, 0, time.UTC) 336 | 337 | ims := r.Header.Get("If-Modified-Since") 338 | t, _ := time.Parse(http.TimeFormat, ims) 339 | 340 | if t.IsZero() || t.Before(modtime) { 341 | r.Header.Del("If-Modified-Since") 342 | http.ServeFile(w, r, "testdata/logo.png") 343 | } else { 344 | w.WriteHeader(http.StatusNotModified) 345 | } 346 | }) 347 | 348 | ts := httptest.NewServer(mux) 349 | defer ts.Close() 350 | 351 | u, err := url.Parse(ts.URL) 352 | if err != nil { 353 | t.Error(err) 354 | } 355 | 356 | p := manael.NewServeProxy(u) 357 | 358 | for _, tc := range ifModifiedSinceTests { 359 | req := httptest.NewRequest(http.MethodGet, "https://manael.test"+tc.path, nil) 360 | 361 | if !tc.modtime.IsZero() { 362 | req.Header.Set("If-Modified-Since", tc.modtime.Format(http.TimeFormat)) 363 | } 364 | 365 | w := httptest.NewRecorder() 366 | 367 | p.ServeHTTP(w, req) 368 | 369 | resp := w.Result() 370 | defer resp.Body.Close() 371 | 372 | if got, want := resp.StatusCode, tc.statusCode; got != want { 373 | t.Errorf("Status Code is %d, want %d (%s)", got, want, tc.modtime) 374 | } 375 | 376 | body, _ := io.ReadAll(resp.Body) 377 | 378 | if got, want := len(body), tc.contentLength; got != want { 379 | t.Errorf("Response body is %d bytes, want %d", got, want) 380 | } 381 | } 382 | } 383 | 384 | var ifNoneMatchTests = []struct { 385 | path string 386 | etag string 387 | statusCode int 388 | contentLength int 389 | }{ 390 | { 391 | "/logo.png", 392 | `W/"fcaec3a55087c997f24ba2a70383ed9b7607fd85f0ae2e0dccb5ec094c75f009"`, 393 | http.StatusNotModified, 394 | 0, 395 | }, 396 | { 397 | "/logo.png", 398 | "", 399 | http.StatusOK, 400 | 4090, 401 | }, 402 | { 403 | "/logo.png", 404 | "invalidETag", 405 | http.StatusOK, 406 | 4090, 407 | }, 408 | } 409 | 410 | func TestNewServeProxy_ifNoneMatch(t *testing.T) { 411 | mux := http.NewServeMux() 412 | mux.HandleFunc("/logo.png", func(w http.ResponseWriter, r *http.Request) { 413 | if r.Header.Get("If-None-Match") != fmt.Sprintf(`W/"%x"`, sha256.Sum256([]byte("etag"))) { 414 | http.ServeFile(w, r, "testdata/logo.png") 415 | } else { 416 | w.WriteHeader(http.StatusNotModified) 417 | } 418 | }) 419 | 420 | ts := httptest.NewServer(mux) 421 | defer ts.Close() 422 | 423 | u, err := url.Parse(ts.URL) 424 | if err != nil { 425 | t.Error(err) 426 | } 427 | 428 | p := manael.NewServeProxy(u) 429 | 430 | for _, tc := range ifNoneMatchTests { 431 | req := httptest.NewRequest(http.MethodGet, "https://manael.local"+tc.path, nil) 432 | 433 | if tc.etag != "" { 434 | req.Header.Set("If-None-Match", tc.etag) 435 | } 436 | 437 | w := httptest.NewRecorder() 438 | 439 | p.ServeHTTP(w, req) 440 | 441 | resp := w.Result() 442 | defer resp.Body.Close() 443 | 444 | if got, want := resp.StatusCode, tc.statusCode; got != want { 445 | t.Errorf("Status Code is %d, want %d", got, want) 446 | } 447 | 448 | body, _ := io.ReadAll(resp.Body) 449 | 450 | if got, want := len(body), tc.contentLength; got != want { 451 | t.Errorf("Response body is %d bytes, want %d", got, want) 452 | } 453 | } 454 | } 455 | 456 | var acceptRangesTests = []struct { 457 | path string 458 | accept string 459 | acceptRanges string 460 | }{ 461 | { 462 | "/logo.png", 463 | "image/webp,image/*,*/*;q=0.8", 464 | "", 465 | }, 466 | { 467 | "/logo.png", 468 | "image/*,*/*;q=0.8", 469 | "bytes", 470 | }, 471 | } 472 | 473 | func TestNewServeProxy_acceptRanges(t *testing.T) { 474 | mux := http.NewServeMux() 475 | mux.HandleFunc("/logo.png", func(w http.ResponseWriter, r *http.Request) { 476 | w.Header().Set("Accept-Ranges", "bytes") 477 | http.ServeFile(w, r, "testdata/logo.png") 478 | }) 479 | 480 | ts := httptest.NewServer(mux) 481 | defer ts.Close() 482 | 483 | u, err := url.Parse(ts.URL) 484 | if err != nil { 485 | t.Error(err) 486 | } 487 | 488 | p := manael.NewServeProxy(u) 489 | 490 | for _, tc := range acceptRangesTests { 491 | req := httptest.NewRequest(http.MethodGet, tc.path, nil) 492 | req.Header.Set("Accept", tc.accept) 493 | 494 | w := httptest.NewRecorder() 495 | 496 | p.ServeHTTP(w, req) 497 | 498 | resp := w.Result() 499 | defer resp.Body.Close() 500 | 501 | if got, want := resp.Header.Get("Accept-Ranges"), tc.acceptRanges; got != want { 502 | t.Errorf(`Accept-Ranges is "%s", want "%s"`, got, want) 503 | } 504 | } 505 | } 506 | 507 | var noTransformTests = []struct { 508 | path string 509 | contentType string 510 | format string 511 | }{ 512 | { 513 | "/logo.png", 514 | "image/webp", 515 | "image/webp", 516 | }, 517 | { 518 | "/logo.png?raw=1", 519 | "image/png", 520 | "image/png", 521 | }, 522 | { 523 | "/photo.jpeg", 524 | "image/webp", 525 | "image/webp", 526 | }, 527 | { 528 | "/photo.jpeg?raw=1", 529 | "image/jpeg", 530 | "image/jpeg", 531 | }, 532 | } 533 | 534 | func TestNewServeProxy_noTransform(t *testing.T) { 535 | mux := http.NewServeMux() 536 | mux.HandleFunc("/logo.png", func(w http.ResponseWriter, r *http.Request) { 537 | if r.URL.Query().Get("raw") == "1" { 538 | w.Header().Set("Cache-Control", "no-transform") 539 | } 540 | 541 | http.ServeFile(w, r, "testdata/logo.png") 542 | }) 543 | mux.HandleFunc("/photo.jpeg", func(w http.ResponseWriter, r *http.Request) { 544 | if r.URL.Query().Get("raw") == "1" { 545 | w.Header().Set("Cache-Control", "no-transform") 546 | } 547 | 548 | http.ServeFile(w, r, "testdata/photo.jpeg") 549 | }) 550 | 551 | ts := httptest.NewServer(mux) 552 | defer ts.Close() 553 | 554 | u, err := url.Parse(ts.URL) 555 | if err != nil { 556 | t.Error(err) 557 | } 558 | 559 | p := manael.NewServeProxy(u) 560 | 561 | for _, tc := range noTransformTests { 562 | req := httptest.NewRequest(http.MethodGet, tc.path, nil) 563 | req.Header.Set("Accept", "image/webp,image/*,*/*;q=0.8") 564 | 565 | w := httptest.NewRecorder() 566 | 567 | p.ServeHTTP(w, req) 568 | 569 | resp := w.Result() 570 | defer resp.Body.Close() 571 | 572 | if got, want := resp.Header.Get("Content-Type"), tc.contentType; got != want { 573 | t.Errorf("Content-Type is %s, want %s", got, want) 574 | } 575 | 576 | body, err := io.ReadAll(resp.Body) 577 | if err != nil { 578 | t.Error(err) 579 | } 580 | 581 | if got, want := http.DetectContentType(body), tc.format; got != want { 582 | t.Errorf("Detect format is %s, want %s", got, want) 583 | } 584 | } 585 | } 586 | 587 | var xForwardedForTests = []struct { 588 | path string 589 | xff string 590 | }{ 591 | { 592 | "/xff.txt", 593 | "203.0.113.6, 192.0.2.44", 594 | }, 595 | } 596 | 597 | func TestNewServeProxy_xForwardedFor(t *testing.T) { 598 | mux := http.NewServeMux() 599 | mux.HandleFunc("/xff.txt", func(w http.ResponseWriter, r *http.Request) { 600 | io.WriteString(w, r.Header.Get("X-Forwarded-For")) 601 | }) 602 | 603 | ts := httptest.NewServer(mux) 604 | defer ts.Close() 605 | 606 | u, err := url.Parse(ts.URL) 607 | if err != nil { 608 | t.Error(err) 609 | } 610 | 611 | p := manael.NewServeProxy(u) 612 | 613 | for _, tc := range xForwardedForTests { 614 | req := httptest.NewRequest(http.MethodGet, "https://manael.test"+tc.path, nil) 615 | req.Header.Set("X-Forwarded-For", tc.xff) 616 | 617 | w := httptest.NewRecorder() 618 | 619 | p.ServeHTTP(w, req) 620 | 621 | resp := w.Result() 622 | defer resp.Body.Close() 623 | 624 | xff, err := io.ReadAll(resp.Body) 625 | if err != nil { 626 | t.Fatal(err) 627 | } 628 | 629 | remoteIP, _, err := net.SplitHostPort(req.RemoteAddr) 630 | if err != nil { 631 | t.Fatal(err) 632 | } 633 | 634 | if got, want := string(xff), fmt.Sprintf("%s, %s", tc.xff, remoteIP); got != want { 635 | t.Errorf(`X-Forwarded-For is "%s", want "%s"`, got, want) 636 | } 637 | } 638 | } 639 | 640 | var avifTests = []struct { 641 | accept string 642 | path string 643 | statusCode int 644 | contentType string 645 | }{ 646 | { 647 | "image/avif,image/webp,image/*,*/*;q=0.8", 648 | "/photo.jpeg", 649 | http.StatusOK, 650 | "image/avif", 651 | }, 652 | { 653 | "image/avif,image/webp,image/*,*/*;q=0.8", 654 | "/logo.png", 655 | http.StatusOK, 656 | "image/webp", 657 | }, 658 | } 659 | 660 | func TestNewServeProxy_avif(t *testing.T) { 661 | if os.Getenv("MANAEL_ENABLE_AVIF") != "true" { 662 | t.Skip("Skipping test when avif disabled.") 663 | } 664 | 665 | mux := http.NewServeMux() 666 | mux.HandleFunc("/logo.png", func(w http.ResponseWriter, r *http.Request) { 667 | http.ServeFile(w, r, "testdata/logo.png") 668 | }) 669 | mux.HandleFunc("/photo.jpeg", func(w http.ResponseWriter, r *http.Request) { 670 | http.ServeFile(w, r, "testdata/photo.jpeg") 671 | }) 672 | 673 | ts := httptest.NewServer(mux) 674 | defer ts.Close() 675 | 676 | u, err := url.Parse(ts.URL) 677 | if err != nil { 678 | t.Error(err) 679 | } 680 | 681 | p := manael.NewServeProxy(u) 682 | 683 | for _, tc := range avifTests { 684 | req := httptest.NewRequest(http.MethodGet, "https://manael.test"+tc.path, nil) 685 | req.Header.Set("Accept", tc.accept) 686 | 687 | w := httptest.NewRecorder() 688 | 689 | p.ServeHTTP(w, req) 690 | 691 | resp := w.Result() 692 | defer resp.Body.Close() 693 | 694 | if got, want := resp.StatusCode, tc.statusCode; got != want { 695 | t.Errorf("Status Code is %d, want %d", got, want) 696 | } 697 | 698 | if got, want := resp.Header.Get("Content-Type"), tc.contentType; got != want { 699 | t.Errorf("Content-Type is %s, want %s", got, want) 700 | } 701 | } 702 | } 703 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@commitlint/cli": "^19.6.0", 4 | "@commitlint/config-conventional": "^19.6.0", 5 | "@inabagumi/prettier-config": "^3.0.0", 6 | "@types/react": "^19.0.1", 7 | "eslint": "^8.57.1", 8 | "eslint-plugin-react": "^7.37.2", 9 | "eslint-plugin-react-hooks": "^5.1.0", 10 | "husky": "^9.1.7", 11 | "is-ci": "^4.1.0", 12 | "lint-staged": "^15.2.11", 13 | "prettier": "^3.4.2", 14 | "typescript": "^5.7.2" 15 | }, 16 | "packageManager": "pnpm@10.11.1", 17 | "prettier": "@inabagumi/prettier-config", 18 | "private": true, 19 | "scripts": { 20 | "build": "pnpm --filter @manael/website build", 21 | "format": "prettier --write './**/*.{json,yml}'", 22 | "format-check": "prettier -c './**/*.{json,yml}'", 23 | "lint": "eslint .", 24 | "prepare": "is-ci || husky" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - . 3 | - website 4 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "release-type": "go" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /testdata/empty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manaelproxy/manael/f9a31332bb7a1a5f2a6838c08479e3360ac91d58/testdata/empty.gif -------------------------------------------------------------------------------- /testdata/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manaelproxy/manael/f9a31332bb7a1a5f2a6838c08479e3360ac91d58/testdata/empty.txt -------------------------------------------------------------------------------- /testdata/gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manaelproxy/manael/f9a31332bb7a1a5f2a6838c08479e3360ac91d58/testdata/gray.png -------------------------------------------------------------------------------- /testdata/invalid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manaelproxy/manael/f9a31332bb7a1a5f2a6838c08479e3360ac91d58/testdata/invalid.png -------------------------------------------------------------------------------- /testdata/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manaelproxy/manael/f9a31332bb7a1a5f2a6838c08479e3360ac91d58/testdata/logo.png -------------------------------------------------------------------------------- /testdata/photo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manaelproxy/manael/f9a31332bb7a1a5f2a6838c08479e3360ac91d58/testdata/photo.jpeg -------------------------------------------------------------------------------- /website/docs/0-introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: / 3 | --- 4 | 5 | # Introduction 6 | 7 | Manael is an HTTP proxy that converts JPEG and PNG to WebP. Placing between the storage server and the cache server provides optimal resource conversion. 8 | -------------------------------------------------------------------------------- /website/docs/1-installation.md: -------------------------------------------------------------------------------- 1 | # Installation Guide 2 | 3 | ## Using Docker {#using-the-docker} 4 | 5 | It is recommended to run Manael with [Docker](https://www.docker.com/). A Docker image for running Manael is published on [Docker Hub](https://hub.docker.com/). 6 | 7 | Get the image with `docker pull manael/manael:latest` command before running Manael with Docker. Using Docker eliminates a need to add unnecessary files to your environment. 8 | 9 | ## Using a binary {#using-a-built-binary} 10 | 11 | You can download the Manael build for 64bit GNU/Linux. 12 | 13 | ### 1. Create a working directory {#1-create-a-working-directory} 14 | 15 | First, create a working directory to extract the downloaded file when installing Manael. 16 | 17 | ```console 18 | $ mkdir manael 19 | $ cd manael 20 | ``` 21 | 22 | ### 2. Download {#2-download} 23 | 24 | Download the latest version of Manael (`manael_1.x.y_Linux_x86_64.tar.gz`) from the [release page](https://github.com/manaelproxy/manael/releases) on GitHub. Then, extract the downloaded file to the directory created in step 1. 25 | 26 | ```console 27 | $ wget https://github.com/manaelproxy/manael/releases/download/v1.x.y/manael_1.x.y_Linux_x86_64.tar.gz 28 | $ tar xf manael_1.x.y_Linux_x86_64.tar.gz 29 | ``` 30 | 31 | ### 3. Install {#3-install} 32 | 33 | Use the `install` command to copy the file. You can do the same thing with the `cp` and `mv` commands, but using the `install` command gives the executable the appropriate execution permissions. 34 | 35 | ```console 36 | $ sudo install manael /usr/local/bin 37 | ``` 38 | 39 | ## Build from source {#build-from-a-source-code} 40 | 41 | The source code is hosted on [GitHub](https://github.com/manaelproxy/manael), and Manael is written in [Go](https://go.dev/). To install Manael, make sure to install Go and [Git](https://git-scm.com/) first, and [copy the repository](https://gist.github.com/natedana/cc71d496b611e70673cab5e8f5a78485). 42 | 43 | ```console 44 | $ go build -o manael cmd/manael/main.go 45 | ``` 46 | -------------------------------------------------------------------------------- /website/docs/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Getting Started", 3 | "position": 1 4 | } 5 | -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // `@type` JSDoc annotations allow editor autocompletion and type checking 3 | // (when paired with `@ts-check`). 4 | // There are various equivalent ways to declare your Docusaurus config. 5 | // See: https://docusaurus.io/docs/api/docusaurus-config 6 | 7 | /** @type {import('@docusaurus/types').Config} */ 8 | const config = { 9 | baseUrl: '/', 10 | favicon: 'img/manael.png', 11 | i18n: { 12 | defaultLocale: 'en', 13 | locales: ['en', 'ja'] 14 | }, 15 | onBrokenLinks: 'throw', 16 | onBrokenMarkdownLinks: 'warn', 17 | organizationName: 'manaelproxy', 18 | presets: [ 19 | [ 20 | 'classic', 21 | /** @type {import('@docusaurus/preset-classic').Options} */ 22 | ({ 23 | docs: { 24 | sidebarPath: './sidebars.js' 25 | }, 26 | theme: { 27 | customCss: './src/css/custom.css' 28 | } 29 | }) 30 | ] 31 | ], 32 | projectName: 'manael', 33 | tagline: 'Manael is a simple HTTP proxy for processing images.', 34 | themeConfig: 35 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 36 | ({ 37 | algolia: { 38 | appId: 'ZX7VYOHRJ3', 39 | apiKey: '43f66f766ffb77ee2280608d793ab235', 40 | indexName: 'docusaurus' 41 | }, 42 | footer: { 43 | copyright: 'Copyright © 2018 The Manael Authors.', 44 | links: [], 45 | style: 'dark' 46 | }, 47 | navbar: { 48 | hideOnScroll: true, 49 | items: [ 50 | { 51 | activeBasePath: 'docs', 52 | label: 'Docs', 53 | position: 'left', 54 | to: 'docs/' 55 | }, 56 | { 57 | type: 'localeDropdown', 58 | position: 'right' 59 | }, 60 | { 61 | href: 'https://github.com/manaelproxy/manael', 62 | label: 'GitHub', 63 | position: 'right' 64 | } 65 | ], 66 | logo: { 67 | alt: 'Manael Logo', 68 | src: 'img/manael.png' 69 | }, 70 | title: 'Manael' 71 | } 72 | }), 73 | title: 'Manael', 74 | trailingSlash: false, 75 | url: 'https://manael.org' 76 | } 77 | 78 | export default config 79 | -------------------------------------------------------------------------------- /website/i18n/en/code.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme.NotFound.title": { 3 | "message": "Page Not Found", 4 | "description": "The title of the 404 page" 5 | }, 6 | "theme.NotFound.p1": { 7 | "message": "We could not find what you were looking for.", 8 | "description": "The first paragraph of the 404 page" 9 | }, 10 | "theme.NotFound.p2": { 11 | "message": "Please contact the owner of the site that linked you to the original URL and let them know their link is broken.", 12 | "description": "The 2nd paragraph of the 404 page" 13 | }, 14 | "theme.AnnouncementBar.closeButtonAriaLabel": { 15 | "message": "Close", 16 | "description": "The ARIA label for close button of announcement bar" 17 | }, 18 | "theme.blog.paginator.navAriaLabel": { 19 | "message": "Blog list page navigation", 20 | "description": "The ARIA label for the blog pagination" 21 | }, 22 | "theme.blog.paginator.newerEntries": { 23 | "message": "Newer Entries", 24 | "description": "The label used to navigate to the newer blog posts page (previous page)" 25 | }, 26 | "theme.blog.paginator.olderEntries": { 27 | "message": "Older Entries", 28 | "description": "The label used to navigate to the older blog posts page (next page)" 29 | }, 30 | "theme.blog.post.readingTime.plurals": { 31 | "message": "One min read|{readingTime} min read", 32 | "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" 33 | }, 34 | "theme.tags.tagsListLabel": { 35 | "message": "Tags:", 36 | "description": "The label alongside a tag list" 37 | }, 38 | "theme.blog.post.readMore": { 39 | "message": "Read More", 40 | "description": "The label used in blog post item excerpts to link to full blog posts" 41 | }, 42 | "theme.blog.post.paginator.navAriaLabel": { 43 | "message": "Blog post page navigation", 44 | "description": "The ARIA label for the blog posts pagination" 45 | }, 46 | "theme.blog.post.paginator.newerPost": { 47 | "message": "Newer Post", 48 | "description": "The blog post button label to navigate to the newer/previous post" 49 | }, 50 | "theme.blog.post.paginator.olderPost": { 51 | "message": "Older Post", 52 | "description": "The blog post button label to navigate to the older/next post" 53 | }, 54 | "theme.tags.tagsPageTitle": { 55 | "message": "Tags", 56 | "description": "The title of the tag list page" 57 | }, 58 | "theme.blog.post.plurals": { 59 | "message": "One post|{count} posts", 60 | "description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" 61 | }, 62 | "theme.blog.tagTitle": { 63 | "message": "{nPosts} tagged with \"{tagName}\"", 64 | "description": "The title of the page for a blog tag" 65 | }, 66 | "theme.tags.tagsPageLink": { 67 | "message": "View All Tags", 68 | "description": "The label of the link targeting the tag list page" 69 | }, 70 | "theme.CodeBlock.copyButtonAriaLabel": { 71 | "message": "Copy code to clipboard", 72 | "description": "The ARIA label for copy code blocks button" 73 | }, 74 | "theme.CodeBlock.copied": { 75 | "message": "Copied", 76 | "description": "The copied button label on code blocks" 77 | }, 78 | "theme.CodeBlock.copy": { 79 | "message": "Copy", 80 | "description": "The copy button label on code blocks" 81 | }, 82 | "theme.docs.sidebar.expandButtonTitle": { 83 | "message": "Expand sidebar", 84 | "description": "The ARIA label and title attribute for expand button of doc sidebar" 85 | }, 86 | "theme.docs.sidebar.expandButtonAriaLabel": { 87 | "message": "Expand sidebar", 88 | "description": "The ARIA label and title attribute for expand button of doc sidebar" 89 | }, 90 | "theme.docs.paginator.navAriaLabel": { 91 | "message": "Docs pages navigation", 92 | "description": "The ARIA label for the docs pagination" 93 | }, 94 | "theme.docs.paginator.previous": { 95 | "message": "Previous", 96 | "description": "The label used to navigate to the previous doc" 97 | }, 98 | "theme.docs.paginator.next": { 99 | "message": "Next", 100 | "description": "The label used to navigate to the next doc" 101 | }, 102 | "theme.docs.sidebar.responsiveCloseButtonLabel": { 103 | "message": "Close menu", 104 | "description": "The ARIA label for close button of mobile doc sidebar" 105 | }, 106 | "theme.docs.sidebar.responsiveOpenButtonLabel": { 107 | "message": "Open menu", 108 | "description": "The ARIA label for open button of mobile doc sidebar" 109 | }, 110 | "theme.docs.sidebar.collapseButtonTitle": { 111 | "message": "Collapse sidebar", 112 | "description": "The title attribute for collapse button of doc sidebar" 113 | }, 114 | "theme.docs.sidebar.collapseButtonAriaLabel": { 115 | "message": "Collapse sidebar", 116 | "description": "The title attribute for collapse button of doc sidebar" 117 | }, 118 | "theme.docs.versions.unreleasedVersionLabel": { 119 | "message": "This is unreleased documentation for {siteTitle} {versionLabel} version.", 120 | "description": "The label used to tell the user that he's browsing an unreleased doc version" 121 | }, 122 | "theme.docs.versions.unmaintainedVersionLabel": { 123 | "message": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.", 124 | "description": "The label used to tell the user that he's browsing an unmaintained doc version" 125 | }, 126 | "theme.docs.versions.latestVersionSuggestionLabel": { 127 | "message": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).", 128 | "description": "The label userd to tell the user that he's browsing an unmaintained doc version" 129 | }, 130 | "theme.docs.versions.latestVersionLinkLabel": { 131 | "message": "latest version", 132 | "description": "The label used for the latest version suggestion link label" 133 | }, 134 | "theme.common.editThisPage": { 135 | "message": "Edit this page", 136 | "description": "The link label to edit the current page" 137 | }, 138 | "theme.common.headingLinkTitle": { 139 | "message": "Direct link to heading", 140 | "description": "Title for link to heading" 141 | }, 142 | "theme.lastUpdated.atDate": { 143 | "message": " on {date}", 144 | "description": "The words used to describe on which date a page has been last updated" 145 | }, 146 | "theme.lastUpdated.byUser": { 147 | "message": " by {user}", 148 | "description": "The words used to describe by who the page has been last updated" 149 | }, 150 | "theme.lastUpdated.lastUpdatedAtBy": { 151 | "message": "Last updated{atDate}{byUser}", 152 | "description": "The sentence used to display when a page has been last updated, and by who" 153 | }, 154 | "theme.common.skipToMainContent": { 155 | "message": "Skip to main content", 156 | "description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation" 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /website/i18n/en/docusaurus-plugin-content-docs/current.json: -------------------------------------------------------------------------------- 1 | { 2 | "version.label": { 3 | "message": "Next", 4 | "description": "The label for version current" 5 | }, 6 | "sidebar.docs.category.Getting Started": { 7 | "message": "Getting Started", 8 | "description": "The label for category Getting Started in sidebar docs" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /website/i18n/en/docusaurus-theme-classic/footer.json: -------------------------------------------------------------------------------- 1 | { 2 | "copyright": { 3 | "message": "Copyright © 2018 The Manael Authors.", 4 | "description": "The footer copyright" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /website/i18n/en/docusaurus-theme-classic/navbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": { 3 | "message": "Manael", 4 | "description": "The title in the navbar" 5 | }, 6 | "item.label.Docs": { 7 | "message": "Docs", 8 | "description": "Navbar item with label Docs" 9 | }, 10 | "item.label.GitHub": { 11 | "message": "GitHub", 12 | "description": "Navbar item with label GitHub" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /website/i18n/ja/code.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme.NotFound.title": { 3 | "message": "ページが見つかりません", 4 | "description": "The title of the 404 page" 5 | }, 6 | "theme.NotFound.p1": { 7 | "message": "お探しのページが見つかりませんでした。", 8 | "description": "The first paragraph of the 404 page" 9 | }, 10 | "theme.NotFound.p2": { 11 | "message": "このページにリンクしているサイトの所有者に連絡をしてリンクが壊れていることを伝えてください。", 12 | "description": "The 2nd paragraph of the 404 page" 13 | }, 14 | "theme.AnnouncementBar.closeButtonAriaLabel": { 15 | "message": "閉じる", 16 | "description": "The ARIA label for close button of announcement bar" 17 | }, 18 | "theme.blog.paginator.navAriaLabel": { 19 | "message": "ブログ記事一覧のナビゲーション", 20 | "description": "The ARIA label for the blog pagination" 21 | }, 22 | "theme.blog.paginator.newerEntries": { 23 | "message": "新しい記事", 24 | "description": "The label used to navigate to the newer blog posts page (previous page)" 25 | }, 26 | "theme.blog.paginator.olderEntries": { 27 | "message": "過去の記事", 28 | "description": "The label used to navigate to the older blog posts page (next page)" 29 | }, 30 | "theme.blog.post.readingTime.plurals": { 31 | "message": "約{readingTime}分", 32 | "description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" 33 | }, 34 | "theme.tags.tagsListLabel": { 35 | "message": "タグ:", 36 | "description": "The label alongside a tag list" 37 | }, 38 | "theme.blog.post.readMore": { 39 | "message": "もっと見る", 40 | "description": "The label used in blog post item excerpts to link to full blog posts" 41 | }, 42 | "theme.blog.post.paginator.navAriaLabel": { 43 | "message": "ブログ記事のナビゲーション", 44 | "description": "The ARIA label for the blog posts pagination" 45 | }, 46 | "theme.blog.post.paginator.newerPost": { 47 | "message": "新しい記事", 48 | "description": "The blog post button label to navigate to the newer/previous post" 49 | }, 50 | "theme.blog.post.paginator.olderPost": { 51 | "message": "過去の記事", 52 | "description": "The blog post button label to navigate to the older/next post" 53 | }, 54 | "theme.tags.tagsPageTitle": { 55 | "message": "タグ", 56 | "description": "The title of the tag list page" 57 | }, 58 | "theme.blog.post.plurals": { 59 | "message": "{count}件", 60 | "description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)" 61 | }, 62 | "theme.blog.tagTitle": { 63 | "message": "「{tagName}」タグの記事が{nPosts}あります", 64 | "description": "The title of the page for a blog tag" 65 | }, 66 | "theme.tags.tagsPageLink": { 67 | "message": "全てのタグを見る", 68 | "description": "The label of the link targeting the tag list page" 69 | }, 70 | "theme.CodeBlock.copyButtonAriaLabel": { 71 | "message": "クリップボードにコードをコピー", 72 | "description": "The ARIA label for copy code blocks button" 73 | }, 74 | "theme.CodeBlock.copied": { 75 | "message": "コピーしました", 76 | "description": "The copied button label on code blocks" 77 | }, 78 | "theme.CodeBlock.copy": { 79 | "message": "コピー", 80 | "description": "The copy button label on code blocks" 81 | }, 82 | "theme.docs.sidebar.expandButtonTitle": { 83 | "message": "サイドバーを開く", 84 | "description": "The ARIA label and title attribute for expand button of doc sidebar" 85 | }, 86 | "theme.docs.sidebar.expandButtonAriaLabel": { 87 | "message": "サイドバーを開く", 88 | "description": "The ARIA label and title attribute for expand button of doc sidebar" 89 | }, 90 | "theme.docs.paginator.navAriaLabel": { 91 | "message": "ドキュメントのナビゲーション", 92 | "description": "The ARIA label for the docs pagination" 93 | }, 94 | "theme.docs.paginator.previous": { 95 | "message": "前へ", 96 | "description": "The label used to navigate to the previous doc" 97 | }, 98 | "theme.docs.paginator.next": { 99 | "message": "次へ", 100 | "description": "The label used to navigate to the next doc" 101 | }, 102 | "theme.docs.sidebar.responsiveCloseButtonLabel": { 103 | "message": "メニューを閉じる", 104 | "description": "The ARIA label for close button of mobile doc sidebar" 105 | }, 106 | "theme.docs.sidebar.responsiveOpenButtonLabel": { 107 | "message": "メニューを開く", 108 | "description": "The ARIA label for open button of mobile doc sidebar" 109 | }, 110 | "theme.docs.sidebar.collapseButtonTitle": { 111 | "message": "サイドバーを隠す", 112 | "description": "The title attribute for collapse button of doc sidebar" 113 | }, 114 | "theme.docs.sidebar.collapseButtonAriaLabel": { 115 | "message": "サイドバーを隠す", 116 | "description": "The title attribute for collapse button of doc sidebar" 117 | }, 118 | "theme.docs.versions.unreleasedVersionLabel": { 119 | "message": "これはリリース前の{siteTitle} {versionLabel}のドキュメントです。", 120 | "description": "The label used to tell the user that he's browsing an unreleased doc version" 121 | }, 122 | "theme.docs.versions.unmaintainedVersionLabel": { 123 | "message": "これは{siteTitle} {versionLabel}のドキュメントで現在はアクティブにメンテナンスされていません。", 124 | "description": "The label used to tell the user that he's browsing an unmaintained doc version" 125 | }, 126 | "theme.docs.versions.latestVersionSuggestionLabel": { 127 | "message": "最新のドキュメントは{latestVersionLink} ({versionLabel}) を見てください。", 128 | "description": "The label userd to tell the user that he's browsing an unmaintained doc version" 129 | }, 130 | "theme.docs.versions.latestVersionLinkLabel": { 131 | "message": "最新バージョン", 132 | "description": "The label used for the latest version suggestion link label" 133 | }, 134 | "theme.common.editThisPage": { 135 | "message": "このページを編集", 136 | "description": "The link label to edit the current page" 137 | }, 138 | "theme.common.headingLinkTitle": { 139 | "message": "見出しへの直接リンク", 140 | "description": "Title for link to heading" 141 | }, 142 | "theme.lastUpdated.atDate": { 143 | "message": "{date}に", 144 | "description": "The words used to describe on which date a page has been last updated" 145 | }, 146 | "theme.lastUpdated.byUser": { 147 | "message": "{user}が", 148 | "description": "The words used to describe by who the page has been last updated" 149 | }, 150 | "theme.lastUpdated.lastUpdatedAtBy": { 151 | "message": "{atDate}{byUser}最終更新", 152 | "description": "The sentence used to display when a page has been last updated, and by who" 153 | }, 154 | "theme.common.skipToMainContent": { 155 | "message": "メインコンテンツまでスキップ", 156 | "description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation" 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /website/i18n/ja/docusaurus-plugin-content-docs/current.json: -------------------------------------------------------------------------------- 1 | { 2 | "version.label": { 3 | "message": "Next", 4 | "description": "The label for version current" 5 | }, 6 | "sidebar.docs.category.Getting Started": { 7 | "message": "初めに", 8 | "description": "The label for category Getting Started in sidebar docs" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /website/i18n/ja/docusaurus-plugin-content-docs/current/0-introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: / 3 | --- 4 | 5 | # Manaelとは 6 | 7 | Manael は JPEG と PNG を WebP に変換する HTTP プロキシです。ストレージサーバーとキャッシュサーバーの間に配置すると最適なリソース変換が行われます。 8 | -------------------------------------------------------------------------------- /website/i18n/ja/docusaurus-plugin-content-docs/current/1-installation.md: -------------------------------------------------------------------------------- 1 | # インストールガイド 2 | 3 | ## Docker を使う {#using-the-docker} 4 | 5 | Manael は [Docker](https://www.docker.com/) で動かすことを推奨しています。Manael の Docker イメージは [Docker Hub](https://hub.docker.com/) で公開されています。 6 | 7 | Manael を Docker を使って動かす場合は `docker pull manael/manael:latest` コマンドで取得してください。Docker を使うことによって既存の環境に不必要なファイルを増やさずに最新版の Manael が使えるようになります。 8 | 9 | ## ビルド済みバイナリを使う {#using-a-built-binary} 10 | 11 | 64 ビット版の GNU/Linux を対象にしてビルドされた Manael をダウンロードできます。 12 | 13 | ### 1. ディレクトリを作る {#1-create-a-working-directory} 14 | 15 | まず Manael をインストールする際にダウンロードしたファイルを展開するためのディレクトリを作ります。 16 | 17 | ```console 18 | $ mkdir manael 19 | $ cd manael 20 | ``` 21 | 22 | ### 2. ダウンロード {#2-download} 23 | 24 | [リリースページ](https://github.com/manaelproxy/manael/releases)から最新版の Manael (`manael_1.x.y_Linux_x86_64.tar.gz`) をダウンロードして 1. で作ったディレクトリに展開します。 25 | 26 | ```console 27 | $ wget https://github.com/manaelproxy/manael/releases/download/v1.x.y/manael_1.x.y_Linux_x86_64.tar.gz 28 | $ tar xf manael_1.x.y_Linux_x86_64.tar.gz 29 | ``` 30 | 31 | ### 3. インストール {#3-install} 32 | 33 | ファイルをコピーするために `install` コマンドを利用します。`cp` コマンドや `mv` コマンドでも同様の作業はできますが、`install` コマンドを使うことによって適切な実行権限が実行ファイルに与えられます。 34 | 35 | ```console 36 | $ sudo install manael /usr/local/bin 37 | ``` 38 | 39 | ## ソースコードからビルド {#build-from-a-source-code} 40 | 41 | Manael のソースコードは [GitHub](https://github.com/manaelproxy/manael) にホストされています。Manael は [Go](https://golang.org/) で書かれていて、`go` コマンドを使って簡単にビルドできます。 42 | 43 | ```console 44 | $ go build -o manael cmd/manael/main.go 45 | ``` 46 | -------------------------------------------------------------------------------- /website/i18n/ja/docusaurus-plugin-content-docs/current/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "始めに", 3 | "position": 1 4 | } 5 | -------------------------------------------------------------------------------- /website/i18n/ja/docusaurus-theme-classic/footer.json: -------------------------------------------------------------------------------- 1 | { 2 | "copyright": { 3 | "message": "Copyright © 2018 The Manael Authors.", 4 | "description": "The footer copyright" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /website/i18n/ja/docusaurus-theme-classic/navbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": { 3 | "message": "Manael", 4 | "description": "The title in the navbar" 5 | }, 6 | "item.label.Docs": { 7 | "message": "ドキュメント", 8 | "description": "Navbar item with label Docs" 9 | }, 10 | "item.label.GitHub": { 11 | "message": "GitHub", 12 | "description": "Navbar item with label GitHub" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "browserslist": { 3 | "development": [ 4 | "last 1 chrome version", 5 | "last 1 firefox version", 6 | "last 1 safari version" 7 | ], 8 | "production": [ 9 | ">0.2%", 10 | "not dead", 11 | "not op_mini all" 12 | ] 13 | }, 14 | "devDependencies": { 15 | "@docusaurus/core": "^3.6.3", 16 | "@docusaurus/preset-classic": "^3.6.3", 17 | "@mdx-js/react": "^3.1.0", 18 | "classnames": "^2.5.1", 19 | "prop-types": "^15.8.1", 20 | "react": "^19.0.0", 21 | "react-dom": "^19.0.0" 22 | }, 23 | "name": "@manael/website", 24 | "private": true, 25 | "scripts": { 26 | "build": "docusaurus build", 27 | "clear": "docusaurus clear", 28 | "deploy": "docusaurus deploy", 29 | "serve": "docusaurus serve", 30 | "start": "docusaurus start", 31 | "swizzle": "docusaurus swizzle", 32 | "write-heading-ids": "docusaurus write-heading-ids", 33 | "write-translations": "docusaurus write-translations" 34 | }, 35 | "version": "1.0.0" 36 | } 37 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tutorialSidebar: [ 3 | { 4 | dirName: '.', 5 | type: 'autogenerated' 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | } 3 | 4 | .navbar__logo { 5 | border-radius: 4px; 6 | } 7 | -------------------------------------------------------------------------------- /website/src/pages/index.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import classnames from 'classnames' 3 | import Layout from '@theme/Layout' 4 | import Link from '@docusaurus/Link' 5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext' 6 | import { useBaseUrlUtils } from '@docusaurus/useBaseUrl' 7 | import styles from './styles.module.css' 8 | 9 | const features = [ 10 | { 11 | description: <>Just run a one binary!, 12 | title: <>Simple! 13 | }, 14 | { 15 | description: <>Manael’s binary run anywhere in a GNU/Linux environment., 16 | title: <>Portability! 17 | }, 18 | { 19 | description: ( 20 | <>Manael is fast because there is no unnecessary processing! 21 | ), 22 | title: <>High Peformance! 23 | } 24 | ] 25 | 26 | function Feature({ description, imageUrl, title }) { 27 | const { withBaseUrl } = useBaseUrlUtils() 28 | 29 | return ( 30 |
31 | {imageUrl && ( 32 |
33 | {title} 38 |
39 | )} 40 |

{title}

41 |

{description}

42 |
43 | ) 44 | } 45 | 46 | Feature.propTypes = { 47 | description: PropTypes.string.isRequired, 48 | imageUrl: PropTypes.string, 49 | title: PropTypes.string.isRequired 50 | } 51 | 52 | function Home() { 53 | const { siteConfig = {} } = useDocusaurusContext() 54 | const { withBaseUrl } = useBaseUrlUtils() 55 | 56 | return ( 57 | 58 |
59 |
60 |

61 | {siteConfig.title} 68 |

69 |

{siteConfig.tagline}

70 |
71 | 78 | Get Started 79 | 80 |
81 |
82 |
83 |
84 | {features && features.length && ( 85 |
86 |
87 |
88 | {features.map((props, idx) => ( 89 | 90 | ))} 91 |
92 |
93 |
94 | )} 95 |
96 |
97 | ) 98 | } 99 | 100 | export default Home 101 | -------------------------------------------------------------------------------- /website/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | .heroBanner { 2 | padding: 6rem 0; 3 | text-align: center; 4 | position: relative; 5 | overflow: hidden; 6 | } 7 | 8 | @media screen and (max-width: 966px) { 9 | .heroBanner { 10 | padding: 4rem 2rem; 11 | } 12 | } 13 | 14 | .heroBannerLogo { 15 | border-radius: 6px; 16 | } 17 | 18 | .buttons { 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | } 23 | 24 | .features { 25 | display: flex; 26 | align-items: center; 27 | padding: 2rem 0; 28 | width: 100%; 29 | } 30 | 31 | .featureImage { 32 | height: 200px; 33 | width: 200px; 34 | } 35 | -------------------------------------------------------------------------------- /website/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manaelproxy/manael/f9a31332bb7a1a5f2a6838c08479e3360ac91d58/website/static/img/logo.png -------------------------------------------------------------------------------- /website/static/img/manael.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manaelproxy/manael/f9a31332bb7a1a5f2a6838c08479e3360ac91d58/website/static/img/manael.png -------------------------------------------------------------------------------- /website/static/x/manael.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | manael 16 |

...

17 | -------------------------------------------------------------------------------- /website/static/x/manael/cmd/manael.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | manael 16 |

...

17 | -------------------------------------------------------------------------------- /website/static/x/manael/internal/testutil.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | manael 16 |

...

17 | -------------------------------------------------------------------------------- /website/static/x/manael/v2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | manael 16 |

...

17 | -------------------------------------------------------------------------------- /website/static/x/manael/v2/cmd/manael.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | manael 16 |

...

17 | -------------------------------------------------------------------------------- /website/static/x/manael/v2/internal/testutil.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | manael 16 |

...

17 | --------------------------------------------------------------------------------