├── .dockerignore ├── .github ├── CODEOWNERS └── workflows │ ├── release.yaml │ └── test.yml ├── .gitignore ├── .goreleaser.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.ja.md ├── README.md ├── cmd └── fanlin │ ├── .gitignore │ ├── main.go │ ├── sample-conf-container.json │ └── sample-conf.json ├── compose.yaml ├── go.mod ├── go.sum ├── img ├── 404.png ├── Lenna.jpg └── test │ └── test.png ├── lib ├── conf │ ├── conf.go │ └── conf_test.go ├── content │ ├── content.go │ ├── content_test.go │ ├── local │ │ ├── local.go │ │ └── local_test.go │ ├── s3 │ │ ├── s3.go │ │ └── s3_test.go │ ├── source.go │ └── web │ │ ├── web.go │ │ └── web_test.go ├── error │ ├── error.go │ └── error_test.go ├── handler │ ├── handler.go │ └── handler_test.go ├── image │ ├── default.icc │ ├── image.go │ └── image_test.go ├── logger │ └── logger.go ├── query │ ├── query.go │ └── query_test.go └── test │ ├── helper.go │ ├── img │ ├── Lenna.bmp │ ├── Lenna.gif │ ├── Lenna.jpg │ ├── Lenna.png │ ├── Lenna_lossless.webp │ └── Lenna_lossy.webp │ ├── test_conf.json │ ├── test_conf3.json │ ├── test_conf4.json │ ├── test_conf8.json │ └── test_conf9.json ├── network.json └── plugin ├── .gitignore └── explanation.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | *.log 4 | *.out 5 | *.test 6 | *.txt 7 | *.md 8 | *.json 9 | *.yaml 10 | __debug_bin 11 | results.bin 12 | /img 13 | /cmd/fanlin/server 14 | /lib/test 15 | /tmp 16 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ieee0824 2 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # @see https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions 3 | # @see https://github.com/actions/virtual-environments 4 | name: Release 5 | on: 6 | push: 7 | tags: 8 | - "v*" 9 | defaults: 10 | run: 11 | shell: bash 12 | concurrency: ${{ github.workflow }} 13 | jobs: 14 | executable-files: 15 | name: Executable Files 16 | if: github.repository == 'livesense-inc/fanlin' 17 | timeout-minutes: 10 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: write 21 | defaults: 22 | run: 23 | shell: bash 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Set up Go 29 | uses: actions/setup-go@v5 30 | with: 31 | go-version-file: go.mod 32 | check-latest: true 33 | cache: true 34 | 35 | - name: Print libc version 36 | run: ldd --version 37 | 38 | - name: Install dependencies 39 | run: | 40 | sudo apt update 41 | sudo apt install -y libaom-dev liblcms2-dev 42 | 43 | - name: Run GoReleaser 44 | uses: goreleaser/goreleaser-action@v5 45 | with: 46 | distribution: goreleaser 47 | version: latest 48 | args: release --clean 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | container: 52 | name: Container 53 | if: github.repository == 'livesense-inc/fanlin' 54 | timeout-minutes: 15 55 | runs-on: ubuntu-latest 56 | permissions: 57 | packages: write 58 | env: 59 | IMAGE_NAME: fanlin 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v4 63 | 64 | - name: Build 65 | run: docker build . -t $IMAGE_NAME 66 | 67 | - name: Version 68 | id: version 69 | run: | 70 | version=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') 71 | [[ "${{ github.ref }}" == "refs/tags/"* ]] && version=$(echo $version | sed -e 's/^v//') 72 | [ "$version" == "master" ] && version=latest 73 | echo "value=$version" >> "$GITHUB_OUTPUT" 74 | 75 | - name: Login 76 | run: > 77 | echo "${{ secrets.GITHUB_TOKEN }}" 78 | | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin 79 | 80 | - name: Tag 81 | id: tag 82 | run: | 83 | tag=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:${{ steps.version.outputs.value }} 84 | docker tag $IMAGE_NAME $tag 85 | echo "value=$tag" >> "$GITHUB_OUTPUT" 86 | 87 | - name: Push 88 | run: docker push ${{ steps.tag.outputs.value }} 89 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | timeout-minutes: 10 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Check out code 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version-file: go.mod 26 | check-latest: true 27 | 28 | - name: Install dependencies 29 | run: | 30 | sudo apt update 31 | sudo apt install -y libaom-dev liblcms2-dev 32 | 33 | - name: Get dependencies 34 | run: go mod download 35 | 36 | - name: Run test 37 | run: make test 38 | 39 | lint: 40 | name: Lint 41 | timeout-minutes: 5 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Check out code 45 | uses: actions/checkout@v4 46 | 47 | - name: Set up Go 48 | uses: actions/setup-go@v5 49 | with: 50 | go-version-file: go.mod 51 | check-latest: true 52 | 53 | - name: Install dependencies 54 | run: | 55 | sudo apt update 56 | sudo apt install -y libaom-dev liblcms2-dev 57 | 58 | - name: Download modules 59 | run: go mod download 60 | 61 | - name: Run lint 62 | run: make lint 63 | 64 | - uses: dominikh/staticcheck-action@v1 65 | with: 66 | version: "latest" 67 | install-go: false 68 | 69 | benchmark: 70 | name: Benchmark 71 | timeout-minutes: 10 72 | runs-on: ubuntu-latest 73 | steps: 74 | - name: Check out code 75 | uses: actions/checkout@v4 76 | 77 | - name: Set up Go 78 | uses: actions/setup-go@v5 79 | with: 80 | go-version-file: go.mod 81 | check-latest: true 82 | 83 | - name: Install dependencies 84 | run: | 85 | sudo apt update 86 | sudo apt install -y libaom-dev liblcms2-dev 87 | 88 | - name: Download modules 89 | run: go mod download 90 | 91 | - name: Run benchmark 92 | run: make bench 93 | 94 | profiling: 95 | name: Profiling 96 | timeout-minutes: 10 97 | runs-on: ubuntu-latest 98 | strategy: 99 | fail-fast: false 100 | matrix: 101 | package: ["handler"] 102 | type: ["cpu", "block", "mem"] 103 | steps: 104 | - name: Check out code 105 | uses: actions/checkout@v4 106 | 107 | - name: Set up Go 108 | uses: actions/setup-go@v5 109 | with: 110 | go-version-file: go.mod 111 | check-latest: true 112 | 113 | - name: Install dependencies 114 | run: | 115 | sudo apt update 116 | sudo apt install -y libaom-dev liblcms2-dev 117 | 118 | - name: Download modules 119 | run: go mod download 120 | 121 | - name: Run profiling 122 | run: make prof 123 | env: 124 | PKG: ${{ matrix.package }} 125 | TYPE: ${{ matrix.type }} 126 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,vim,go,visualstudiocode 3 | 4 | ### OSX ### 5 | *.DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | 33 | ### Vim ### 34 | # swap 35 | [._]*.s[a-w][a-z] 36 | [._]s[a-w][a-z] 37 | # session 38 | Session.vim 39 | # temporary 40 | .netrwhist 41 | *~ 42 | # auto-generated tag files 43 | tags 44 | 45 | 46 | ### Go ### 47 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 48 | *.o 49 | *.a 50 | *.so 51 | 52 | # Folders 53 | _obj 54 | _test 55 | 56 | # Architecture specific extensions/prefixes 57 | *.[568vq] 58 | [568vq].out 59 | 60 | *.cgo1.go 61 | *.cgo2.c 62 | _cgo_defun.c 63 | _cgo_gotypes.go 64 | _cgo_export.* 65 | 66 | _testmain.go 67 | 68 | *.exe 69 | *.test 70 | *.prof 71 | 72 | 73 | ### VisualStudioCode ### 74 | .vscode 75 | 76 | vendor/ 77 | /server 78 | /fanlin.json 79 | /*.log 80 | /*.out 81 | /*.test 82 | __debug_bin 83 | results.bin 84 | /tmp 85 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: fanlin 2 | 3 | # https://goreleaser.com/customization/build/ 4 | # https://go.dev/doc/install/source#environment 5 | builds: 6 | - env: 7 | - CGO_ENABLED=1 8 | flags: 9 | - -trimpath 10 | - -tags=timetzdata 11 | ldflags: 12 | - -s -w 13 | goos: 14 | - linux 15 | goarch: 16 | - amd64 17 | main: ./cmd/fanlin 18 | 19 | # https://goreleaser.com/customization/archive/ 20 | archives: 21 | - name_template: '{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}-v{{ . }}{{ end }}' 22 | format: binary 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1-bookworm AS builder 2 | RUN set -eux; \ 3 | apt-get update -y; \ 4 | apt-get install -y --no-install-recommends \ 5 | ca-certificates \ 6 | build-essential \ 7 | make \ 8 | libaom-dev \ 9 | liblcms2-dev 10 | WORKDIR /usr/src/app 11 | COPY . . 12 | RUN make build 13 | 14 | # https://github.com/GoogleContainerTools/distroless 15 | # https://console.cloud.google.com/gcr/images/distroless/GLOBAL 16 | FROM gcr.io/distroless/cc-debian12:nonroot-amd64 17 | COPY --from=builder --chmod=644 /lib/x86_64-linux-gnu/libaom.so.* /lib/x86_64-linux-gnu/ 18 | COPY --from=builder --chmod=644 /lib/x86_64-linux-gnu/liblcms2.so.* /lib/x86_64-linux-gnu/ 19 | COPY --from=builder --chmod=755 /usr/src/app/cmd/fanlin/server /usr/local/bin/fanlin 20 | ENTRYPOINT ["/usr/local/bin/fanlin"] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 LIVESENSE.Inc, jobtalk, na-o-ys,ieee0824 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 | MAKEFLAGS += --warn-undefined-variables 2 | GOOS ?= $(shell go env GOOS) 3 | GOARCH ?= $(shell go env GOARCH) 4 | CGO_ENABLED ?= $(shell go env CGO_ENABLED) 5 | AWS_ENDPOINT_URL := http://127.0.0.1:4567 6 | AWS_REGION := ap-northeast-1 7 | AWS_CMD_ENV += AWS_ACCESS_KEY_ID=AAAAAAAAAAAAAAAAAAAA 8 | AWS_CMD_ENV += AWS_SECRET_ACCESS_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 9 | AWS_CMD_OPT += --endpoint-url=${AWS_ENDPOINT_URL} 10 | AWS_CMD_OPT += --region=${AWS_REGION} 11 | AWS_CMD := ${AWS_CMD_ENV} aws ${AWS_CMD_OPT} 12 | AWS_S3_BUCKET_NAME := local-test 13 | 14 | build: cmd/fanlin/server 15 | 16 | cmd/fanlin/server: cmd/fanlin/main.go 17 | GOOS=${GOOS} GOARCH=${GOARCH} CGO_ENABLED=${CGO_ENABLED} go build -ldflags="-s -w" -trimpath -tags timetzdata -o $@ $^ 18 | 19 | cmd/fanlin/fanlin.json: cmd/fanlin/sample-conf.json 20 | @cp $^ $@ 21 | 22 | fanlin.json: cmd/fanlin/fanlin.json 23 | @ln -sf $^ 24 | 25 | run: cmd/fanlin/server fanlin.json 26 | @$^ 27 | 28 | test: 29 | @go clean -testcache 30 | @go test -race ./... 31 | 32 | lint: 33 | @go vet ./... 34 | 35 | bench: 36 | @go test -bench=. -benchmem -run=NONE ./... 37 | 38 | prof: PKG ?= handler 39 | prof: TYPE ?= mem 40 | prof: 41 | @if [ -z "${PKG}" ]; then echo 'empty variable: PKG'; exit 1; fi 42 | @if [ -z "${TYPE}" ]; then echo 'empty variable: TYPE'; exit 1; fi 43 | @if [ ! -d "./lib/${PKG}" ]; then echo 'package not found: ${PKG}'; exit 1; fi 44 | @go test -bench=. -run=NONE -${TYPE}profile=${TYPE}.out ./lib/${PKG} 45 | @go tool pprof -text -nodecount=10 ./${PKG}.test ${TYPE}.out 46 | 47 | clean: 48 | @unlink fanlin.json || true 49 | @rm -f cmd/fanlin/server cmd/fanlin/fanlin.json 50 | 51 | create-s3-bucket: 52 | @${AWS_CMD} s3api create-bucket \ 53 | --bucket=${AWS_S3_BUCKET_NAME} \ 54 | --create-bucket-configuration LocationConstraint=${AWS_REGION} 55 | 56 | clean-s3-bucket: 57 | @${AWS_CMD} s3 rm s3://${AWS_S3_BUCKET_NAME} --include='*' --recursive 58 | 59 | list-s3-bucket: 60 | @${AWS_CMD} s3 ls s3://${AWS_S3_BUCKET_NAME}/${FOLDER} 61 | 62 | copy-object: 63 | @${AWS_CMD} s3 cp ${SRC} s3://${AWS_S3_BUCKET_NAME}/${DEST} 64 | 65 | .PHONY: build cmd/fanlin/server run test lint bench prof clean 66 | -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | # fanlin 2 | 3 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) 4 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/livesense-inc/fanlin) 5 | ![Test](https://github.com/livesense-inc/fanlin/actions/workflows/test.yml/badge.svg?branch=master) 6 | ![Release](https://github.com/livesense-inc/fanlin/actions/workflows/release.yaml/badge.svg) 7 | 8 | [English](README.md) | 日本語 9 | 10 | ## 概要 11 | fanlinはGo言語で作られた軽量画像プロキシです. 12 | Amazon S3と外部HTTPサーバー上の画像をリアルタイムで加工することができます. 13 | 14 | ## 環境 15 | ### OS 16 | * Linux (x86 and amd64) 17 | * macOS 18 | 19 | ### Go Versions 20 | * `go.mod` ファイル参照 21 | 22 | ## 対応画像フォーマット 23 | * JPEG 24 | * PNG 25 | * GIF 26 | * WebP 27 | * AVIF (エンコードのみ) 28 | 29 | ## macOS の環境構築 30 | ## master pushの悲劇を防ぐために 31 | [ここを参考に設定すること](http://ganmacs.hatenablog.com/entry/2014/06/18/224132) 32 | 33 | ## 依存ライブラリ 34 | AVIFフォーマットのエンコードのためにlibaomが必要です。事前にインストールしておいてください。 35 | 36 | ``` 37 | $ sudo apt install libaom-dev 38 | ``` 39 | 40 | また、ICCプロファイルを利用してCMYKをRGBに変換するための以下も必要です。 41 | 42 | ``` 43 | $ sudo apt install liblcms2-dev 44 | ``` 45 | 46 | ## Linux用にクロスコンパイルする 47 | ``` 48 | $ GOOS=linux GOARCH=amd64 go build github.com/livesense-inc/fanlin/cmd/fanlin 49 | ``` 50 | 51 | ## サーバーに配布するもの 52 | ビルドして作った実行ファイル 53 | 設定ファイル 54 | 55 | ## API 56 | getパラメータに値を渡して操作する 57 | `w`画像の横幅 58 | `h`画像の縦幅 59 | 色を指定しない時`w`と`h`を指定した時は小さい方に合わせて縮尺を変更する 60 | この時アスペクト比は維持する 61 | また一方が`0`の時は`0`ではない値を基準に縮尺を変更する 62 | `w`及び`h`が`0`であるときは元のサイズで表示する 63 | あまりにも大きいサイズが指定された時は設定ファイルにかかれている上限の大きさで拡大する 64 | `rgb`で色を指定した場合`w`と`h`で指定された大きさの画像を生成する 65 | この時画像のアスペクト比が違うときは隙間を指定した色で塗りつぶす 66 | `quality`で`0`から`100`までの数値を指定した場合その数値にクオリティを設定した画像を生成する 67 | それ以外の数値が指定された場合は`jpeg.DefaultQuality`の値(`75`)が指定される 68 | `crop`で`true`が指定された場合`w`と`h`で指定した比に合わせて、画像中央を基準としてはみ出した部分クロッピングして画像を生成する 69 | 例: 70 | ``` 71 | http://localhost:8080/path/to/image/?h=400&w=400&rgb=100,100,100&quality=80&crop=true 72 | ``` 73 | 74 | ## testing 75 | ``` 76 | $ go test -cover ./... 77 | ``` 78 | 79 | ## 設定項目に関して 80 | だいたいこんな感じでかけます 81 | ```json 82 | { 83 | "port": 8080, 84 | "max_width": 1000, 85 | "max_height": 1000, 86 | "404_img_path": "/path/to/404/image", 87 | "access_log_path": "/path/to/access/log", 88 | "error_log_path": "/path/to/error/log", 89 | "use_server_timing": true, 90 | "providers": [ 91 | { 92 | "/alias/0" : { 93 | "type" : "web", 94 | "src" : "http://aaa.com/bbb", 95 | "priority" : 10 96 | } 97 | }, 98 | { 99 | "/alias/1" : { 100 | "type" : "web", 101 | "src" : "https://ccc.com", 102 | "priority" : 20 103 | } 104 | }, 105 | { 106 | "/alias/3" : { 107 | "type" : "s3", 108 | "src" : "s3://bucket/path", 109 | "region" : "ap-northeast-1", 110 | "priority" : 30 111 | } 112 | } 113 | ] 114 | } 115 | ``` 116 | 117 | ## ログの出力先を制御する 118 | 設定項目の`access_log_path`/`error_log_path`/`debug_log_path`にパスを指定することで、それぞれのログをファイルに出力できます。 119 | 標準出力にログを出力したい場合は、`/dev/stdout`を指定してください。 120 | 121 | ## WebPフォーマットの利用方法と制限事項 122 | fanlinへのリクエストに `webp=true` getパラメータを付与することで、WebPフォーマットの画像を返すことが出来ます. 123 | 124 | 例: 125 | 126 | - JPG画像のURL: 127 | - http://localhost:8080/abc.jpg?h=400&w=400&quality=80 128 | - WebPに変換した画像のURL: 129 | - http://localhost:8080/abc.jpg?h=400&w=400&quality=80&webp=true 130 | 131 | また、以下の条件の場合は、Lossless WebPフォーマットで変換します. 132 | 133 | - getパラメータにて `quality=100` を指定かつ元画像のフォーマットが PNG / GIF / WebP のいずれか 134 | 135 | ### 制限事項 136 | 137 | - アニメーションには対応していません 138 | 139 | 140 | ## Server-Timingのサポートに関して 141 | 142 | 設定ファイルのグローバル設定値に `"use_server_timing": true` を入れることで[Server-Timing](https://www.w3.org/TR/server-timing/)が出力されます. 143 | Server-Timingの出力によって、システムの内部構成やパフォーマンスがエンドユーザーに見えてしまう可能性があります.利用に際してはご注意ください. 144 | 145 | 現在の出力項目は以下: 146 | 147 | - f_load: ソース画像のロード時間 148 | - f_decode: ソース画像のデコードと加工時間 149 | - f_encode: 最終出力フォーマットへのエンコード時間 150 | 151 | ## モックサーバーを利用してAmazon S3バックエンドの動作確認を手元でする 152 | `providers` directive にて `use_mock` 属性を `true` に指定すると fanlin はローカルのモックサーバーを参照するように動作します。 153 | 154 | ```json 155 | { 156 | "port": 3000, 157 | "max_width": 2000, 158 | "max_height": 1000, 159 | "404_img_path": "img/404.png", 160 | "access_log_path": "/dev/stdout", 161 | "error_log_path": "/dev/stderr", 162 | "max_clients": 50, 163 | "providers": [ 164 | { 165 | "/foo": { 166 | "type": "s3", 167 | "src": "s3://local-test/images", 168 | "region": "ap-northeast-1", 169 | "norm_form": "nfd", 170 | "use_mock": true 171 | } 172 | }, 173 | { 174 | "/bar": { 175 | "type": "web", 176 | "src": "http://localhost:3000/foo" 177 | } 178 | }, 179 | { 180 | "/baz": { 181 | "type": "local", 182 | "src": "img" 183 | } 184 | } 185 | 186 | ] 187 | } 188 | ``` 189 | 190 | fanlin 起動前に Docker compose でモックサーバーを起動しておいてください。 191 | 192 | ``` 193 | $ docker compose up 194 | $ make create-s3-bucket 195 | $ make copy-object SRC=img/Lenna.jpg DEST=images/Lenna.jpg 196 | $ make run 197 | ``` 198 | 199 | これでローカルで動作確認ができます。 200 | 201 | ``` 202 | $ curl -I 'http://localhost:3000/foo/Lenna.jpg?w=300&h=200&rgb=64,64,64' 203 | $ curl -I 'http://localhost:3000/bar/Lenna.jpg?w=300&h=200&rgb=64,64,64' 204 | $ curl -I 'http://localhost:3000/baz/Lenna.jpg?w=300&h=200&rgb=64,64,64' 205 | ``` 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fanlin 2 | 3 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) 4 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/livesense-inc/fanlin) 5 | ![Test](https://github.com/livesense-inc/fanlin/actions/workflows/test.yml/badge.svg?branch=master) 6 | ![Release](https://github.com/livesense-inc/fanlin/actions/workflows/release.yaml/badge.svg) 7 | 8 | English | [日本語](README.ja.md) 9 | 10 | ## abstract 11 | fanlin is image proxy server written in Go language. 12 | 13 | ## Support 14 | ### OS 15 | * Linux (x86 and amd64) 16 | * macOS 17 | 18 | ### Go Versions 19 | * Please see `go.mod` 20 | 21 | ### Image Format 22 | * JPEG 23 | * PNG 24 | * GIF 25 | * WebP 26 | * AVIF (encode only) 27 | 28 | ## Requirements 29 | Make sure libaom is installed for AVIF format encoding. 30 | 31 | ``` 32 | $ sudo apt install libaom-dev 33 | ``` 34 | 35 | Also, we need the following dependency for the conversion between CMYK and RGB color spaces with ICC profiles. 36 | 37 | ``` 38 | $ sudo apt install liblcms2-dev 39 | ``` 40 | 41 | ## Cross compile for amd64 Linux 42 | ``` 43 | $ GOOS=linux GOARCH=amd64 go build github.com/livesense-inc/fanlin/cmd/fanlin 44 | ``` 45 | 46 | ## testing 47 | ``` 48 | $ go test -cover ./... 49 | ``` 50 | 51 | ## configure 52 | On Unix, Linux and macOS, fanlin programs read startup options from the following files, in the specified order(top files are read first, and precedence). 53 | 54 | ``` 55 | /etc/fanlin.json 56 | /etc/fanlin.cnf 57 | /etc/fanlin.conf 58 | /usr/local/etc/fanlin.json 59 | /usr/local/etc/fanlin.cnf 60 | /usr/local/etc/fanlin.conf 61 | ./fanlin.json 62 | ./fanlin.cnf 63 | ./fanlin.conf 64 | ./conf.json 65 | ~/.fanlin.json 66 | ~/.fanlin.cnf 67 | ``` 68 | 69 | ### example 70 | 71 | #### fanlin.json 72 | ```json 73 | { 74 | "port": 8080, 75 | "max_width": 1000, 76 | "max_height": 1000, 77 | "404_img_path": "/path/to/404/image", 78 | "access_log_path": "/path/to/access/log", 79 | "error_log_path": "/path/to/error/log", 80 | "use_server_timing": true, 81 | "providers": [ 82 | { 83 | "/alias/0" : { 84 | "type" : "web", 85 | "src" : "http://aaa.com/bbb", 86 | "priority" : 10 87 | } 88 | }, 89 | { 90 | "/alias/1" : { 91 | "type" : "web", 92 | "src" : "https://ccc.com", 93 | "priority" : 20 94 | } 95 | }, 96 | { 97 | "/alias/3" : { 98 | "type" : "s3", 99 | "src" : "s3://bucket/path", 100 | "region" : "ap-northeast-1", 101 | "priority" : 30 102 | } 103 | } 104 | ] 105 | } 106 | ``` 107 | 108 | ## Controling where logs are output to 109 | You can output each log to a file by specifying the path in `access_log_path`/`error_log_path`/`debug_log_path`. 110 | If you want to output logs to standard output, specify `/dev/stdout`. 111 | 112 | ## Using WebP and Limitations 113 | You can get WebP image format with GET parameter `webp=true` requeest. 114 | 115 | Examples: 116 | 117 | - JPG image URL: 118 | - http://localhost:8080/abc.jpg?h=400&w=400&quality=80 119 | - WebP encoded image URL: 120 | - http://localhost:8080/abc.jpg?h=400&w=400&quality=80&webp=true 121 | 122 | fanlin returns lossless WebP image in following conditions. 123 | 124 | - GET parameter `quality=100` AND source image format is PNG / GIF / WebP 125 | 126 | ### Limitations 127 | 128 | - Do not support animations 129 | 130 | 131 | ## Server-Timing Support 132 | 133 | Add `"use_server_timing": true` at Global parameters in config file. 134 | You will get [Server-Timing](https://www.w3.org/TR/server-timing/) output. 135 | Be careful, your system architecture or perfomance will be exposed to enduser with Server-Timing output. 136 | 137 | fanlin outputs following timings: 138 | 139 | - f_load: The time for load source image. 140 | - f_decode: The time for decode and format source image. 141 | - f_encode: The time for encode to final image format. 142 | 143 | ## Local test with Amazon S3 mock server 144 | If you specify `use_mock` attribute as `true` in `providers` directive, fanlin behaves local access mode to refer a mock server compatible with Amazon S3. 145 | 146 | ```json 147 | { 148 | "port": 3000, 149 | "max_width": 2000, 150 | "max_height": 1000, 151 | "404_img_path": "img/404.png", 152 | "access_log_path": "/dev/stdout", 153 | "error_log_path": "/dev/stderr", 154 | "max_clients": 50, 155 | "providers": [ 156 | { 157 | "/foo": { 158 | "type": "s3", 159 | "src": "s3://local-test/images", 160 | "region": "ap-northeast-1", 161 | "norm_form": "nfd", 162 | "use_mock": true 163 | } 164 | }, 165 | { 166 | "/bar": { 167 | "type": "web", 168 | "src": "http://localhost:3000/foo" 169 | } 170 | }, 171 | { 172 | "/baz": { 173 | "type": "local", 174 | "src": "img" 175 | } 176 | } 177 | 178 | ] 179 | } 180 | ``` 181 | 182 | Also, It requires booting the mock server in advance. 183 | 184 | ``` 185 | $ docker compose up 186 | $ make create-s3-bucket 187 | $ make copy-object SRC=img/Lenna.jpg DEST=images/Lenna.jpg 188 | $ make run 189 | ``` 190 | 191 | Now you can test fanlin locally. 192 | 193 | ``` 194 | $ curl -I 'http://localhost:3000/foo/Lenna.jpg?w=300&h=200&rgb=64,64,64' 195 | $ curl -I 'http://localhost:3000/bar/Lenna.jpg?w=300&h=200&rgb=64,64,64' 196 | $ curl -I 'http://localhost:3000/baz/Lenna.jpg?w=300&h=200&rgb=64,64,64' 197 | ``` 198 | 199 | ## LICENSE 200 | Written in Go and licensed under [the MIT License](https://opensource.org/licenses/MIT), it can also be used as a library. 201 | -------------------------------------------------------------------------------- /cmd/fanlin/.gitignore: -------------------------------------------------------------------------------- 1 | aws.json 2 | *.log 3 | lvimg.json 4 | fanlin.json 5 | /server 6 | -------------------------------------------------------------------------------- /cmd/fanlin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "path/filepath" 13 | "runtime" 14 | "syscall" 15 | "time" 16 | 17 | configure "github.com/livesense-inc/fanlin/lib/conf" 18 | "github.com/livesense-inc/fanlin/lib/handler" 19 | "github.com/livesense-inc/fanlin/lib/logger" 20 | servertiming "github.com/mitchellh/go-server-timing" 21 | "github.com/sirupsen/logrus" 22 | "golang.org/x/net/netutil" 23 | ) 24 | 25 | var confList = []string{ 26 | "/etc/fanlin.json", 27 | "/etc/fanlin.cnf", 28 | "/etc/fanlin.conf", 29 | "/usr/local/etc/fanlin.json", 30 | "/usr/local/etc/fanlin.cnf", 31 | "/usr/local/etc/fanlin.conf", 32 | "/usr/local/fanlin/fanlin.json", 33 | "/usr/local/fanlin.json", 34 | "/usr/local/fanlin.conf", 35 | "./fanlin.json", 36 | "./fanlin.cnf", 37 | "./fanlin.conf", 38 | "./conf.json", 39 | "~/.fanlin.json", 40 | "~/.fanlin.cnf", 41 | } 42 | 43 | var ( 44 | buildVersion string 45 | buildHash string 46 | buildDate string 47 | goversion string 48 | ) 49 | 50 | var ( 51 | vOption bool 52 | ) 53 | 54 | func showVersion() { 55 | fmt.Println() 56 | if buildVersion != "" { 57 | fmt.Println("build version: ", buildVersion) 58 | } else { 59 | fmt.Println("build version: ", buildHash) 60 | } 61 | fmt.Println("build date: ", buildDate) 62 | fmt.Println("GO version: ", goversion) 63 | } 64 | 65 | func main() { 66 | conf := func() *configure.Conf { 67 | for _, confName := range confList { 68 | if conf := configure.NewConfigure(confName); conf != nil { 69 | if p, err := filepath.Abs(confName); err != nil { 70 | fmt.Println("read configure. :", confName) 71 | } else { 72 | fmt.Println("read configure. :", p) 73 | } 74 | return conf 75 | } 76 | } 77 | return nil 78 | }() 79 | if conf == nil { 80 | log.Fatal("Can not read configure.") 81 | } 82 | 83 | notFoundImagePath := flag.String("nfi", conf.NotFoundImagePath(), "not found image path") 84 | errorLogPath := flag.String("err", conf.ErrorLogPath(), "error log path") 85 | accessLogPath := flag.String("log", conf.AccessLogPath(), "access log path") 86 | port := flag.Int("p", conf.Port(), "port") 87 | port = flag.Int("port", *port, "port") 88 | maxProcess := flag.Int("cpu", conf.MaxProcess(), "max process.") 89 | debug := flag.Bool("debug", false, "debug mode.") 90 | flag.BoolVar(&vOption, "v", false, "version") 91 | flag.Parse() 92 | 93 | if vOption { 94 | showVersion() 95 | os.Exit(128) 96 | } 97 | 98 | conf.Set("404_img_path", *notFoundImagePath) 99 | conf.Set("error_log_path", *errorLogPath) 100 | conf.Set("access_log_path", *accessLogPath) 101 | conf.Set("port", *port) 102 | conf.Set("max_process", *maxProcess) 103 | 104 | loggers := map[string]*logrus.Logger{ 105 | "err": logger.NewLogger(conf.ErrorLogPath()), 106 | "access": logger.NewLogger(conf.AccessLogPath()), 107 | } 108 | 109 | if *debug { 110 | log.Print(conf) 111 | } 112 | 113 | http.DefaultClient.Timeout = conf.BackendRequestTimeout() 114 | runtime.GOMAXPROCS(conf.MaxProcess()) 115 | 116 | if err := handler.Prepare(conf); err != nil { 117 | log.Fatal(err) 118 | } 119 | 120 | fn := func(w http.ResponseWriter, r *http.Request) { 121 | handler.MainHandler(w, r, conf, loggers) 122 | } 123 | var h http.Handler = http.HandlerFunc(fn) 124 | if conf.UseServerTiming() { 125 | h = servertiming.Middleware(h, nil) 126 | } 127 | http.Handle("/", h) 128 | http.HandleFunc("/healthCheck", handler.HealthCheckHandler) 129 | 130 | if conf.EnableMetricsEndpoint() { 131 | metricsHandler := handler.MakeMetricsHandler(conf, log.New(os.Stderr, "", log.LstdFlags)) 132 | http.HandleFunc("/metrics", metricsHandler.ServeHTTP) 133 | } 134 | 135 | if err := runServer(conf); err != nil { 136 | log.Fatal(err) 137 | } 138 | 139 | loggers["err"].Print("shut down") 140 | os.Exit(0) 141 | } 142 | 143 | func runServer(conf *configure.Conf) error { 144 | listener, err := net.Listen("tcp", fmt.Sprintf(":%d", conf.Port())) 145 | if err != nil { 146 | return err 147 | } 148 | if conf.MaxClients() > 0 { 149 | listener = netutil.LimitListener(listener, conf.MaxClients()) 150 | } 151 | defer listener.Close() 152 | 153 | server := &http.Server{} 154 | if conf.ServerTimeout() > 0*time.Second { 155 | server.ReadTimeout = conf.ServerTimeout() 156 | server.WriteTimeout = conf.ServerTimeout() 157 | } 158 | if conf.ServerIdleTimeout() > 0*time.Second { 159 | server.IdleTimeout = conf.ServerIdleTimeout() 160 | } 161 | 162 | c := make(chan os.Signal, 1) 163 | defer close(c) 164 | signal.Notify(c, syscall.SIGTERM, syscall.SIGINT, os.Interrupt) 165 | defer signal.Stop(c) 166 | 167 | go func(s *http.Server, c <-chan os.Signal) { 168 | if _, ok := <-c; ok { 169 | s.SetKeepAlivesEnabled(false) 170 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 171 | defer cancel() 172 | _ = s.Shutdown(ctx) 173 | } 174 | }(server, c) 175 | 176 | if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { 177 | return err 178 | } 179 | return nil 180 | } 181 | -------------------------------------------------------------------------------- /cmd/fanlin/sample-conf-container.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000, 3 | "max_width": 3000, 4 | "max_height": 3000, 5 | "404_img_path": "/var/lib/fanlin/img/404.png", 6 | "access_log_path": "/dev/stdout", 7 | "error_log_path": "/dev/stderr", 8 | "use_server_timing": true, 9 | "enable_metrics_endpoint": true, 10 | "max_clients": 50, 11 | "use_icc_profile_cmyk_converter": true, 12 | "providers": [ 13 | { 14 | "/" : { 15 | "type" : "local", 16 | "src" : "/var/lib/fanlin/img" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /cmd/fanlin/sample-conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000, 3 | "max_width": 1000, 4 | "max_height": 1000, 5 | "404_img_path": "img/404.png", 6 | "access_log_path": "/dev/stdout", 7 | "error_log_path": "/dev/stderr", 8 | "use_server_timing": true, 9 | "enable_metrics_endpoint": true, 10 | "max_clients": 50, 11 | "providers": [ 12 | { 13 | "/" : { 14 | "type" : "local", 15 | "src" : "img" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | s3: 4 | image: "minio/minio" 5 | ports: 6 | - "4567:9000" 7 | environment: 8 | MINIO_ROOT_USER: AAAAAAAAAAAAAAAAAAAA 9 | MINIO_ROOT_PASSWORD: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 10 | volumes: 11 | - ./tmp/s3:/data 12 | command: 13 | - server 14 | - /data 15 | # app: 16 | # build: . 17 | # image: fanlin 18 | # ports: 19 | # - "3001:3000" 20 | # volumes: 21 | # - ./img:/var/lib/fanlin/img 22 | # - ./cmd/fanlin/sample-conf-container.json:/etc/fanlin.json 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/livesense-inc/fanlin 2 | 3 | go 1.23.5 4 | 5 | require ( 6 | github.com/Kagami/go-avif v0.1.0 7 | github.com/aws/aws-sdk-go-v2 v1.36.3 8 | github.com/aws/aws-sdk-go-v2/config v1.29.9 9 | github.com/aws/aws-sdk-go-v2/credentials v1.17.62 10 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.66 11 | github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 12 | github.com/chai2010/webp v1.4.0 13 | github.com/disintegration/gift v1.2.1 14 | github.com/ieee0824/libcmyk v0.0.0-20171222081915-8cf9151a408c 15 | github.com/ieee0824/logrus-formatter v1.0.0 16 | github.com/livesense-inc/go-lcms v1.0.0 17 | github.com/mitchellh/go-server-timing v1.0.1 18 | github.com/prometheus/client_golang v1.21.1 19 | github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd 20 | github.com/sirupsen/logrus v1.9.3 21 | golang.org/x/image v0.25.0 22 | golang.org/x/net v0.38.0 23 | golang.org/x/text v0.23.0 24 | ) 25 | 26 | require ( 27 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect 28 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 29 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 30 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 31 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 32 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect 33 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 34 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect 35 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 36 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect 37 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect 38 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect 39 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect 40 | github.com/aws/smithy-go v1.22.2 // indirect 41 | github.com/beorn7/perks v1.0.1 // indirect 42 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 43 | github.com/felixge/httpsnoop v1.0.0 // indirect 44 | github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5 // indirect 45 | github.com/klauspost/compress v1.17.11 // indirect 46 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 47 | github.com/prometheus/client_model v0.6.1 // indirect 48 | github.com/prometheus/common v0.62.0 // indirect 49 | github.com/prometheus/procfs v0.15.1 // indirect 50 | golang.org/x/sys v0.31.0 // indirect 51 | google.golang.org/protobuf v1.36.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /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/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= 4 | github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 5 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs= 6 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14= 7 | github.com/aws/aws-sdk-go-v2/config v1.29.9 h1:Kg+fAYNaJeGXp1vmjtidss8O2uXIsXwaRqsQJKXVr+0= 8 | github.com/aws/aws-sdk-go-v2/config v1.29.9/go.mod h1:oU3jj2O53kgOU4TXq/yipt6ryiooYjlkqqVaZk7gY/U= 9 | github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU= 10 | github.com/aws/aws-sdk-go-v2/credentials v1.17.62/go.mod h1:ElETBxIQqcxej++Cs8GyPBbgMys5DgQPTwo7cUPDKt8= 11 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= 12 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= 13 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.66 h1:MTLivtC3s89de7Fe3P8rzML/8XPNRfuyJhlRTsCEt0k= 14 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.66/go.mod h1:NAuQ2s6gaFEsuTIb2+P5t6amB1w5MhvJFxppoezGWH0= 15 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= 16 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= 17 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= 18 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= 19 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 20 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 21 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= 22 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= 23 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= 24 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= 25 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU= 26 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0= 27 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= 28 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= 29 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg= 30 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA= 31 | github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 h1:jIiopHEV22b4yQP2q36Y0OmwLbsxNWdWwfZRR5QRRO4= 32 | github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc= 33 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0= 34 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= 35 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA= 36 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= 37 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= 38 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= 39 | github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= 40 | github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 41 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 42 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 43 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 44 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 45 | github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko= 46 | github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= 47 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 48 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 49 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 50 | github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc= 51 | github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= 52 | github.com/felixge/httpsnoop v1.0.0 h1:gh8fMGz0rlOv/1WmRZm7OgncIOTsAj21iNJot48omJQ= 53 | github.com/felixge/httpsnoop v1.0.0/go.mod h1:3+D9sFq0ahK/JeJPhCBUV1xlf4/eIYrUQaxulT0VzX8= 54 | github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5 h1:yrv1uUvgXH/tEat+wdvJMRJ4g51GlIydtDpU9pFjaaI= 55 | github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= 56 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 57 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 58 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 59 | github.com/ieee0824/libcmyk v0.0.0-20171222081915-8cf9151a408c h1:lZIOmqDjhSYVjRKP2uDatozIISiwqf2SKdJiY9guH/8= 60 | github.com/ieee0824/libcmyk v0.0.0-20171222081915-8cf9151a408c/go.mod h1:vJVtPiJ4YFtcMk8WkYjRnhqve5VTNuURZ2qmD0yRdyE= 61 | github.com/ieee0824/logrus-formatter v1.0.0 h1:xGeQsIDW3Cap0t2be6xyT4CEu4QTTNXxRGv8+foASM8= 62 | github.com/ieee0824/logrus-formatter v1.0.0/go.mod h1:QM8kqMUENvvVuwCSSd/LoSewOx30414UpVUjWf/OdZQ= 63 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 64 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 65 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 66 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 67 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 68 | github.com/livesense-inc/go-lcms v1.0.0 h1:T0m581YeSP2EeRC8N1x0zNFEMjdiDm6+cnf9rAyDHic= 69 | github.com/livesense-inc/go-lcms v1.0.0/go.mod h1:Jd/5ZbbCThy7Ohr0WX6ASlvqjck/vLEARBcR4wItHcw= 70 | github.com/mitchellh/go-server-timing v1.0.1 h1:f00/aIe8T3MrnLhQHu3tSWvnwc5GV/p5eutuu3hF/tE= 71 | github.com/mitchellh/go-server-timing v1.0.1/go.mod h1:Mo6GKi9FSLwWFAMn3bqVPWe20y5ri5QGQuO9D9MCOxk= 72 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 73 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 74 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 75 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 76 | github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= 77 | github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= 78 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 79 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 80 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 81 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 82 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 83 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 84 | github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= 85 | github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= 86 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 87 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 88 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 89 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 90 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 91 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 92 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 93 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 94 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 95 | golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= 96 | golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= 97 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 98 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 99 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 102 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 103 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 104 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 105 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 106 | google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= 107 | google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 108 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 109 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 110 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 111 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 112 | -------------------------------------------------------------------------------- /img/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livesense-inc/fanlin/4c91fb7b2c7b35d19837d08f97fc4801ce568146/img/404.png -------------------------------------------------------------------------------- /img/Lenna.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livesense-inc/fanlin/4c91fb7b2c7b35d19837d08f97fc4801ce568146/img/Lenna.jpg -------------------------------------------------------------------------------- /img/test/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livesense-inc/fanlin/4c91fb7b2c7b35d19837d08f97fc4801ce568146/img/test/test.png -------------------------------------------------------------------------------- /lib/conf/conf.go: -------------------------------------------------------------------------------- 1 | package configure 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type Conf struct { 14 | c map[string]interface{} 15 | } 16 | 17 | func (c *Conf) UseMLCMYKConverter() bool { 18 | b, ok := c.c["use_ml_cmyk_converter"] 19 | if !ok { 20 | return false 21 | } 22 | r, ok := b.(bool) 23 | if !ok { 24 | return false 25 | } 26 | return r 27 | } 28 | 29 | func (c *Conf) MLCMYKConverterNetworkFilePath() string { 30 | i, ok := c.c["ml_cmyk_converter_network_file_path"] 31 | if !ok { 32 | return "" 33 | } 34 | s, ok := i.(string) 35 | if !ok { 36 | return "" 37 | } 38 | return s 39 | } 40 | 41 | func (c *Conf) UseICCProfileCMYKConverter() bool { 42 | b, ok := c.c["use_icc_profile_cmyk_converter"] 43 | if !ok { 44 | return false 45 | } 46 | r, ok := b.(bool) 47 | if !ok { 48 | return false 49 | } 50 | return r 51 | } 52 | 53 | func (c *Conf) UseServerTiming() bool { 54 | b, ok := c.c["use_server_timing"] 55 | if !ok { 56 | return false 57 | } 58 | r, ok := b.(bool) 59 | if !ok { 60 | return false 61 | } 62 | return r 63 | } 64 | 65 | func (c *Conf) EnableMetricsEndpoint() bool { 66 | b, ok := c.c["enable_metrics_endpoint"] 67 | if !ok { 68 | return false 69 | } 70 | r, ok := b.(bool) 71 | if !ok { 72 | return false 73 | } 74 | return r 75 | } 76 | 77 | func (c *Conf) MaxClients() int { 78 | b, ok := c.c["max_clients"] 79 | if !ok { 80 | return 0 81 | } 82 | n, ok := b.(float64) 83 | if !ok { 84 | return 50 85 | } 86 | return int(n) 87 | } 88 | 89 | func (c *Conf) Set(k string, v interface{}) { 90 | if v == nil { 91 | return 92 | } 93 | c.c[k] = v 94 | } 95 | 96 | func (c *Conf) Get(k string) interface{} { 97 | return c.c[k] 98 | } 99 | 100 | func (c *Conf) UA() string { 101 | ua := c.c["user_agent"] 102 | if ua == nil { 103 | ua = fmt.Sprintf("Mozilla/5.0 (fanlin; arch: %s; OS: %s; Go version: %s) Go language Client/1.1 (KHTML, like Gecko) Version/1.0 fanlin", runtime.GOARCH, runtime.GOOS, runtime.Version()) 104 | } 105 | return ua.(string) 106 | } 107 | 108 | func (c *Conf) Providers() []interface{} { 109 | if providers, ok := c.c["providers"].([]interface{}); ok { 110 | return providers 111 | } 112 | return nil 113 | } 114 | 115 | func (c *Conf) NotFoundImagePath() string { 116 | path := c.c["404_img_path"] 117 | if path == nil { 118 | path = "./" 119 | } 120 | return path.(string) 121 | } 122 | 123 | func (c *Conf) ErrorLogPath() string { 124 | path := c.c["error_log_path"] 125 | if path == nil { 126 | path = "./error.log" 127 | } 128 | return path.(string) 129 | } 130 | 131 | func (c *Conf) AccessLogPath() string { 132 | path := c.c["access_log_path"] 133 | if path == nil { 134 | path = "./access.log" 135 | } 136 | return path.(string) 137 | } 138 | 139 | func (c *Conf) DebugLogPath() string { 140 | path := c.c["debug_log_path"] 141 | if path == nil { 142 | path = "/dev/null" 143 | } 144 | return path.(string) 145 | } 146 | 147 | func (c *Conf) BackendRequestTimeout() time.Duration { 148 | tstr, ok := c.c["backend_request_timeout"] 149 | if !ok { 150 | return 10 * time.Second 151 | } 152 | 153 | t, ok := tstr.(string) 154 | if !ok { 155 | return 10 * time.Second 156 | } 157 | 158 | d, err := time.ParseDuration(t) 159 | if err != nil { 160 | return 10 * time.Second 161 | } 162 | return d 163 | } 164 | 165 | func (c *Conf) ServerTimeout() time.Duration { 166 | tstr, ok := c.c["server_timeout"] 167 | if !ok { 168 | return 0 * time.Second 169 | } 170 | 171 | t, ok := tstr.(string) 172 | if !ok { 173 | return 0 * time.Second 174 | } 175 | 176 | d, err := time.ParseDuration(t) 177 | if err != nil { 178 | return 0 * time.Second 179 | } 180 | return d 181 | } 182 | 183 | func (c *Conf) ServerIdleTimeout() time.Duration { 184 | tstr, ok := c.c["server_idle_timeout"] 185 | if !ok { 186 | return 0 * time.Second 187 | } 188 | 189 | t, ok := tstr.(string) 190 | if !ok { 191 | return 0 * time.Second 192 | } 193 | 194 | d, err := time.ParseDuration(t) 195 | if err != nil { 196 | return 0 * time.Second 197 | } 198 | return d 199 | } 200 | 201 | func (c *Conf) Port() int { 202 | port := c.c["port"] 203 | return convInterfaceToInt(port, 8080) 204 | } 205 | 206 | func (c *Conf) MaxSize() (uint, uint) { 207 | width := c.c["max_width"] 208 | height := c.c["max_height"] 209 | w := convInterfaceToInt(width, 1000) 210 | h := convInterfaceToInt(height, 1000) 211 | 212 | return uint(w), uint(h) 213 | } 214 | 215 | func (c *Conf) MaxProcess() int { 216 | maxProcess := c.c["max_process"] 217 | return convInterfaceToInt(maxProcess, runtime.NumCPU()) 218 | } 219 | 220 | func convInterfaceToInt(v interface{}, exception int) int { 221 | if n, ok := v.(float64); ok { 222 | return int(n) 223 | } else if n, ok := v.(int); ok { 224 | return n 225 | } else if n, ok := v.(uint); ok { 226 | return int(n) 227 | } 228 | return exception 229 | } 230 | 231 | func (c *Conf) getIncludePath() []string { 232 | if includes, ok := c.c["include"].([]interface{}); ok { 233 | ret := make([]string, len(includes)) 234 | for i, s := range includes { 235 | if path, ok := s.(string); ok { 236 | ret[i] = path 237 | } 238 | } 239 | return ret 240 | } 241 | return nil 242 | } 243 | 244 | func toAbsPath(mainConfPath string, includeConfPath string) string { 245 | var includeAbs string 246 | mainConfAbs, err := filepath.Abs(mainConfPath) 247 | if err != nil { 248 | return "" 249 | } 250 | if filepath.IsAbs(includeConfPath) { 251 | return includeConfPath 252 | } 253 | includeAbs, err = filepath.Abs(mainConfAbs + "/" + includeConfPath) 254 | if err != nil { 255 | return "" 256 | } 257 | return includeAbs 258 | } 259 | 260 | func (c *Conf) includeConfigure(mainConfPath string, pathList []string) { 261 | parentConfPath := func() string { 262 | d := strings.Split(mainConfPath, "/") 263 | d = d[:len(d)-1] 264 | return strings.Join(d, "/") 265 | }() 266 | for _, path := range pathList { 267 | abs := toAbsPath(parentConfPath, path) 268 | if abs == "" { 269 | continue 270 | } 271 | include := NewConfigure(abs) 272 | if include == nil { 273 | continue 274 | } 275 | 276 | for k, v := range include.c { 277 | c.c[k] = v 278 | } 279 | } 280 | } 281 | 282 | func NewConfigure(confPath string) *Conf { 283 | var conf map[string]interface{} 284 | bin, err := os.ReadFile(confPath) 285 | if err != nil { 286 | return nil 287 | } 288 | err = json.Unmarshal(bin, &conf) 289 | if err != nil { 290 | return nil 291 | } 292 | c := Conf{ 293 | c: conf, 294 | } 295 | includes := c.getIncludePath() 296 | c.includeConfigure(confPath, includes) 297 | 298 | delete(c.c, "include") 299 | return &c 300 | } 301 | -------------------------------------------------------------------------------- /lib/conf/conf_test.go: -------------------------------------------------------------------------------- 1 | package configure 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | var testConfig = "../test/test_conf.json" 9 | 10 | func TestReadConfigure(t *testing.T) { 11 | conf := NewConfigure(testConfig) 12 | func() { 13 | t.Log("test conf struct all.") 14 | if conf == nil { 15 | t.Fatalf("conf is nil.") 16 | } 17 | }() 18 | 19 | func() { 20 | t.Log("port setting test.") 21 | if conf.Port() != 8080 { 22 | t.Fatalf("port is not equal 8080, value is \"%v\"", conf.Port()) 23 | } 24 | }() 25 | 26 | func() { 27 | t.Log("max size test") 28 | w, h := conf.MaxSize() 29 | if w != 5000 { 30 | t.Fatalf("value is %v", w) 31 | } 32 | if h != 5000 { 33 | t.Fatalf("value is %v", h) 34 | } 35 | }() 36 | 37 | func() { 38 | t.Log("use_server_timing test") 39 | ok := conf.UseServerTiming() 40 | if !ok { 41 | t.Fatalf("value is %v", ok) 42 | } 43 | }() 44 | 45 | func() { 46 | t.Log("enable_metrics_endpoint test") 47 | ok := conf.EnableMetricsEndpoint() 48 | if !ok { 49 | t.Fatalf("value is %v", ok) 50 | } 51 | }() 52 | 53 | func() { 54 | t.Log("max_clients test") 55 | n := conf.MaxClients() 56 | if n != 50 { 57 | t.Fatalf("value is %d", n) 58 | } 59 | }() 60 | 61 | func() { 62 | t.Log("server_timeout test") 63 | n := conf.ServerTimeout() 64 | if n != 30*time.Second { 65 | t.Errorf("value is %d", n) 66 | } 67 | }() 68 | 69 | func() { 70 | t.Log("server_idle_timeout test") 71 | n := conf.ServerIdleTimeout() 72 | if n != 65*time.Second { 73 | t.Errorf("value is %d", n) 74 | } 75 | }() 76 | 77 | func() { 78 | t.Log("use_icc_profile_cmyk_converter test") 79 | ok := conf.UseICCProfileCMYKConverter() 80 | if ok { 81 | t.Errorf("value is %v", ok) 82 | } 83 | }() 84 | } 85 | -------------------------------------------------------------------------------- /lib/content/content.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/url" 7 | "os" 8 | "sort" 9 | "strings" 10 | 11 | configure "github.com/livesense-inc/fanlin/lib/conf" 12 | ) 13 | 14 | type Content struct { 15 | SourcePlace string 16 | SourceType string 17 | Meta map[string]interface{} 18 | } 19 | 20 | type provider struct { 21 | alias string 22 | meta map[string]interface{} 23 | } 24 | 25 | var ( 26 | providers []provider 27 | noContentImage []byte 28 | ) 29 | 30 | const DEFAULT_PRIORITY float64 = 10.0 31 | 32 | func init() { 33 | providers = nil 34 | noContentImage = nil 35 | } 36 | 37 | func getProviders(c *configure.Conf) []provider { 38 | ret := make([]provider, 0, len(c.Providers())) 39 | for _, p := range c.Providers() { 40 | for alias, meta := range convertInterfaceToMap(p) { 41 | m := convertInterfaceToMap(meta) 42 | if _, ok := m["priority"]; !ok { 43 | m["priority"] = DEFAULT_PRIORITY 44 | } 45 | 46 | ret = append(ret, provider{alias, m}) 47 | } 48 | } 49 | 50 | sort.Slice(ret, func(i, j int) bool { 51 | return ret[i].meta["priority"].(float64) < ret[j].meta["priority"].(float64) 52 | }) 53 | 54 | return ret 55 | } 56 | 57 | func getContent(urlPath string, p []provider) *Content { 58 | if urlPath == "/" || urlPath == "" { 59 | return nil 60 | } 61 | var ret Content 62 | ret.Meta = map[string]interface{}{} 63 | index := serachProviderIndex(urlPath, p) 64 | if index < 0 { 65 | return nil 66 | } 67 | targetProvider := p[index] 68 | for k, v := range targetProvider.meta { 69 | switch k { 70 | case "src": 71 | src := v.(string) 72 | path := urlPath[len(targetProvider.alias):] 73 | if !strings.HasPrefix(path, "/") { 74 | path = "/" + path 75 | } 76 | 77 | ret.SourcePlace, _ = url.QueryUnescape(src + path) 78 | case "type": 79 | ret.SourceType = v.(string) 80 | default: 81 | ret.Meta[k] = v 82 | } 83 | } 84 | return &ret 85 | } 86 | 87 | func serachProviderIndex(urlPath string, p []provider) int { 88 | for i, v := range p { 89 | if strings.HasPrefix(urlPath, v.alias) { 90 | return i 91 | } 92 | } 93 | return -1 94 | } 95 | 96 | func convertInterfaceToMap(i interface{}) map[string]interface{} { 97 | if ret, ok := i.(map[string]interface{}); ok { 98 | return ret 99 | } 100 | return map[string]interface{}(nil) 101 | } 102 | 103 | func loadFile(path string) ([]byte, error) { 104 | f, err := os.Open(path) 105 | if err != nil { 106 | return nil, err 107 | } 108 | defer f.Close() 109 | var b bytes.Buffer 110 | if _, err := io.Copy(&b, f); err != nil { 111 | return nil, err 112 | } 113 | return b.Bytes(), nil 114 | } 115 | 116 | func SetupNoContentImage(conf *configure.Conf) (err error) { 117 | if conf == nil { 118 | return 119 | } 120 | 121 | noContentImage, err = loadFile(conf.NotFoundImagePath()) 122 | return 123 | } 124 | 125 | func SetUpProviders(conf *configure.Conf) { 126 | if conf == nil { 127 | return 128 | } 129 | 130 | providers = getProviders(conf) 131 | } 132 | 133 | func GetNoContentImage() io.Reader { 134 | return bytes.NewReader(noContentImage) 135 | } 136 | 137 | func GetContent(urlPath string, _conf *configure.Conf) *Content { 138 | if urlPath == "" { 139 | return nil 140 | } 141 | 142 | return getContent(urlPath, providers) 143 | } 144 | -------------------------------------------------------------------------------- /lib/content/content_test.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | configure "github.com/livesense-inc/fanlin/lib/conf" 8 | ) 9 | 10 | func TestGetContent(t *testing.T) { 11 | t.Parallel() 12 | conf := configure.NewConfigure("../test/test_conf8.json") 13 | if conf == nil { 14 | t.Fatal("failed to read conf") 15 | } 16 | SetUpProviders(conf) 17 | cases := []struct { 18 | urlPath string 19 | wantSourcePlace string 20 | }{ 21 | {"/image.jpg", "/tmp/image.jpg"}, 22 | {"/foo/image.jpg", "/tmp/foo/image.jpg"}, 23 | {"/foobar/image.jpg", "/tmp/foobar/image.jpg"}, 24 | {"/foobarbaz/image.jpg", "/tmp/foobarbaz/image.jpg"}, 25 | {"/foobarbazgqu/image.jpg", "/tmp/foobarbazgqu/image.jpg"}, 26 | {"/foobarbazgquu/image.jpg", "/tmp/foobarbazgquu/image.jpg"}, 27 | {"/foobarbazgquuu/image.jpg", "/tmp/foobarbazgquuu/image.jpg"}, 28 | {"/foobarbazgquuuu/image.jpg", "/tmp/foobarbazgquuuu/image.jpg"}, 29 | {"/foobarbazgquuuuu/image.jpg", "/tmp/foobarbazgquuuuu/image.jpg"}, 30 | {"/foobarbazgquuuuuu/image.jpg", "/tmp/foobarbazgquuuuuu/image.jpg"}, 31 | } 32 | for n, c := range cases { 33 | n := n 34 | c := c 35 | t.Run(fmt.Sprintf("case-%d", n), func(t *testing.T) { 36 | t.Parallel() 37 | got := GetContent(c.urlPath, conf) 38 | if got == nil { 39 | t.Errorf("no content") 40 | return 41 | } 42 | if got.SourcePlace != c.wantSourcePlace { 43 | t.Errorf("want=%s, got=%s", c.wantSourcePlace, got.SourcePlace) 44 | } 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/content/local/local.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path" 9 | 10 | "github.com/livesense-inc/fanlin/lib/content" 11 | ) 12 | 13 | func GetImageBinary(c *content.Content) (io.Reader, error) { 14 | f, err := os.Open(path.Clean(c.SourcePlace)) 15 | if err != nil { 16 | return nil, fmt.Errorf("failed to open a file: %s: %w", c.SourcePlace, err) 17 | } 18 | defer f.Close() 19 | var b bytes.Buffer 20 | if _, err := io.Copy(&b, f); err != nil { 21 | return nil, err 22 | } 23 | return &b, nil 24 | } 25 | 26 | func init() { 27 | content.RegisterContentType("local", GetImageBinary) 28 | } 29 | -------------------------------------------------------------------------------- /lib/content/local/local_test.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/livesense-inc/fanlin/lib/content" 8 | ) 9 | 10 | func TestGetImageBinary(t *testing.T) { 11 | c := content.Content{ 12 | SourcePlace: "../../test/img/Lenna.jpg", 13 | } 14 | if r, err := GetImageBinary(&c); err != nil { 15 | t.Fatal(err) 16 | } else { 17 | if b, err := io.ReadAll(r); err != nil { 18 | t.Fatal(err) 19 | } else { 20 | if len(b) == 0 { 21 | t.Error("something was wrong: zero byte") 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/content/s3/s3.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/url" 10 | "strings" 11 | 12 | "github.com/aws/aws-sdk-go-v2/aws" 13 | "github.com/aws/aws-sdk-go-v2/config" 14 | "github.com/aws/aws-sdk-go-v2/credentials" 15 | s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 16 | "github.com/aws/aws-sdk-go-v2/service/s3" 17 | "github.com/livesense-inc/fanlin/lib/content" 18 | imgproxyerr "github.com/livesense-inc/fanlin/lib/error" 19 | "golang.org/x/text/unicode/norm" 20 | ) 21 | 22 | var s3GetSourceFunc = getS3ImageBinary 23 | 24 | // Test dedicated function 25 | func setS3GetFunc(f func(cli *s3.Client, bucket, key string) (io.Reader, error)) { 26 | s3GetSourceFunc = f 27 | } 28 | 29 | func GetImageBinary(c *content.Content) (io.Reader, error) { 30 | if c == nil { 31 | return nil, errors.New("content is nil") 32 | } 33 | s3url := c.SourcePlace 34 | u, err := url.Parse(s3url) 35 | if err != nil { 36 | return nil, imgproxyerr.New(imgproxyerr.WARNING, errors.New("can not parse s3 url")) 37 | } 38 | 39 | bucket := u.Host 40 | 41 | if region, ok := c.Meta["region"].(string); ok { 42 | path, err := url.QueryUnescape(u.EscapedPath()) 43 | if err != nil { 44 | return nil, err 45 | } 46 | if form, ok := c.Meta["norm_form"].(string); ok { 47 | path, err = NormalizePath(path, form) 48 | if err != nil { 49 | return nil, err 50 | } 51 | } 52 | useMock := false 53 | if v, ok := c.Meta["use_mock"]; ok { 54 | if b, ok := v.(bool); ok { 55 | useMock = b 56 | } 57 | } 58 | cli, err := makeClient(region, useMock) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return s3GetSourceFunc(cli, bucket, path) 63 | } 64 | return nil, imgproxyerr.New(imgproxyerr.ERROR, errors.New("can not parse configure")) 65 | } 66 | 67 | func makeClient(region string, useMock bool) (*s3.Client, error) { 68 | if !useMock { 69 | cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region)) 70 | if err != nil { 71 | return nil, err 72 | } 73 | return s3.NewFromConfig( 74 | cfg, 75 | func(o *s3.Options) { 76 | o.DisableLogOutputChecksumValidationSkipped = true 77 | }, 78 | ), nil 79 | } 80 | 81 | cfg, err := config.LoadDefaultConfig( 82 | context.TODO(), 83 | config.WithRegion(region), 84 | config.WithCredentialsProvider( 85 | credentials.NewStaticCredentialsProvider( 86 | "AAAAAAAAAAAAAAAAAAAA", 87 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 88 | "", 89 | ), 90 | ), 91 | ) 92 | if err != nil { 93 | return nil, err 94 | } 95 | return s3.NewFromConfig( 96 | cfg, 97 | s3.WithEndpointResolverV2( 98 | s3.EndpointResolverV2( 99 | s3.NewDefaultEndpointResolverV2(), 100 | ), 101 | ), 102 | func(o *s3.Options) { 103 | o.BaseEndpoint = aws.String("http://localhost:4567") 104 | o.UsePathStyle = true 105 | o.DisableLogOutputChecksumValidationSkipped = true 106 | }, 107 | ), nil 108 | } 109 | 110 | func NormalizePath(path string, form string) (string, error) { 111 | switch form { 112 | case "nfd": 113 | return norm.NFD.String(path), nil 114 | case "nfc": 115 | return norm.NFC.String(path), nil 116 | case "nfkc": 117 | return norm.NFKC.String(path), nil 118 | case "nfkd": 119 | return norm.NFKD.String(path), nil 120 | } 121 | return "", imgproxyerr.New(imgproxyerr.WARNING, errors.New("invalid normalization form("+form+")")) 122 | } 123 | 124 | func getS3ImageBinary(cli *s3.Client, bucket, key string) (io.Reader, error) { 125 | key = strings.TrimPrefix(key, "/") 126 | downloader := s3manager.NewDownloader(cli) 127 | buf := s3manager.NewWriteAtBuffer([]byte{}) 128 | input := &s3.GetObjectInput{ 129 | Bucket: aws.String(bucket), 130 | Key: aws.String(key), 131 | } 132 | _, err := downloader.Download(context.TODO(), buf, input) 133 | if err != nil { 134 | return nil, imgproxyerr.New(imgproxyerr.WARNING, fmt.Errorf("bucket=%s, key=%s: %w", bucket, key, err)) 135 | } 136 | return bytes.NewReader(buf.Bytes()), nil 137 | } 138 | 139 | func init() { 140 | content.RegisterContentType("s3", GetImageBinary) 141 | } 142 | -------------------------------------------------------------------------------- /lib/content/s3/s3_test.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/aws/aws-sdk-go-v2/service/s3" 10 | "github.com/livesense-inc/fanlin/lib/content" 11 | ) 12 | 13 | var ( 14 | SetS3GetFunc = setS3GetFunc 15 | testBucket = "testBucket" 16 | testRegion = "ap-northeast-1" 17 | testKey = "test/test.jpg" 18 | ) 19 | 20 | func initialize() { 21 | SetS3GetFunc(mockS3GetFunc) 22 | testBucket = "testBucket" 23 | testRegion = "ap-northeast-1" 24 | testKey = "test/test.jpg" 25 | } 26 | 27 | func mockS3GetFunc(client *s3.Client, bucket, key string) (io.Reader, error) { 28 | if client == nil { 29 | return strings.NewReader("failed"), errors.New("client is empty") 30 | } else if bucket == "" { 31 | return strings.NewReader("failed"), errors.New("bucket is empty") 32 | } else if bucket != testBucket { 33 | return strings.NewReader("failed"), errors.New("Mismatch of the bucket. bucket: " + bucket + ", testBucket: " + testBucket) 34 | } else if key == "" { 35 | return strings.NewReader("failed"), errors.New("key is empty") 36 | } else if key == testKey { 37 | return strings.NewReader("failed"), errors.New("Mismatch of the key. key:" + key + ", testKey:" + testKey) 38 | } 39 | return strings.NewReader("success."), nil 40 | } 41 | 42 | func newTestContent() *content.Content { 43 | return &content.Content{ 44 | SourcePlace: "s3://" + testBucket + "/" + testKey, 45 | SourceType: "s3", 46 | Meta: map[string]interface{}{ 47 | "region": testRegion, 48 | }, 49 | } 50 | } 51 | 52 | func TestGetImageBinary(t *testing.T) { 53 | initialize() 54 | c := newTestContent() 55 | if _, err := GetImageBinary(c); err != nil { 56 | t.Log("normal pattern.") 57 | t.Fatal(err) 58 | } 59 | if _, err := GetImageBinary(nil); err == nil { 60 | t.Log("abnormal pattern.") 61 | t.Fatal("err is nil.") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/content/source.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "errors" 5 | 6 | "io" 7 | 8 | imgproxyerr "github.com/livesense-inc/fanlin/lib/error" 9 | ) 10 | 11 | type source struct { 12 | name string 13 | getImageBinary func(*Content) (io.Reader, error) 14 | } 15 | 16 | var sources []source 17 | 18 | // RegisterContentType registers an content type for use by GetContent. 19 | // Name is the name of the content type, like "web" or "s3". 20 | func RegisterContentType(name string, getImageBinary func(*Content) (io.Reader, error)) { 21 | sources = append(sources, source{ 22 | name, 23 | getImageBinary, 24 | }) 25 | } 26 | 27 | // Sniff determines the contentType of c's data. 28 | func sniff(c *Content) source { 29 | for _, ci := range sources { 30 | if ci.name == c.SourceType { 31 | return ci 32 | } 33 | } 34 | return source{} 35 | } 36 | 37 | func GetImageBinary(c *Content) (io.Reader, error) { 38 | f := sniff(c) 39 | if f.getImageBinary == nil { 40 | return nil, imgproxyerr.New(imgproxyerr.WARNING, errors.New("unknown content type")) 41 | } 42 | m, err := f.getImageBinary(c) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return m, nil 47 | } 48 | -------------------------------------------------------------------------------- /lib/content/web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "runtime" 7 | 8 | "bytes" 9 | "io" 10 | 11 | "github.com/livesense-inc/fanlin/lib/content" 12 | "github.com/livesense-inc/fanlin/lib/error" 13 | ) 14 | 15 | var ua = fmt.Sprintf("Mozilla/5.0 (fanlin; arch: %s; OS: %s; Go version: %s) Go language Client/1.1 (KHTML, like Gecko) Version/1.0 fanlin", runtime.GOARCH, runtime.GOOS, runtime.Version()) 16 | 17 | var httpClient = Client{ 18 | Http: new(RealWebClient), 19 | } 20 | 21 | type RealWebClient struct { 22 | } 23 | 24 | type WebClient interface { 25 | Get(string) (io.Reader, error) 26 | } 27 | 28 | type Client struct { 29 | Http WebClient 30 | } 31 | 32 | func (r *RealWebClient) Get(url string) (io.Reader, error) { 33 | req, err := http.NewRequest("GET", url, nil) 34 | if err != nil { 35 | return nil, imgproxyerr.New(imgproxyerr.ERROR, err) 36 | } 37 | req.Header.Set("User-Agent", ua) 38 | 39 | resp, err := http.DefaultClient.Do(req) 40 | if err != nil { 41 | return nil, imgproxyerr.New(imgproxyerr.ERROR, err) 42 | } 43 | defer resp.Body.Close() 44 | 45 | if isErrorCode(resp.StatusCode) { 46 | return nil, imgproxyerr.New(imgproxyerr.WARNING, fmt.Errorf("received error status code(%d)", resp.StatusCode)) 47 | } 48 | 49 | buffer := new(bytes.Buffer) 50 | if _, err := io.Copy(buffer, resp.Body); err != nil { 51 | return nil, err 52 | } 53 | 54 | return buffer, nil 55 | } 56 | 57 | func isErrorCode(status int) bool { 58 | switch status / 100 { 59 | case 4, 5: 60 | return true 61 | default: 62 | return false 63 | } 64 | } 65 | 66 | func GetImageBinary(c *content.Content) (io.Reader, error) { 67 | return httpClient.Http.Get(c.SourcePlace) 68 | } 69 | 70 | func setHttpClient(c Client) { 71 | httpClient = c 72 | } 73 | 74 | func init() { 75 | content.RegisterContentType("web", GetImageBinary) 76 | } 77 | -------------------------------------------------------------------------------- /lib/content/web/web_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/livesense-inc/fanlin/lib/content" 10 | ) 11 | 12 | var SetHttpClient = setHttpClient 13 | var ( 14 | IsErrorCode = isErrorCode 15 | targetURL = "https://google.co.jp" 16 | testReader = strings.NewReader("It works!") 17 | ) 18 | 19 | type MockWebClient struct { 20 | } 21 | 22 | func getTestClient() *Client { 23 | c := new(Client) 24 | c.Http = new(MockWebClient) 25 | return c 26 | } 27 | 28 | func (mwc *MockWebClient) Get(url string) (io.Reader, error) { 29 | if url != targetURL { 30 | return nil, errors.New("not match url. url: " + url + ", targetURL: " + targetURL) 31 | } 32 | return testReader, nil 33 | } 34 | 35 | func TestIsErrorCode(t *testing.T) { 36 | if IsErrorCode(200) { 37 | t.Fatal(200, IsErrorCode(200)) 38 | } 39 | if IsErrorCode(203) { 40 | t.Fatal(203, IsErrorCode(203)) 41 | } 42 | if !IsErrorCode(404) { 43 | t.Fatal(404, IsErrorCode(404)) 44 | } 45 | if !IsErrorCode(500) { 46 | t.Fatal(500, IsErrorCode(500)) 47 | } 48 | } 49 | 50 | func TestGetImageBinary(t *testing.T) { 51 | SetHttpClient(*getTestClient()) 52 | c := &content.Content{ 53 | SourcePlace: targetURL, 54 | } 55 | result, err := GetImageBinary(c) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | bin, err := io.ReadAll(result) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | if string(bin) != "It works!" { 64 | t.Fatal(string(bin)) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/error/error.go: -------------------------------------------------------------------------------- 1 | package imgproxyerr 2 | 3 | const ( 4 | ERROR = "error" 5 | WARNING = "warning" 6 | ) 7 | 8 | // やばそうやつから配列の前に積んでいく 9 | var level = [...]string{ 10 | ERROR, 11 | WARNING, 12 | } 13 | 14 | func getLevel(Type string) int { 15 | for l, t := range level { 16 | if t == Type { 17 | return l 18 | } 19 | } 20 | // 存在しないのが来た時わからないので危険側に倒す 21 | // 仕様上からの見過ごし防止の為 22 | return -1 23 | } 24 | 25 | func New(t string, err error) error { 26 | if err == nil { 27 | return nil 28 | } 29 | if e, ok := err.(*Err); ok { 30 | if e.cmp(t) { 31 | return &Err{e.Type, e} 32 | } 33 | } 34 | return &Err{t, err} 35 | } 36 | 37 | type Err struct { 38 | Type string 39 | Err error 40 | } 41 | 42 | func (e *Err) Error() string { 43 | return e.Err.Error() 44 | } 45 | 46 | func (e *Err) cmp(v string) bool { 47 | return getLevel(e.Type)-getLevel(v) <= 0 48 | } 49 | -------------------------------------------------------------------------------- /lib/error/error_test.go: -------------------------------------------------------------------------------- 1 | package imgproxyerr 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | var GetLevel = getLevel 9 | var Level = level 10 | var TestStr = "John Doe" 11 | var testWarn = &Err{WARNING, errors.New("test warning")} 12 | var testError = &Err{ERROR, errors.New("test error")} 13 | 14 | func TestNew(t *testing.T) { 15 | t.Log("test func New()") 16 | errNil := New("error", nil) 17 | if errNil != nil { 18 | t.Error(errNil, errNil.Error()) 19 | } 20 | errNNil := New("error", errors.New("test error")) 21 | if errNNil == nil { 22 | t.Error("error is nil") 23 | } 24 | 25 | func() { 26 | t.Log("warning -> errorへ上書きが発生するときのテスト") 27 | warningErr := New(WARNING, errors.New("warning")) 28 | errorErr := New(ERROR, warningErr) 29 | if e, ok := errorErr.(*Err); ok { 30 | if e.Type != ERROR { 31 | t.Fatal(e) 32 | } 33 | } else { 34 | t.Fatal("can not cast") 35 | } 36 | }() 37 | 38 | func() { 39 | t.Log("warning -> errorへ上書きが発生するときのテスト") 40 | errorErr := New(ERROR, errors.New("error")) 41 | warningErr := New(WARNING, errorErr) 42 | if e, ok := warningErr.(*Err); ok { 43 | if e.Type != ERROR { 44 | t.Fatal(e) 45 | } 46 | } else { 47 | t.Fatal("can not cast") 48 | } 49 | }() 50 | } 51 | 52 | func TestGetLevel(t *testing.T) { 53 | t.Log("test func getLevel()") 54 | t.Log("level変数に存在している時") 55 | 56 | if GetLevel(ERROR) != 0 { 57 | t.Fatal("level:", GetLevel(ERROR), ", status:", ERROR) 58 | } 59 | if GetLevel(WARNING) != 1 { 60 | t.Fatal("level:", GetLevel(WARNING), ", status:", WARNING) 61 | } 62 | if GetLevel("unknown") != -1 { 63 | t.Fatal("level:", GetLevel("unknown"), ", status:", "unknown") 64 | } 65 | if func(status string) bool { 66 | for i := range Level { 67 | if i == GetLevel(status) { 68 | return true 69 | } 70 | } 71 | return false 72 | }(TestStr) { 73 | t.Fatal("level:", GetLevel(TestStr), ", status", TestStr) 74 | } 75 | } 76 | 77 | func TestError(t *testing.T) { 78 | t.Log("test Error()") 79 | 80 | if testWarn.Error() != "test warning" { 81 | t.Fatal(testWarn.Type, testWarn.Error()) 82 | } 83 | if testError.Error() != "test error" { 84 | t.Fatal(testError.Type, testError.Error()) 85 | } 86 | } 87 | 88 | func TestCmp(t *testing.T) { 89 | t.Log("test cmp") 90 | 91 | if !testError.cmp(ERROR) { 92 | t.Fatal(testError.cmp(ERROR)) 93 | } 94 | if !testWarn.cmp(WARNING) { 95 | t.Fatal(testWarn.cmp(WARNING)) 96 | } 97 | if !testError.cmp(WARNING) { 98 | t.Fatal(testError.cmp(WARNING)) 99 | } 100 | if testWarn.cmp(ERROR) { 101 | t.Fatal(testWarn.cmp(ERROR)) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "os" 10 | "time" 11 | 12 | configure "github.com/livesense-inc/fanlin/lib/conf" 13 | "github.com/livesense-inc/fanlin/lib/content" 14 | _ "github.com/livesense-inc/fanlin/lib/content/local" 15 | _ "github.com/livesense-inc/fanlin/lib/content/s3" 16 | _ "github.com/livesense-inc/fanlin/lib/content/web" 17 | imgproxyerr "github.com/livesense-inc/fanlin/lib/error" 18 | imageprocessor "github.com/livesense-inc/fanlin/lib/image" 19 | "github.com/livesense-inc/fanlin/lib/query" 20 | _ "github.com/livesense-inc/fanlin/plugin" 21 | servertiming "github.com/mitchellh/go-server-timing" 22 | "github.com/prometheus/client_golang/prometheus" 23 | "github.com/prometheus/client_golang/prometheus/promhttp" 24 | "github.com/sirupsen/logrus" 25 | ) 26 | 27 | var devNull, _ = os.Open("/dev/null") 28 | 29 | func create404Page(w http.ResponseWriter, r *http.Request, conf *configure.Conf) { 30 | q := query.NewQueryFromGet(r) 31 | width, height := clampBounds(conf, q) 32 | w.WriteHeader(http.StatusNotFound) 33 | var b bytes.Buffer 34 | if err := imageprocessor.Set404Image( 35 | &b, 36 | content.GetNoContentImage(), 37 | width, 38 | height, 39 | *q.FillColor(), 40 | ); err != nil { 41 | writeDebugLog(err, conf.DebugLogPath()) 42 | log.Println(err) 43 | fmt.Fprintf(w, "%s", "404 Not found.") 44 | } else { 45 | _, _ = io.Copy(w, &b) 46 | } 47 | } 48 | 49 | func fallback( 50 | w http.ResponseWriter, 51 | r *http.Request, 52 | conf *configure.Conf, 53 | loggers map[string]*logrus.Logger, 54 | err error, 55 | ) { 56 | create404Page(w, r, conf) 57 | if err == nil { 58 | return 59 | } 60 | if loggers != nil { 61 | errLogger := func() *logrus.Entry { 62 | logger := loggers["err"] 63 | return logger.WithFields(logrus.Fields{ 64 | "UA": r.UserAgent(), 65 | "access_ip": r.RemoteAddr, 66 | "url": r.URL.String(), 67 | "type": r.Method, 68 | "version": r.Proto, 69 | }) 70 | }() 71 | if e, ok := err.(*imgproxyerr.Err); ok { 72 | switch e.Type { 73 | case imgproxyerr.WARNING: 74 | os.Stderr = devNull 75 | errLogger.Warn(err) 76 | case imgproxyerr.ERROR: 77 | writeDebugLog(err, conf.DebugLogPath()) 78 | errLogger.Error(err) 79 | default: 80 | writeDebugLog(err, conf.DebugLogPath()) 81 | errLogger.Error(err) 82 | } 83 | } else { 84 | writeDebugLog(err, conf.DebugLogPath()) 85 | errLogger.Error(err) 86 | } 87 | } else { 88 | writeDebugLog(err, conf.DebugLogPath()) 89 | log.Println(err) 90 | } 91 | fmt.Fprintf(w, "%s", "") 92 | } 93 | 94 | func writeDebugLog(err interface{}, debugFile string) { 95 | stackWriter, _ := os.OpenFile(debugFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 96 | t := time.Now() 97 | stackWriter.Write([]byte("\n")) 98 | stackWriter.Write([]byte("==========================================\n")) 99 | stackWriter.Write([]byte(t.String() + "\n")) 100 | stackWriter.Write([]byte(fmt.Sprint(err, "\n"))) 101 | stackWriter.Write([]byte("==========================================\n\n")) 102 | os.Stderr = stackWriter 103 | } 104 | 105 | func MainHandler( 106 | w http.ResponseWriter, 107 | r *http.Request, 108 | conf *configure.Conf, 109 | loggers map[string]*logrus.Logger, 110 | ) { 111 | accessLogger := loggers["access"] 112 | accessLogger.WithFields(logrus.Fields{ 113 | "UA": r.UserAgent(), 114 | "access_ip": r.RemoteAddr, 115 | "url": r.URL.String(), 116 | }).Info() 117 | 118 | timing := servertiming.FromContext(r.Context()) 119 | 120 | m := timing.NewMetric("f_load").Start() 121 | buf, err := getImage(r.URL.Path, conf) 122 | if err != nil { 123 | fallback( 124 | w, r, conf, loggers, 125 | imgproxyerr.New(imgproxyerr.WARNING, fmt.Errorf("failed to get image data: %w", err)), 126 | ) 127 | return 128 | } 129 | if buf == nil { 130 | create404Page(w, r, conf) 131 | return 132 | } 133 | m.Stop() 134 | 135 | q := query.NewQueryFromGet(r) 136 | 137 | m = timing.NewMetric("f_process").Start() 138 | img, err := processImage(buf, conf, q) 139 | if err != nil { 140 | fallback( 141 | w, r, conf, loggers, 142 | imgproxyerr.New(imgproxyerr.ERROR, fmt.Errorf("failed to decode image data: %w", err)), 143 | ) 144 | return 145 | } 146 | m.Stop() 147 | 148 | m = timing.NewMetric("f_encode").Start() 149 | var b bytes.Buffer 150 | if err := encodeImage(&b, img, q); err != nil { 151 | fallback( 152 | w, r, conf, loggers, 153 | imgproxyerr.New(imgproxyerr.ERROR, fmt.Errorf("failed to encode image data: %w", err)), 154 | ) 155 | return 156 | } 157 | m.Stop() 158 | 159 | if q.UseAVIF() { 160 | w.Header().Set("Content-Type", "image/avif") 161 | } 162 | w.WriteHeader(http.StatusOK) 163 | if _, err := io.Copy(w, &b); err != nil { 164 | loggers["err"].Print("failed to write data to response:", err) 165 | } 166 | } 167 | 168 | func getImage(reqPath string, conf *configure.Conf) (io.Reader, error) { 169 | ctt := content.GetContent(reqPath, conf) 170 | if ctt == nil { 171 | return nil, nil 172 | } 173 | return content.GetImageBinary(ctt) 174 | } 175 | 176 | func processImage(buf io.Reader, conf *configure.Conf, q *query.Query) (*imageprocessor.Image, error) { 177 | img, err := imageprocessor.DecodeImage(buf) 178 | if err != nil { 179 | return nil, err 180 | } 181 | if conf.UseMLCMYKConverter() { 182 | if err := img.ConvertColor(conf.MLCMYKConverterNetworkFilePath()); err != nil { 183 | return nil, err 184 | } 185 | } else if conf.UseICCProfileCMYKConverter() { 186 | img.ConvertColorWithICCProfile() 187 | } 188 | img.ApplyOrientation() 189 | w, h := clampBounds(conf, q) 190 | if q.Crop() { 191 | img.Crop(w, h) 192 | } else { 193 | img.ResizeAndFill(w, h, *q.FillColor()) 194 | } 195 | if q.Grayscale() { 196 | img.Grayscale() 197 | } else if q.Inverse() { 198 | img.Invert() 199 | } 200 | img.Process() 201 | return img, nil 202 | } 203 | 204 | func clampBounds(conf *configure.Conf, q *query.Query) (w uint, h uint) { 205 | mW, mX := conf.MaxSize() 206 | b := q.Bounds() 207 | w = min(b.W, mW) 208 | h = min(b.H, mX) 209 | return 210 | } 211 | 212 | func encodeImage( 213 | w io.Writer, 214 | img *imageprocessor.Image, 215 | q *query.Query, 216 | ) (err error) { 217 | switch img.GetFormat() { 218 | case "jpeg": 219 | if q.UseWebP() { 220 | err = imageprocessor.EncodeWebP(w, img.GetImg(), q.Quality(), false) 221 | } else if q.UseAVIF() { 222 | err = imageprocessor.EncodeAVIF(w, img.GetImg(), q.Quality()) 223 | } else { 224 | err = imageprocessor.EncodeJpeg(w, img.GetImg(), q.Quality()) 225 | } 226 | case "png": 227 | if q.UseWebP() { 228 | useLossless := (q.Quality() == 100) 229 | err = imageprocessor.EncodeWebP(w, img.GetImg(), q.Quality(), useLossless) 230 | } else if q.UseAVIF() { 231 | err = imageprocessor.EncodeAVIF(w, img.GetImg(), q.Quality()) 232 | } else { 233 | err = imageprocessor.EncodePNG(w, img.GetImg(), q.Quality()) 234 | } 235 | case "gif": 236 | if q.UseWebP() { 237 | useLossless := (q.Quality() == 100) 238 | err = imageprocessor.EncodeWebP(w, img.GetImg(), q.Quality(), useLossless) 239 | } else if q.UseAVIF() { 240 | err = imageprocessor.EncodeAVIF(w, img.GetImg(), q.Quality()) 241 | } else { 242 | err = imageprocessor.EncodeGIF(w, img.GetImg(), q.Quality()) 243 | } 244 | case "webp": 245 | useLossless := (q.Quality() == 100) 246 | err = imageprocessor.EncodeWebP(w, img.GetImg(), q.Quality(), useLossless) 247 | case "avif": 248 | err = imageprocessor.EncodeAVIF(w, img.GetImg(), q.Quality()) 249 | default: 250 | if q.UseWebP() { 251 | err = imageprocessor.EncodeWebP(w, img.GetImg(), q.Quality(), false) 252 | } else if q.UseAVIF() { 253 | err = imageprocessor.EncodeAVIF(w, img.GetImg(), q.Quality()) 254 | } else { 255 | err = imageprocessor.EncodeJpeg(w, img.GetImg(), q.Quality()) 256 | } 257 | } 258 | return 259 | } 260 | 261 | func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { 262 | w.WriteHeader(200) 263 | fmt.Fprintf(w, "%s", "") 264 | } 265 | 266 | func MakeMetricsHandler(conf *configure.Conf, logger *log.Logger) http.Handler { 267 | return promhttp.InstrumentMetricHandler( 268 | prometheus.DefaultRegisterer, 269 | promhttp.HandlerFor( 270 | prometheus.DefaultGatherer, 271 | promhttp.HandlerOpts{ 272 | DisableCompression: true, 273 | ErrorLog: logger, 274 | Timeout: conf.BackendRequestTimeout(), 275 | }, 276 | ), 277 | ) 278 | } 279 | 280 | func Prepare(conf *configure.Conf) error { 281 | content.SetUpProviders(conf) 282 | if err := content.SetupNoContentImage(conf); err != nil { 283 | return err 284 | } 285 | if err := imageprocessor.SetUpColorConverter(); err != nil { 286 | return err 287 | } 288 | return nil 289 | } 290 | -------------------------------------------------------------------------------- /lib/handler/handler_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/url" 7 | "testing" 8 | 9 | configure "github.com/livesense-inc/fanlin/lib/conf" 10 | helper "github.com/livesense-inc/fanlin/lib/test" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func TestMakeMetricsHandler(t *testing.T) { 15 | r := &http.Request{ 16 | RemoteAddr: "127.0.0.1", 17 | Proto: "HTTP/1.1", 18 | Method: http.MethodGet, 19 | Host: "127.0.0.1:8080", 20 | URL: &url.URL{ 21 | Path: "/metrics", 22 | }, 23 | } 24 | w := helper.NewNullResponseWriter() 25 | h := MakeMetricsHandler(&configure.Conf{}, helper.NullLogger()) 26 | h.ServeHTTP(w, r) 27 | if w.StatusCode() != http.StatusOK { 28 | t.Errorf("want=%d, got=%d", http.StatusOK, w.StatusCode()) 29 | } 30 | } 31 | 32 | func BenchmarkMainHandler(b *testing.B) { 33 | c := configure.NewConfigure("../test/test_conf9.json") 34 | if c == nil { 35 | b.Fatal("Failed to build config") 36 | } 37 | Prepare(c) 38 | 39 | w := helper.NewNullResponseWriter() 40 | r := &http.Request{ 41 | RemoteAddr: "127.0.0.1", 42 | Proto: "HTTP/1.1", 43 | Method: http.MethodGet, 44 | Host: "127.0.0.1:3000", 45 | URL: &url.URL{ 46 | Path: "/Lenna.jpg", 47 | RawQuery: func() string { 48 | q := url.Values{} 49 | q.Set("w", "300") 50 | q.Set("h", "200") 51 | q.Set("rgb", "32,32,32") 52 | return q.Encode() 53 | }(), 54 | }, 55 | Header: http.Header{ 56 | "Content-Type": []string{"application/x-www-form-urlencoded"}, 57 | }, 58 | } 59 | 60 | stdOut := logrus.New() 61 | stdOut.Out = io.Discard 62 | stdErr := logrus.New() 63 | stdErr.Out = io.Discard 64 | l := map[string]*logrus.Logger{"access": stdOut, "err": stdErr} 65 | 66 | b.ResetTimer() 67 | for i := 0; i < b.N; i++ { 68 | MainHandler(w, r, c, l) 69 | if w.StatusCode() != 200 { 70 | b.Fatalf("want: 200, got: %d", w.StatusCode()) 71 | } 72 | if w.BodySize() == 0 { 73 | b.Fatal("empty response body") 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/image/default.icc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livesense-inc/fanlin/4c91fb7b2c7b35d19837d08f97fc4801ce568146/lib/image/default.icc -------------------------------------------------------------------------------- /lib/image/image.go: -------------------------------------------------------------------------------- 1 | package imageprocessor 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "errors" 7 | "image" 8 | "image/color" 9 | "image/draw" 10 | "image/gif" 11 | "image/jpeg" 12 | "image/png" 13 | "io" 14 | "math" 15 | "runtime" 16 | "sync" 17 | 18 | "github.com/Kagami/go-avif" 19 | "github.com/chai2010/webp" 20 | "github.com/disintegration/gift" 21 | "github.com/ieee0824/libcmyk" 22 | imgproxyerr "github.com/livesense-inc/fanlin/lib/error" 23 | "github.com/livesense-inc/go-lcms/lcms" 24 | "github.com/rwcarlsen/goexif/exif" 25 | _ "golang.org/x/image/bmp" 26 | ) 27 | 28 | var affines = map[int]gift.Filter{ 29 | 2: gift.FlipHorizontal(), 30 | 3: gift.Rotate180(), 31 | 4: gift.FlipVertical(), 32 | 5: gift.Transpose(), 33 | 6: gift.Rotate270(), 34 | 7: gift.Transverse(), 35 | 8: gift.Rotate90(), 36 | } 37 | 38 | var mlConverterCache = &sync.Map{} 39 | 40 | //go:embed default.icc 41 | var defaultICCProfile []byte 42 | 43 | var cmykToRGBTransformer *lcms.Transform 44 | 45 | type Image struct { 46 | img image.Image 47 | format string 48 | orientation int 49 | fillColor color.Color 50 | outerBounds image.Rectangle 51 | filter *gift.GIFT 52 | } 53 | 54 | func (i *Image) ConvertColor(networkPath string) error { 55 | sc := i.img.At(0, 0) 56 | _, ok := sc.(color.CMYK) 57 | if !ok { 58 | return nil 59 | } 60 | 61 | rect := i.img.Bounds() 62 | ret := image.NewRGBA(rect) 63 | 64 | var converter *libcmyk.Converter 65 | iface, ok := mlConverterCache.Load(networkPath) 66 | if !ok { 67 | cr, err := libcmyk.New(networkPath) 68 | if err != nil { 69 | return err 70 | } 71 | mlConverterCache.Store(networkPath, cr) 72 | converter = cr 73 | } else { 74 | converter = iface.(*libcmyk.Converter) 75 | } 76 | 77 | w := rect.Max.X 78 | h := rect.Max.Y 79 | 80 | for y := 0; y < h; y++ { 81 | for x := 0; x < w; x++ { 82 | cmyk := i.img.At(x, y).(color.CMYK) 83 | rgba, err := converter.CMYK2RGBA(&cmyk) 84 | if err != nil { 85 | return err 86 | } 87 | ret.Set(x, y, rgba) 88 | } 89 | } 90 | i.img = ret 91 | return nil 92 | } 93 | 94 | func (i *Image) ConvertColorWithICCProfile() { 95 | switch src := i.img.(type) { 96 | case *image.CMYK: 97 | if cmykToRGBTransformer == nil { 98 | return 99 | } 100 | dst := image.NewRGBA(i.img.Bounds()) 101 | cmykToRGBTransformer.DoTransform(src.Pix, dst.Pix, len(src.Pix)/4) 102 | for i := range dst.Pix { 103 | if (i+1)%4 == 0 { 104 | dst.Pix[i] = 255 // Alpha 105 | } 106 | } 107 | i.img = dst 108 | } 109 | } 110 | 111 | func EncodeJpeg(buf io.Writer, img *image.Image, q int) error { 112 | if *img == nil { 113 | return imgproxyerr.New(imgproxyerr.WARNING, errors.New("img is nil")) 114 | } 115 | 116 | if !(0 <= q && q <= 100) { 117 | q = jpeg.DefaultQuality 118 | } 119 | 120 | err := jpeg.Encode(buf, *img, &jpeg.Options{Quality: q}) 121 | return imgproxyerr.New(imgproxyerr.WARNING, err) 122 | } 123 | 124 | func EncodePNG(buf io.Writer, img *image.Image, q int) error { 125 | if *img == nil { 126 | return imgproxyerr.New(imgproxyerr.WARNING, errors.New("img is nil")) 127 | } 128 | 129 | // Split quality from 0 to 100 in 4 CompressionLevel 130 | // https://golang.org/pkg/image/png/#CompressionLevel 131 | var e png.Encoder 132 | switch { 133 | case 0 <= q && q <= 25: 134 | e.CompressionLevel = png.BestCompression 135 | case 25 < q && q <= 50: 136 | e.CompressionLevel = png.DefaultCompression 137 | case 50 < q && q <= 75: 138 | e.CompressionLevel = png.BestSpeed 139 | case 75 < q && q <= 100: 140 | e.CompressionLevel = png.NoCompression 141 | default: 142 | e.CompressionLevel = png.DefaultCompression 143 | } 144 | 145 | err := e.Encode(buf, *img) 146 | return imgproxyerr.New(imgproxyerr.WARNING, err) 147 | } 148 | 149 | func EncodeGIF(buf io.Writer, img *image.Image, q int) error { 150 | if *img == nil { 151 | return imgproxyerr.New(imgproxyerr.WARNING, errors.New("img is nil")) 152 | } 153 | 154 | // GIF is not support quality 155 | 156 | err := gif.Encode(buf, *img, &gif.Options{}) 157 | return imgproxyerr.New(imgproxyerr.WARNING, err) 158 | } 159 | 160 | func EncodeWebP(buf io.Writer, img *image.Image, q int, lossless bool) error { 161 | if *img == nil { 162 | return imgproxyerr.New(imgproxyerr.WARNING, errors.New("img is nil")) 163 | } 164 | if !(0 <= q && q < 100) { 165 | // webp.DefaulQuality = 90 is large, adjust to JPEG 166 | q = jpeg.DefaultQuality 167 | } 168 | 169 | var option webp.Options 170 | if lossless { 171 | option.Lossless = true 172 | } else { 173 | option.Lossless = false 174 | option.Quality = float32(q) 175 | } 176 | 177 | err := webp.Encode(buf, *img, &option) 178 | return imgproxyerr.New(imgproxyerr.WARNING, err) 179 | } 180 | 181 | func EncodeAVIF(buf io.Writer, img *image.Image, q int) error { 182 | if *img == nil { 183 | return imgproxyerr.New(imgproxyerr.WARNING, errors.New("img is nil")) 184 | } 185 | 186 | // https://pkg.go.dev/github.com/Kagami/go-avif 187 | if q < 0 { 188 | // not specified 189 | q = avif.MinQuality + 20 190 | } else if q < avif.MinQuality { 191 | q = avif.MinQuality 192 | } else if q > avif.MaxQuality { 193 | q = avif.MaxQuality 194 | } 195 | q = avif.MaxQuality - q // lower is better, invert 196 | 197 | opts := avif.Options{ 198 | Threads: 0, // all available cores 199 | Speed: avif.MaxSpeed, // bigger is faster, but lower compress ratio 200 | Quality: q, // lower is better, zero is lossless 201 | SubsampleRatio: nil, // 4:2:0 202 | } 203 | if err := avif.Encode(buf, *img, &opts); err != nil { 204 | return imgproxyerr.New(imgproxyerr.WARNING, err) 205 | } 206 | 207 | return nil 208 | } 209 | 210 | func DecodeImage(r io.Reader) (*Image, error) { 211 | img, format, orientation, err := decode(r) 212 | if err != nil { 213 | return nil, imgproxyerr.New(imgproxyerr.WARNING, err) 214 | } 215 | return &Image{ 216 | img: img, 217 | format: format, 218 | orientation: orientation, 219 | outerBounds: img.Bounds(), 220 | filter: gift.New(), 221 | }, nil 222 | } 223 | 224 | func (i *Image) Process() { 225 | bounds := i.filter.Bounds(i.img.Bounds()) 226 | dest := image.NewRGBA(bounds) 227 | i.filter.Draw(dest, i.img) 228 | 229 | if dest.Bounds() == i.outerBounds || i.fillColor == nil { 230 | i.img = dest 231 | return 232 | } 233 | 234 | bg := image.NewRGBA(i.outerBounds) 235 | draw.Draw(bg, bg.Bounds(), &image.Uniform{i.fillColor}, image.Point{}, draw.Src) 236 | centerH := math.Abs(float64(i.outerBounds.Max.Y-bounds.Max.Y)) / 2.0 237 | centerW := math.Abs(float64(i.outerBounds.Max.X-bounds.Max.X)) / 2.0 238 | center := bounds.Min.Sub(image.Pt(int(centerW), int(centerH))) 239 | draw.Draw(bg, bg.Bounds(), dest, center, draw.Over) 240 | i.img = bg 241 | } 242 | 243 | func (i *Image) ResizeAndFill(w, h uint, c color.Color) { 244 | if w == 0 || h == 0 { 245 | return 246 | } 247 | innerW := w 248 | innerH := h 249 | r := i.img.Bounds() 250 | if int(innerW)*r.Max.Y < int(innerH)*r.Max.X { 251 | innerH = 0 252 | } else { 253 | innerW = 0 254 | } 255 | i.filter.Add(gift.Resize(int(innerW), int(innerH), gift.LanczosResampling)) 256 | i.outerBounds = image.Rect(0, 0, int(w), int(h)) 257 | i.fillColor = c 258 | } 259 | 260 | func (i *Image) Crop(w, h uint) { 261 | if w == 0 || h == 0 { 262 | return 263 | } 264 | i.filter.Add(gift.ResizeToFill(int(w), int(h), gift.LanczosResampling, gift.CenterAnchor)) 265 | i.outerBounds = image.Rect(0, 0, int(w), int(h)) 266 | } 267 | 268 | func (i *Image) Invert() { 269 | i.filter.Add(gift.Invert()) 270 | } 271 | 272 | func (i *Image) Grayscale() { 273 | i.filter.Add(gift.Grayscale()) 274 | } 275 | 276 | func (i *Image) ApplyOrientation() { 277 | if affine, ok := affines[i.orientation]; ok { 278 | i.filter.Add(affine) 279 | } 280 | } 281 | 282 | func (i *Image) GetImg() *image.Image { 283 | if i.img == nil { 284 | return nil 285 | } 286 | return &i.img 287 | } 288 | 289 | func (i *Image) GetFormat() string { 290 | return i.format 291 | } 292 | 293 | func Set404Image(buf io.Writer, data io.Reader, w uint, h uint, c color.Color) error { 294 | img, err := DecodeImage(data) 295 | if err != nil { 296 | return imgproxyerr.New(imgproxyerr.ERROR, err) 297 | } 298 | img.ResizeAndFill(w, h, c) 299 | img.Process() 300 | return EncodeJpeg(buf, img.GetImg(), jpeg.DefaultQuality) 301 | } 302 | 303 | func readOrientation(r io.Reader) (o int, err error) { 304 | e, err := exif.Decode(r) 305 | if err != nil { 306 | return 307 | } 308 | tag, err := e.Get(exif.Orientation) 309 | if err != nil { 310 | return 311 | } 312 | o, err = tag.Int(0) 313 | if err != nil { 314 | return 315 | } 316 | return 317 | } 318 | 319 | func decode(r io.Reader) (d image.Image, format string, o int, err error) { 320 | var buf bytes.Buffer 321 | tee := io.TeeReader(r, &buf) 322 | d, format, err = image.Decode(tee) 323 | if err != nil { 324 | return 325 | } 326 | 327 | raw := buf.Bytes() 328 | o, _ = readOrientation(bytes.NewReader(raw)) 329 | return 330 | } 331 | 332 | func SetUpColorConverter() error { 333 | srcProf, err := lcms.OpenProfileFromMem(defaultICCProfile) 334 | if err != nil { 335 | return err 336 | } 337 | defer srcProf.CloseProfile() 338 | 339 | dstProf, err := lcms.CreateSRGBProfile() 340 | if err != nil { 341 | return err 342 | } 343 | defer dstProf.CloseProfile() 344 | 345 | t, err := lcms.CreateTransform(srcProf, lcms.TYPE_CMYK_8, dstProf, lcms.TYPE_RGBA_8) 346 | if err != nil { 347 | return err 348 | } 349 | cmykToRGBTransformer = t 350 | runtime.SetFinalizer(cmykToRGBTransformer, func(t *lcms.Transform) { 351 | t.DeleteTransform() 352 | }) 353 | 354 | return nil 355 | } 356 | -------------------------------------------------------------------------------- /lib/image/image_test.go: -------------------------------------------------------------------------------- 1 | package imageprocessor 2 | 3 | import ( 4 | "bytes" 5 | "image/color" 6 | "log" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | var ( 12 | jpgPath = "../test/img/Lenna.jpg" 13 | bmpPath = "../test/img/Lenna.bmp" 14 | pngPath = "../test/img/Lenna.png" 15 | gifPath = "../test/img/Lenna.gif" 16 | webpLosslessPath = "../test/img/Lenna_lossless.webp" 17 | webpLossyPath = "../test/img/Lenna_lossy.webp" 18 | confPath = "../test/test_conf.json" 19 | ) 20 | 21 | var ( 22 | jpegBin, _ = os.Open(jpgPath) 23 | bmpBin, _ = os.Open(bmpPath) 24 | pngBin, _ = os.Open(pngPath) 25 | gifBin, _ = os.Open(gifPath) 26 | webpLosslessBin, _ = os.Open(webpLosslessPath) 27 | webpLossyBin, _ = os.Open(webpLossyPath) 28 | confBin, _ = os.Open(confPath) 29 | ) 30 | 31 | func TestEncodeJpeg(t *testing.T) { 32 | img, err := DecodeImage(jpegBin) 33 | jpegBin.Seek(0, 0) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | if format := img.GetFormat(); format != "jpeg" { 38 | t.Fatalf("format is %v, expected jpeg", format) 39 | } 40 | 41 | var b bytes.Buffer 42 | if err := EncodeJpeg(&b, img.GetImg(), -1); err != nil { 43 | t.Fatal(err) 44 | } 45 | b.Reset() 46 | 47 | defer confBin.Seek(0, 0) 48 | if _, err := DecodeImage(confBin); err == nil { 49 | t.Error("no error") 50 | } 51 | b.Reset() 52 | } 53 | 54 | func BenchmarkEncodeJpeg(b *testing.B) { 55 | for i := 0; i < b.N; i++ { 56 | img, err := DecodeImage(jpegBin) 57 | jpegBin.Seek(0, 0) 58 | if err != nil { 59 | b.Fatal(err) 60 | } 61 | if format := img.GetFormat(); format != "jpeg" { 62 | log.Fatalf("format is %v, expected jpeg", format) 63 | } 64 | 65 | var buf bytes.Buffer 66 | if err := EncodeJpeg(&buf, img.GetImg(), -1); err != nil { 67 | log.Fatal(err) 68 | } 69 | buf.Reset() 70 | } 71 | } 72 | 73 | func TestEncodePNG(t *testing.T) { 74 | img, err := DecodeImage(pngBin) 75 | pngBin.Seek(0, 0) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | if format := img.GetFormat(); format != "png" { 80 | t.Fatalf("format is %v, expected png", format) 81 | } 82 | 83 | var b bytes.Buffer 84 | if err := EncodePNG(&b, img.GetImg(), -1); err != nil { 85 | t.Fatal(err) 86 | } 87 | b.Reset() 88 | 89 | defer confBin.Seek(0, 0) 90 | if _, err := DecodeImage(confBin); err == nil { 91 | t.Error("no error") 92 | } 93 | b.Reset() 94 | } 95 | 96 | func TestEncodeGIF(t *testing.T) { 97 | img, err := DecodeImage(gifBin) 98 | gifBin.Seek(0, 0) 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | if format := img.GetFormat(); format != "gif" { 103 | t.Fatalf("format is %v, expected png", format) 104 | } 105 | 106 | var b bytes.Buffer 107 | if err := EncodeGIF(&b, img.GetImg(), -1); err != nil { 108 | t.Fatal(err) 109 | } 110 | b.Reset() 111 | 112 | defer confBin.Seek(0, 0) 113 | if _, err = DecodeImage(confBin); err == nil { 114 | t.Error("no error") 115 | } 116 | b.Reset() 117 | } 118 | 119 | func TestEncodeWebP(t *testing.T) { 120 | // Lossless 121 | img, err := DecodeImage(webpLosslessBin) 122 | webpLosslessBin.Seek(0, 0) 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | if format := img.GetFormat(); format != "webp" { 127 | t.Fatalf("format is %v, expected webp", format) 128 | } 129 | 130 | var b bytes.Buffer 131 | if err := EncodeWebP(&b, img.GetImg(), -1, true); err != nil { 132 | t.Fatal(err) 133 | } 134 | b.Reset() 135 | 136 | // Lossy 137 | img, err = DecodeImage(webpLossyBin) 138 | webpLossyBin.Seek(0, 0) 139 | if err != nil { 140 | t.Fatal(err) 141 | } 142 | if format := img.GetFormat(); format != "webp" { 143 | t.Fatalf("format is %v, expected webp", format) 144 | } 145 | 146 | if err := EncodeWebP(&b, img.GetImg(), -1, false); err != nil { 147 | t.Fatal(err) 148 | } 149 | b.Reset() 150 | 151 | // error 152 | defer confBin.Seek(0, 0) 153 | if _, err := DecodeImage(confBin); err == nil { 154 | t.Error("no error") 155 | } 156 | b.Reset() 157 | } 158 | 159 | func TestEncodeAVIF(t *testing.T) { 160 | var b bytes.Buffer 161 | 162 | img, err := DecodeImage(pngBin) 163 | pngBin.Seek(0, 0) 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | if err := EncodeAVIF(&b, img.GetImg(), 50); err != nil { 168 | t.Fatal(err) 169 | } 170 | b.Reset() 171 | 172 | defer confBin.Seek(0, 0) 173 | if _, err := DecodeImage(confBin); err == nil { 174 | t.Error("no error") 175 | } 176 | b.Reset() 177 | } 178 | 179 | func TestDecodeImage(t *testing.T) { 180 | if _, err := DecodeImage(jpegBin); err != nil { 181 | t.Error(err) 182 | } 183 | defer jpegBin.Seek(0, 0) 184 | 185 | if _, err := DecodeImage(bmpBin); err != nil { 186 | t.Error(err) 187 | } 188 | defer bmpBin.Seek(0, 0) 189 | 190 | if _, err := DecodeImage(pngBin); err != nil { 191 | t.Error(err) 192 | } 193 | defer pngBin.Seek(0, 0) 194 | 195 | if _, err := DecodeImage(gifBin); err != nil { 196 | t.Error(err) 197 | } 198 | defer gifBin.Seek(0, 0) 199 | 200 | if _, err := DecodeImage(webpLosslessBin); err != nil { 201 | t.Error(err) 202 | } 203 | defer webpLosslessBin.Seek(0, 0) 204 | 205 | if _, err := DecodeImage(webpLossyBin); err != nil { 206 | t.Error(err) 207 | } 208 | defer webpLossyBin.Seek(0, 0) 209 | 210 | if _, err := DecodeImage(confBin); err == nil { 211 | t.Error("err is nil") 212 | } 213 | defer confBin.Seek(0, 0) 214 | } 215 | 216 | func TestImageProcess(t *testing.T) { 217 | img, err := DecodeImage(jpegBin) 218 | jpegBin.Seek(0, 0) 219 | if err != nil { 220 | t.Fatal(err) 221 | } 222 | 223 | img.ApplyOrientation() 224 | img.ResizeAndFill(1618, 1000, color.RGBA{uint8(32), uint8(32), uint8(32), 0xff}) 225 | img.Process() 226 | 227 | innerP := img.GetImg() 228 | inner := *innerP 229 | if inner.Bounds().Max.X != 1618 { 230 | t.Errorf("want=%d, got=%d", 1618, inner.Bounds().Max.X) 231 | } 232 | if inner.Bounds().Max.Y != 1000 { 233 | t.Errorf("want=%d, got=%d", 1000, inner.Bounds().Max.Y) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /lib/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/ieee0824/logrus-formatter" 8 | ) 9 | 10 | func NewLogger(path string) *logrus.Logger { 11 | logger := logrus.New() 12 | logger.Formatter = new(formatter.SysLogFormatter) 13 | logFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 14 | if err != nil { 15 | logger.Fatalln("Can not create log file: ", path) 16 | } 17 | logger.Out = logFile 18 | return logger 19 | } 20 | -------------------------------------------------------------------------------- /lib/query/query.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "image/color" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type Bounds struct { 11 | W uint 12 | H uint 13 | } 14 | 15 | type Query struct { 16 | b Bounds 17 | fillColor color.Color 18 | crop bool 19 | quality int 20 | grayscale bool 21 | inverse bool 22 | useWebp bool 23 | useAvif bool 24 | } 25 | 26 | func NewQueryFromGet(r *http.Request) *Query { 27 | params := r.URL.Query() 28 | q := Query{} 29 | w, _ := strconv.Atoi(params.Get("w")) 30 | h, _ := strconv.Atoi(params.Get("h")) 31 | rgb := strings.Split(strings.Trim(params.Get("rgb"), "\""), ",") 32 | 33 | var c color.Color 34 | if len(rgb) == 3 { 35 | c = func() color.Color { 36 | r, _ := strconv.Atoi(rgb[0]) 37 | g, _ := strconv.Atoi(rgb[1]) 38 | b, _ := strconv.Atoi(rgb[2]) 39 | return color.RGBA{uint8(r), uint8(g), uint8(b), 0xff} 40 | }() 41 | } else { 42 | c = nil 43 | } 44 | 45 | quality, err := strconv.Atoi(params.Get("quality")) 46 | if err != nil { 47 | quality = -1 48 | } 49 | 50 | crop, err := strconv.ParseBool(params.Get("crop")) 51 | if err != nil { 52 | crop = false 53 | } 54 | 55 | grayscale, err := strconv.ParseBool(params.Get("grayscale")) 56 | if err != nil { 57 | grayscale = false 58 | } 59 | 60 | inverse, err := strconv.ParseBool(params.Get("inverse")) 61 | if err != nil { 62 | inverse = false 63 | } 64 | 65 | webp, err := strconv.ParseBool(params.Get("webp")) 66 | if err != nil { 67 | webp = false 68 | } 69 | 70 | avif, err := strconv.ParseBool(params.Get("avif")) 71 | if err != nil { 72 | avif = false 73 | } 74 | 75 | q.b.H = uint(h) 76 | q.b.W = uint(w) 77 | q.fillColor = c 78 | q.crop = crop 79 | q.quality = quality 80 | q.grayscale = grayscale 81 | q.inverse = inverse 82 | q.useWebp = webp 83 | q.useAvif = avif 84 | return &q 85 | } 86 | 87 | func (q *Query) Bounds() *Bounds { 88 | return &q.b 89 | } 90 | 91 | func (q *Query) FillColor() *color.Color { 92 | return &q.fillColor 93 | } 94 | 95 | func (q *Query) Crop() bool { 96 | return q.crop 97 | } 98 | 99 | func (q *Query) Quality() int { 100 | return q.quality 101 | } 102 | 103 | func (q *Query) Grayscale() bool { 104 | return q.grayscale 105 | } 106 | 107 | func (q *Query) Inverse() bool { 108 | return q.inverse 109 | } 110 | 111 | func (q *Query) UseWebP() bool { 112 | return q.useWebp 113 | } 114 | 115 | func (q *Query) UseAVIF() bool { 116 | return q.useAvif 117 | } 118 | -------------------------------------------------------------------------------- /lib/query/query_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | var getRquest, _ = http.NewRequest("GET", "https://example.com/?w=300&h=200&rgb=255,255,255&crop=true&quality=75", nil) 9 | 10 | func TestNewGet(t *testing.T) { 11 | q := NewQueryFromGet(getRquest) 12 | 13 | if q == nil { 14 | t.Fatalf("query is not allocated.") 15 | } 16 | } 17 | 18 | func TestBounds(t *testing.T) { 19 | q := NewQueryFromGet(getRquest) 20 | b := Bounds{300, 200} 21 | 22 | if *q.Bounds() != b { 23 | t.Fatalf("Bounds: %v.", *q.Bounds()) 24 | } 25 | } 26 | 27 | func TestGetFillColor(t *testing.T) { 28 | q := NewQueryFromGet(getRquest) 29 | 30 | if q.FillColor() == nil { 31 | t.Fatalf("fillcolor is nil.") 32 | } 33 | } 34 | 35 | func TestCrop(t *testing.T) { 36 | q := NewQueryFromGet(getRquest) 37 | 38 | if !q.Crop() { 39 | t.Fatalf("crop is false.") 40 | } 41 | } 42 | 43 | func TestQuality(t *testing.T) { 44 | q := NewQueryFromGet(getRquest) 45 | 46 | if q.Quality() != 75 { 47 | t.Fatalf("quality is %d.", q.Quality()) 48 | } 49 | } 50 | 51 | func TestGrayscale(t *testing.T) { 52 | q := NewQueryFromGet(getRquest) 53 | 54 | if q.Grayscale() { 55 | t.Fatalf("grayscale is true.") 56 | } 57 | } 58 | 59 | func TestInverse(t *testing.T) { 60 | q := NewQueryFromGet(getRquest) 61 | 62 | if q.Inverse() { 63 | t.Fatalf("inverse is true.") 64 | } 65 | } 66 | 67 | func TestWebP(t *testing.T) { 68 | q := NewQueryFromGet(getRquest) 69 | 70 | if q.UseWebP() { 71 | t.Fatalf("webp is true.") 72 | } 73 | } 74 | 75 | func TestAVIF(t *testing.T) { 76 | q := NewQueryFromGet(getRquest) 77 | 78 | if q.UseAVIF() { 79 | t.Fatalf("avif is true.") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/test/helper.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // for test 11 | const ( 12 | Timeout = 5 * time.Second 13 | ) 14 | 15 | // NullResponseWriter is a struct 16 | type NullResponseWriter struct { 17 | h http.Header 18 | b io.Writer 19 | bSize int 20 | sc int 21 | } 22 | 23 | // NewNullResponseWriter returns a instance of NullResponseWriter 24 | func NewNullResponseWriter() *NullResponseWriter { 25 | return &NullResponseWriter{h: http.Header{}, b: io.Discard, sc: http.StatusOK} 26 | } 27 | 28 | // Header returns headers 29 | func (w *NullResponseWriter) Header() http.Header { 30 | return w.h 31 | } 32 | 33 | // Write writes bytes to response writer 34 | func (w *NullResponseWriter) Write(p []byte) (int, error) { 35 | w.bSize += len(p) 36 | return w.b.Write(p) 37 | } 38 | 39 | // WriteHeader updates the status code 40 | func (w *NullResponseWriter) WriteHeader(statusCode int) { 41 | w.sc = statusCode 42 | } 43 | 44 | // StatusCode returns a status code 45 | func (w *NullResponseWriter) StatusCode() int { 46 | return w.sc 47 | } 48 | 49 | // BodySize returns a size of the body 50 | func (w *NullResponseWriter) BodySize() int { 51 | return w.bSize 52 | } 53 | 54 | // NullLogger returns a instance of log.Logger 55 | func NullLogger() *log.Logger { 56 | return log.New(io.Discard, "", 0) 57 | } 58 | -------------------------------------------------------------------------------- /lib/test/img/Lenna.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livesense-inc/fanlin/4c91fb7b2c7b35d19837d08f97fc4801ce568146/lib/test/img/Lenna.bmp -------------------------------------------------------------------------------- /lib/test/img/Lenna.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livesense-inc/fanlin/4c91fb7b2c7b35d19837d08f97fc4801ce568146/lib/test/img/Lenna.gif -------------------------------------------------------------------------------- /lib/test/img/Lenna.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livesense-inc/fanlin/4c91fb7b2c7b35d19837d08f97fc4801ce568146/lib/test/img/Lenna.jpg -------------------------------------------------------------------------------- /lib/test/img/Lenna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livesense-inc/fanlin/4c91fb7b2c7b35d19837d08f97fc4801ce568146/lib/test/img/Lenna.png -------------------------------------------------------------------------------- /lib/test/img/Lenna_lossless.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livesense-inc/fanlin/4c91fb7b2c7b35d19837d08f97fc4801ce568146/lib/test/img/Lenna_lossless.webp -------------------------------------------------------------------------------- /lib/test/img/Lenna_lossy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livesense-inc/fanlin/4c91fb7b2c7b35d19837d08f97fc4801ce568146/lib/test/img/Lenna_lossy.webp -------------------------------------------------------------------------------- /lib/test/test_conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8080, 3 | "max_width": 5000, 4 | "max_height": 5000, 5 | "use_server_timing": true, 6 | "enable_metrics_endpoint": true, 7 | "max_clients": 50, 8 | "server_timeout": "30s", 9 | "server_idle_timeout": "65s" 10 | } 11 | -------------------------------------------------------------------------------- /lib/test/test_conf3.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8080, 3 | "max_width": 5000, 4 | "max_height": 5000 5 | } 6 | -------------------------------------------------------------------------------- /lib/test/test_conf4.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8080, 3 | "max_width": 5000, 4 | "max_height": 5000 5 | } 6 | -------------------------------------------------------------------------------- /lib/test/test_conf8.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8080, 3 | "max_width": 1000, 4 | "max_height": 1000, 5 | "404_img_path": "../../img/404.png", 6 | "access_log_path": "/dev/null", 7 | "error_log_path": "/dev/null", 8 | "providers": [ 9 | { 10 | "/" : { 11 | "type" : "local", 12 | "src" : "/tmp", 13 | "priority": 0 14 | } 15 | }, 16 | { 17 | "/foo" : { 18 | "type" : "local", 19 | "src" : "/tmp/foo", 20 | "priority": 9 21 | } 22 | }, 23 | { 24 | "/foobar" : { 25 | "type" : "local", 26 | "src" : "/tmp/foobar", 27 | "priority": 8 28 | } 29 | }, 30 | { 31 | "/foobarbaz" : { 32 | "type" : "local", 33 | "src" : "/tmp/foobarbaz", 34 | "priority": 7 35 | } 36 | }, 37 | { 38 | "/foobarbazgqu" : { 39 | "type" : "local", 40 | "src" : "/tmp/foobarbazgqu", 41 | "priority": 6 42 | } 43 | }, 44 | { 45 | "/foobarbazgquu" : { 46 | "type" : "local", 47 | "src" : "/tmp/foobarbazgquu", 48 | "priority": 5 49 | } 50 | }, 51 | { 52 | "/foobarbazgquuu" : { 53 | "type" : "local", 54 | "src" : "/tmp/foobarbazgquuu", 55 | "priority": 4 56 | } 57 | }, 58 | { 59 | "/foobarbazgquuuu" : { 60 | "type" : "local", 61 | "src" : "/tmp/foobarbazgquuuu", 62 | "priority": 3 63 | } 64 | }, 65 | { 66 | "/foobarbazgquuuuu" : { 67 | "type" : "local", 68 | "src" : "/tmp/foobarbazgquuuuu", 69 | "priority": 2 70 | } 71 | }, 72 | { 73 | "/foobarbazgquuuuuu" : { 74 | "type" : "local", 75 | "src" : "/tmp/foobarbazgquuuuuu", 76 | "priority": 1 77 | } 78 | } 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /lib/test/test_conf9.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000, 3 | "max_width": 3000, 4 | "max_height": 3000, 5 | "404_img_path": "../../img/404.png", 6 | "access_log_path": "/dev/null", 7 | "error_log_path": "/dev/null", 8 | "max_clients": 50, 9 | "providers": [ 10 | { 11 | "/" : { 12 | "type" : "local", 13 | "src" : "../test/img" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /network.json: -------------------------------------------------------------------------------- 1 | {"NInputs":5,"NHiddens":21,"NOutputs":3,"Regression":false,"InputActivations":[0.023529411764705882,0.0196078431372549,0.03529411764705882,0,1],"HiddenActivations":[0.3258985590506999,0.0000013037800760412309,5.0031645480646566e-8,0.01940216295917526,0.06276646346773744,0.0000172707007717071,0.09882857083442685,0.0000029847426095247393,0.11560777617091327,0.03885652246142518,0.09798970949718173,0.30992013447159544,0.00040635934889513567,2.2998975142149664e-11,0.3086383584747604,0.04122722994876256,0.21867908593648652,0.021083992487061727,0.1320324328616566,0.9367362673067128,1],"OutputActivations":[0.9810362976857497,0.9779216262938949,0.9732306537268766],"Contexts":null,"InputWeights":[[-2.6669584632267793,14.862216341363954,11.652051794125676,2.354161546295245,1.5408756131486396,-3.173698170203262,-17.193362295707765,-2.767432669154119,-5.760449249273182,-16.421571070497034,-0.2038270492103767,-1.0495521267698658,-3.2895467232811555,2.1778322209134915,-2.6159546029358083,0.2126431009843694,-2.8869688305141485,-1.4801284536075772,-4.984015296962759,-1.9260135101688522,-0.5936262467053544],[-1.5386246590088142,-5.145584532687111,0.2645223155032207,-1.8489344176839195,-9.291760864642256,0.8192595864788405,-4.056051366550478,11.11229947872069,-0.3800122959074225,-19.177539714736586,-2.697527926677022,-3.007977452280477,-1.0325601012606296,6.074151330999182,-2.574032407110326,0.45681440931177475,-0.40663032087269935,3.4053506406512466,-10.661681284395506,-2.4420124574812765,0.0821997100174705],[-2.558773363051976,-1.064142610694305,1.7793229195086009,-1.9106847180172304,-14.118270019472288,7.245157716840137,2.7243638045293794,1.3788614959639716,-0.057744034198893804,-13.805097591629751,1.8246935814628518,-0.5680028917998956,8.896326726196868,3.1179093655208274,-0.9005974260341754,-0.5784548749039229,1.1155806511530226,-1.746509883718193,-0.22227980221328583,-1.5130785062442074,0.6021100853053225],[-0.1248608215615445,-0.4704932913258649,3.247719698869701,-1.6182677332067075,0.10356661002367751,5.6005642329591385,0.4160787363800814,2.0361053601065997,-0.541688283819573,0.5084760843920532,0.049615909498476496,-1.5997886500215306,0.258591303741031,16.778608638650926,0.10045275477189514,5.183631608806212,-1.2987758476890823,-0.4558818651887738,-0.5851759877909123,-2.855539459940575,0.9719294586804934],[-0.5435655751909619,-13.761488878327745,-17.152762341194734,-3.874485051273309,-2.0592927008268,-11.163581926745929,-1.8223312601164638,-12.92343191943273,-1.8896400859100353,-1.9585911145372217,-2.2264770724263276,-0.696767974873868,-8.024206833038866,-24.775959437536205,-0.6626882391496868,-3.140100195279668,-1.236832534995303,-3.8082335292942053,-1.5489703829387504,2.8416938582486275,0.45836145346859625]],"OutputWeights":[[1.1472832597329463,0.955746562443108,2.7269033817487274],[-11.420672907261347,0.19342036037842197,-0.26755182931242016],[-9.88992167523026,-1.037838919422172,0.011279988821738331],[-3.0923489967680893,0.4003469365849377,2.5226328058262695],[-0.07074439057001362,-0.9640604855799134,9.56618885856852],[0.8636090110357605,0.15161100047029777,-6.5969587737947295],[9.593510397037646,0.3638937132169543,-0.11373130831213747],[-0.6712604650775144,-9.507199309786694,0.3646894886569875],[4.4572432824103965,0.012399063016573657,-0.16335410318290433],[7.339031466517691,11.055191128193897,9.006890796542843],[0.4658545169782954,2.6869515723811666,-2.3160988324697023],[1.8131156491952116,1.8079863364258681,2.4376629700739296],[-0.5993519698389552,-0.8663047845130812,-6.788982489829873],[-5.035259364947688,-9.807977303635274,-9.229216735683535],[1.5019617200237971,2.529574495493989,1.8283994925782432],[-1.6999774658457119,-1.7942819267772352,-1.8265533949685813],[1.9049164037318946,0.669605622494707,-1.1322705206108175],[-0.08633282881962492,-2.923300650357234,0.18958802714031828],[-0.7470403253795553,7.13104212114532,0.6930359460105399],[1.5627808165683903,1.5479542374929072,1.7154667352282433],[-0.8926382694823627,-0.9412821364878837,-0.7341046971744142]],"InputChanges":[[-4.350404347782128e-8,-2.1402431723360983e-11,-6.718473179961967e-13,-1.100770480617268e-7,-1.9930694100670019e-7,6.707383128150788e-11,0.0000012061416079013695,1.7588715632212244e-11,6.598515775680444e-7,-5.16813943487528e-8,-3.045852515876728e-8,5.343501417248383e-8,1.0623921041167414e-9,8.88331361302043e-17,-1.0112151175019936e-7,-1.3954239549300045e-8,4.6343873170511684e-7,4.028525536969195e-8,-7.584293662107406e-7,2.2761003021391998e-8,-0],[-3.625336956485107e-8,-1.7835359769467487e-11,-5.598727649968306e-13,-9.173087338477233e-8,-1.6608911750558348e-7,5.5894859401256566e-11,0.0000010051180065844746,1.4657263026843537e-11,5.498763146400369e-7,-4.3067828623960663e-8,-2.5382104298972735e-8,4.452917847706986e-8,8.853267534306178e-10,7.402761344183693e-17,-8.426792645849947e-8,-1.1628532957750037e-8,3.861989430875974e-7,3.357104614140996e-8,-6.320244718422838e-7,1.8967502517826665e-8,-0],[-6.525606521673192e-8,-3.2103647585041476e-11,-1.0077709769942951e-12,-1.651155720925902e-7,-2.9896041151005024e-7,1.0061074692226182e-10,0.0000018092124118520542,2.6383073448318363e-11,9.897773663520666e-7,-7.75220915231292e-8,-4.568778773815092e-8,8.015252125872575e-8,1.593588156175112e-9,1.3324970419530646e-16,-1.5168226762529903e-7,-2.0931359323950066e-8,6.951580975576752e-7,6.042788305453791e-8,-0.000001137644049316111,3.4141504532088e-8,-0],[-0,-0,-0,-0,-0,0,0,0,0,-0,-0,0,0,0,-0,-0,0,0,-0,0,-0],[-0.0000018489218478074044,-9.096033482428418e-10,-2.8553511014838362e-11,-0.000004678274542623389,-0.000008470544992784758,2.850637829464085e-9,0.0000512610183358082,7.475204143690203e-10,0.000028043692046641885,-0.000002196459259821994,-0.0000012944873192476094,0.000002270988102330563,4.515166442496151e-8,3.775408285533683e-15,-0.000004297664249383473,-5.930551808452519e-7,0.000019696146097467466,0.0000017121233532119077,-0.000032233248063956476,9.6734262840916e-7,-0]],"OutputChanges":[[0.000019871185593361714,-0.000010210155920325735,-0.000005787450719333635],[7.949607368442554e-11,-4.084644590319959e-11,-2.3153103103361135e-11],[3.0506060406745748e-12,-1.5674536972358694e-12,-8.884840837279287e-13],[0.0000011830183668116211,-6.078551239434286e-7,-3.4455218918976105e-7],[0.000003827092848275943,-0.000001966425934621364,-0.0000011146346126982214],[1.0530555930092877e-9,-5.410780220878083e-10,-3.067007411624134e-10],[0.0000060259268365530775,-0.0000030962245446610776,-0.000001755041461467273],[1.8199029328341999e-10,-9.350973356266533e-11,-5.3004379070704587e-11],[0.00000704901421785867,-0.000003621904385663492,-0.000002053014009358741],[0.000002369219341112486,-0.0000012173455261920041,-6.900313076720114e-7],[0.000005974778499573471,-0.0000030699436520000173,-0.0000017401445909079124],[0.000018896924641653967,-0.000009709563936154239,-0.000005503698790225626],[2.4777163983215606e-8,-1.273093174755622e-8,-7.216309004008926e-9],[1.4023287026429308e-15,-7.205405353526356e-16,-4.084259703942517e-16],[0.00001881877023435491,-0.000009669406861447502,-0.0000054809364452906995],[0.0000025137697454029342,-0.0000012916180027494494,-7.321313795253082e-7],[0.000013333635824252309,-0.000006851050739314532,-0.000003883399904844171],[0.0000012855654501199238,-6.60545573883274e-7,-3.7441886162708484e-7],[0.000008050483516648165,-0.000004136476485149891,-0.000002344690325622731],[0.00005711611697181145,-0.000029347240359959867,-0.000016634976846297584],[0.000060973530080169395,-0.000031329245364160526,-0.000017758442185788502]]} 2 | -------------------------------------------------------------------------------- /plugin/.gitignore: -------------------------------------------------------------------------------- 1 | *.go 2 | / 3 | !explanation.go 4 | -------------------------------------------------------------------------------- /plugin/explanation.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | /* Explanation about this directory. (Don't overwrite this file) */ 4 | // 5 | // - This is a directory where plugins are placed. 6 | // - You can add a GO file as a new plugins. 7 | // - File name should be same as the name of plugin. 8 | // - Note: This function is still in experimental state. 9 | --------------------------------------------------------------------------------