diff --git a/.dockerignore b/.dockerignore index 69b6f4f..bec6640 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,4 +2,7 @@ .github .env .env.example -.git \ No newline at end of file +.git +target/ +.vscode +testImages \ No newline at end of file diff --git a/.env.example b/.env.example index 967a8b5..68a24e5 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,6 @@ API_KEY=00000000-0000-0000-0000-000000000000 API_KEY_HEADER=key CONVERT_TO_RES=1024x720,800x600 +TARGET_FORMATS=jpg,webp,avif MAX_FILE_SIZE=10 -CACHE_TIME=30 -EXPVAR_ENABLED=0 -PPROF_ENABLED=0 \ No newline at end of file +CACHE_TIME=30 \ No newline at end of file diff --git a/.github/workflows/BuildAndPublishApp.yml b/.github/workflows/BuildAndPublishApp.yml index 11ee84b..a69fc4c 100644 --- a/.github/workflows/BuildAndPublishApp.yml +++ b/.github/workflows/BuildAndPublishApp.yml @@ -1,11 +1,12 @@ name: Publish new version on: - workflow_dispatch: - inputs: - version: - description: 'Server version (format: Major.Minor.HotFix, example: 1.2.12)' - required: true + workflow_dispatch: + inputs: + version: + description: 'Server version (format: Major.Minor.HotFix, example: 1.2.12)' + required: true + jobs: build: name: Build and publish @@ -37,7 +38,7 @@ jobs: password: ${{ secrets.DH_TOKEN }} - name: buildDocker - uses: docker/build-push-action@v6.15.0 + uses: docker/build-push-action@v6.16.0 with: context: . push: true @@ -55,7 +56,7 @@ jobs: uses: actions/checkout@v4.2.2 - name: Create release on github - uses: softprops/action-gh-release@v2.2.1 + uses: softprops/action-gh-release@v2.2.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/buildApp.yml b/.github/workflows/buildApp.yml index 815cc54..4338132 100644 --- a/.github/workflows/buildApp.yml +++ b/.github/workflows/buildApp.yml @@ -9,17 +9,22 @@ on: jobs: build_non_docker: name: Build non docker version - runs-on: ubuntu-22.04 + strategy: + matrix: + include: + - os: ubuntu-24.04 + - os: ubuntu-24.04-arm + + runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4.2.2 - - name: Set up Go - uses: actions/setup-go@v5.4.0 + - name: Set up rust + uses: actions-rust-lang/setup-rust-toolchain@v1.12.0 with: - go-version-file: go.mod - cache-dependency-path: go.sum + toolchain: stable - name: Update apt env: @@ -27,14 +32,11 @@ jobs: run: sudo apt-get update -qq -o Acquire::Retries=3 - - name: Prepare go modules - run: go env -w GO111MODULE=auto CGO_ENABLED=1 GOOS=linux && go get -d -v - - name: Start build script - run: go build -v -ldflags="-w -s -X 'easy-image-cdn.pcpl2lab.ovh/app/build.Version=latest' -X 'easy-image-cdn.pcpl2lab.ovh/app/build.Time=$(date)'" ./... + run: cargo build --bins --release - - name: Run image converter test - run: go test -v ./imageConverter + # - name: Run image converter test + # run: go test -v ./imageConverter build_docker: name: Build docker version @@ -52,7 +54,7 @@ jobs: uses: actions/checkout@v4.2.2 - name: buildDocker - uses: docker/build-push-action@v6.15.0 + uses: docker/build-push-action@v6.16.0 with: context: . push: false diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 34b9ceb..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,72 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ "main", "dev" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main", "dev" ] - schedule: - - cron: '34 11 * * 3' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-22.04 - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'go' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v4.1.7 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3.26.6 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3.26.6 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3.26.6 diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index b527c85..715ca9c 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -35,7 +35,7 @@ jobs: password: ${{ secrets.DH_TOKEN }} - name: buildDocker - uses: docker/build-push-action@v6.15.0 + uses: docker/build-push-action@v6.16.0 with: context: . push: true diff --git a/.gitignore b/.gitignore index c5e6fe4..6b9b7ea 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,11 @@ imageCdn .idea* easy-image-cdn.pcpl2lab.ovh -__debug_bin* \ No newline at end of file +__debug_bin* + +# Added by cargo + +/target +output/ + +.DS_Store \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ae1bd65 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "rust-lang.rust-analyzer", + "vadimcn.vscode-lldb", + "humao.rest-client" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index f4ab3f3..e7edad3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,14 +5,41 @@ "version": "0.2.0", "configurations": [ { - "name": "Launch Package", - "type": "go", + "type": "lldb", "request": "launch", - "mode": "auto", - "env": { - "CGO_ENABLED": "1" + "name": "Debug executable 'EasyImageCdn'", + "cargo": { + "args": [ + "build", + "--bin=EasyImageCdn", + "--package=EasyImageCdn" + ], + "filter": { + "name": "EasyImageCdn", + "kind": "bin" + } }, - "program": "${workspaceFolder}" + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'EasyImageCdn'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=EasyImageCdn", + "--package=EasyImageCdn" + ], + "filter": { + "name": "EasyImageCdn", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" } ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dc55bd..1a7a4e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ -# 0.2.4 (??.??.2025) +# 0.3.0 (??.??.2025) -* Updated fiber to 2.52.6 -* Updated go image to 0.26.0 -* Updated go version to 1.24 +* Rewrited app to Rust +* Added support for avif +* Added conversion job status # 0.2.3 (27.02.2024) diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7495c32 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2852 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "EasyImageCdn" +version = "0.3.0" +dependencies = [ + "actix", + "actix-files", + "actix-multipart", + "actix-web", + "actix-web-actors", + "anyhow", + "base64", + "bytes", + "dashmap", + "derive_more 2.0.1", + "dotenv", + "futures-util", + "image", + "mime", + "qstring", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", + "webp", + "xxhash-rust", +] + +[[package]] +name = "actix" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" +dependencies = [ + "actix-macros", + "actix-rt", + "actix_derive", + "bitflags 2.9.0", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.9.0", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-files" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be" +dependencies = [ + "actix-http", + "actix-service", + "actix-utils", + "actix-web", + "bitflags 2.9.0", + "bytes", + "derive_more 0.99.19", + "futures-core", + "http-range", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "v_htmlescape", +] + +[[package]] +name = "actix-http" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa882656b67966045e4152c634051e70346939fced7117d5f0b52146a7c74c9" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "base64", + "bitflags 2.9.0", + "brotli", + "bytes", + "bytestring", + "derive_more 2.0.1", + "encoding_rs", + "flate2", + "foldhash", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.0", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-multipart" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "derive_more 0.99.19", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "rand 0.8.5", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b" +dependencies = [ + "darling", + "parse-size", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6398974fd4284f4768af07965701efbbb5fdc0616bff20cade1bb14b77675e24" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e3b15b3dc6c6ed996e4032389e9849d4ab002b1e92fbfe85b5f307d1479b4d" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more 2.0.1", + "encoding_rs", + "foldhash", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-actors" +version = "4.3.1+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980" +dependencies = [ + "actix", + "actix-codec", + "actix-http", + "actix-web", + "bytes", + "bytestring", + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "actix_derive" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytemuck" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bytestring" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "deranged" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.2", + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libwebp-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cd30df7c7165ce74a456e4ca9732c603e8dc5e60784558c1c6dc047f876733" +dependencies = [ + "cc", + "glob", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.44.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", + "serde", +] + +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "v_htmlescape" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f53152f51fb5af0c08484c33d16cca96175881d1f3dec068c23b31a158c2d99" +dependencies = [ + "image", + "libwebp-sys", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bf10b09 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "EasyImageCdn" +version = "0.3.0" +edition = "2021" + +[dependencies] +actix-web = "4.10.2" +actix-files = "0.6.6" +actix-web-actors = "4" +actix-multipart = "0.7.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1" +base64 = "0.22.1" +image = "0.25.4" +xxhash-rust = { version = "0.8.12", features = ["xxh3", "const_xxh3"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tokio = { version = "1.44.2", features = ["full"] } +derive_more = "2.0.1" +anyhow = "1.0.98" +uuid = { version = "1", features = ["v4", "serde"] } +futures-util = "0.3.31" +dashmap = "6.1.0" +bytes = "1.10.1" +actix = "0.13.5" +mime = "0.3.17" +qstring = "0.7.2" +dotenv = "0.15.0" +webp = "0.3" \ No newline at end of file diff --git a/LICENSE b/LICENSE index 432b097..105505e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 2-Clause License -Copyright (c) 2021, Patryk Ławicki +Copyright (c) 2021-2025, Patryk Ławicki All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/Readme.md b/Readme.md index 1fbe2e5..020f60a 100644 --- a/Readme.md +++ b/Readme.md @@ -1,11 +1,14 @@ # EasyImageCdn -[![Build](https://github.com/pcpl2/EasyImageCdn/actions/workflows/buildApp.yml/badge.svg)](https://github.com/pcpl2/EasyImageCdn/actions/workflows/buildApp.yml) ![Docker Image Size with architecture (latest by date/latest semver)](https://img.shields.io/docker/image-size/pcpl2/easy_image_cdn?arch=amd64&label=Image%20size%20amd64&sort=date) ![Docker Image Size with architecture (latest by date/latest semver)](https://img.shields.io/docker/image-size/pcpl2/easy_image_cdn?arch=arm64&label=Image%20size%20arm64&sort=date) ![Docker Pulls](https://img.shields.io/docker/pulls/pcpl2/easy_image_cdn) ![GitHub](https://img.shields.io/github/license/pcpl2/EasyImageCdn) ![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/pcpl2/easy_image_cdn/0.2.3) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/pcpl2/EasyImageCdn) [![CodeFactor](https://www.codefactor.io/repository/github/pcpl2/easyimagecdn/badge)](https://www.codefactor.io/repository/github/pcpl2/easyimagecdn) +[![Build](https://github.com/pcpl2/EasyImageCdn/actions/workflows/buildApp.yml/badge.svg)](https://github.com/pcpl2/EasyImageCdn/actions/workflows/buildApp.yml) ![Docker Image Size with architecture (latest by date/latest semver)](https://img.shields.io/docker/image-size/pcpl2/easy_image_cdn?arch=amd64&label=Image%20size%20amd64&sort=date) ![Docker Image Size with architecture (latest by date/latest semver)](https://img.shields.io/docker/image-size/pcpl2/easy_image_cdn?arch=arm64&label=Image%20size%20arm64&sort=date) ![Docker Pulls](https://img.shields.io/docker/pulls/pcpl2/easy_image_cdn) ![GitHub](https://img.shields.io/github/license/pcpl2/EasyImageCdn) ![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/pcpl2/easy_image_cdn/0.2.3) [![CodeFactor](https://www.codefactor.io/repository/github/pcpl2/easyimagecdn/badge)](https://www.codefactor.io/repository/github/pcpl2/easyimagecdn) ![GitHub Sponsors](https://img.shields.io/github/sponsors/pcpl2) + Application to create a simple cdn server for images. This application automatically converts the uploaded image to webp format and to all resolutions defined in the configuration. +### Warning! The version on this branch is experimental and does not have all functionalities implemented. + ## How to use ```sh @@ -39,11 +42,21 @@ version: '3.9' - './logs:/var/log/eic' ``` -### Endpoints +## Endpoints +All api definitions has moved to [swagger https://pcpl2.github.io/EasyImageCdn/](https://pcpl2.github.io/EasyImageCdn/?urls.primaryName=EasyImageCdn+0.3.0-beta.1) + + +#### Admin: +#### POST /v1/newImage +For send and update image with using json payload and image bytes in base64 + +**Parameters** -#### Admin +| Name | Required | Type | Description | +| -------------:|:--------:|:-------:| --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | required | string | Image identificator. | +| `image` | required | string | Image bytes encoded in base64 | -`http://localhost:9324/v1/newImage` -> For send and update Image Payload: ```json @@ -71,6 +84,7 @@ API_KEY=00000000-0000-0000-0000-000000000000 API_KEY_HEADER=key CONVERT_TO_RES=1024x720,800x600 MAX_FILE_SIZE=10 +TARGET_FORMATS=jpg,webp,avif ``` ### Config values description @@ -80,10 +94,9 @@ MAX_FILE_SIZE=10 | API_KEY | 00000000-0000-0000-0000-000000000000 | Api key for upload images | | API_KEY_HEADER | key | Header name for an API key in the request. | | CONVERT_TO_RES | 1024x720,800x600 | List of resolutions to which images will be converted. | +| TARGET_FORMATS | jpg | List of target image formats. Current supported is `jpg`,`webp`,`avif` | | MAX_FILE_SIZE | 10 | Maximum size of the file sent to the application in megabytes. | | CACHE_TIME | 30 | Image cache lifetime set in minutes. | -| EXPVAR_ENABLED | 0 | Enable golang Expvar for monitoring. Data is available on `0.0.0.0:9125/debug/vars` | -| PPROF_ENABLED | 0 | Enable golang Pprof for monitoring. Data is available on `0.0.0.0:9125/debug/pprof/` | ### Volumes configuration in container diff --git a/app/build/build.go b/app/build/build.go deleted file mode 100644 index ec5fd28..0000000 --- a/app/build/build.go +++ /dev/null @@ -1,4 +0,0 @@ -package build - -var Time string -var Version string = "development" diff --git a/biz/config.go b/biz/config.go deleted file mode 100644 index 547a702..0000000 --- a/biz/config.go +++ /dev/null @@ -1,63 +0,0 @@ -package biz - -import ( - "errors" - "os" - "strconv" - "strings" - - "github.com/joho/godotenv" - - models "easy-image-cdn.pcpl2lab.ovh/models" - appLogger "easy-image-cdn.pcpl2lab.ovh/utils/logger" -) - -var config models.ApiConfig -var loaded = false - -func InitConfiguration() { - if os.Getenv("IN_DOCKER") != "1" { - err := godotenv.Load(".env") - if err != nil { - appLogger.ErrorLogger.Println("Error with loading .env file: " + err.Error()) - } - } - - maxFilesize, _ := strconv.Atoi(os.Getenv("MAX_FILE_SIZE")) - cacheTime, _ := strconv.Atoi(os.Getenv("CACHE_TIME")) - - //TODO validate all configuration - - config = models.ApiConfig{ - APIKey: os.Getenv("API_KEY"), - APIKeyHeader: os.Getenv("API_KEY_HEADER"), - FilesPath: "/var/lib/images", - MaxFileSize: maxFilesize, - CacheTime: cacheTime, - } - - loadResolutions() -} - -func loadResolutions() { - config.Resolutions = []models.ResElement{} - resList := strings.Split(os.Getenv("CONVERT_TO_RES"), ",") - for _, element := range resList { - res := strings.Split(element, "x") - width, _ := strconv.Atoi(res[0]) - height, _ := strconv.Atoi(res[1]) - config.Resolutions = append(config.Resolutions, models.ResElement{ - Width: width, - Height: height, - }) - } - - loaded = true -} - -func GetConfig() (models.ApiConfig, error) { - if !loaded { - return config, errors.New("configuration not initialized") - } - return config, nil -} diff --git a/controllers/adminApis/adminApi.go b/controllers/adminApis/adminApi.go deleted file mode 100644 index 4370664..0000000 --- a/controllers/adminApis/adminApi.go +++ /dev/null @@ -1,186 +0,0 @@ -package adminapis - -import ( - "encoding/base64" - "io" - - "net/url" - "os" - - "github.com/gofiber/fiber/v2" - - appLogger "easy-image-cdn.pcpl2lab.ovh/utils/logger" - - httpUtils "easy-image-cdn.pcpl2lab.ovh/controllers/utils" - - biz "easy-image-cdn.pcpl2lab.ovh/biz" - ic "easy-image-cdn.pcpl2lab.ovh/imageConverter" - models "easy-image-cdn.pcpl2lab.ovh/models" -) - -func PostNewImage(ctx *fiber.Ctx) error { - config, err := biz.GetConfig() - if err != nil { - appLogger.ErrorLogger.Println(err) - return ctx.SendStatus(fiber.StatusUnauthorized) - } - - if !ctx.Is("json") { - appLogger.WarningLogger.Print("Invalid content type") - return ctx.SendStatus(fiber.StatusBadRequest) - } - - if !httpUtils.ValidateAuth(ctx, config) { - appLogger.WarningLogger.Print("Auth error") - return ctx.SendStatus(fiber.StatusUnauthorized) - } - - var Payload models.ImagePayload - - if err := ctx.BodyParser(&Payload); err != nil { - appLogger.WarningLogger.Print(err.Error()) - return ctx.SendStatus(fiber.StatusBadRequest) - } - - imageFolderPath := config.FilesPath + "/" + url.PathEscape(Payload.ID) - - if err := createFileFolder(imageFolderPath); err != nil { - return ctx.SendStatus(fiber.StatusBadRequest) - } - - dec, err := base64.StdEncoding.DecodeString(Payload.Image) - if err != nil { - appLogger.ErrorLogger.Print("Cannot read file from payload " + err.Error()) - return ctx.SendStatus(fiber.StatusNoContent) - } - - sourceFilename := "source" - sourcePath := imageFolderPath + "/" + sourceFilename - - if err := saveFile(sourcePath, dec); err != nil { - return ctx.SendStatus(fiber.StatusNoContent) - } - - queueList := createConvertCommands(config, imageFolderPath) - - ic.ConvertImage(sourcePath, queueList) - return ctx.SendStatus(200) -} - -func PostNewImageMP(ctx *fiber.Ctx) error { - config, err := biz.GetConfig() - if err != nil { - appLogger.ErrorLogger.Println(err) - return ctx.SendStatus(fiber.StatusUnauthorized) - } - - if !httpUtils.ValidateAuth(ctx, config) { - appLogger.WarningLogger.Print("Auth error") - return ctx.SendStatus(fiber.StatusUnauthorized) - } - - imageId := ctx.Query("imageId") - - if imageId == "" { - appLogger.WarningLogger.Print("Invalid image id") - return ctx.SendStatus(fiber.StatusBadRequest) - } - - imageFolderPath := config.FilesPath + "/" + url.PathEscape(imageId) - - if err := createFileFolder(imageFolderPath); err != nil { - appLogger.ErrorLogger.Print("Cannot create folder: " + err.Error()) - return ctx.SendStatus(fiber.StatusBadRequest) - } - - sourceFilename := "source" - sourcePath := imageFolderPath + "/" + sourceFilename - - file, err := ctx.FormFile("imageFile") - - if err != nil { - appLogger.WarningLogger.Print("Invalid content type") - return ctx.SendStatus(fiber.StatusBadRequest) - - } - - hFile, err := file.Open() - if err != nil { - appLogger.ErrorLogger.Print("Cannot open file: " + err.Error()) - return ctx.SendStatus(fiber.StatusBadRequest) - } - - fbytes, err := io.ReadAll(hFile) - if err != nil { - appLogger.ErrorLogger.Print("Cannot read file: " + err.Error()) - return ctx.SendStatus(fiber.StatusBadRequest) - } - - if err := saveFile(sourcePath, fbytes); err != nil { - appLogger.ErrorLogger.Print("Cannot save file: " + err.Error()) - return ctx.SendStatus(fiber.StatusBadRequest) - } - - queueList := createConvertCommands(config, imageFolderPath) - - ic.ConvertImage(sourcePath, queueList) - return ctx.SendStatus(200) -} - -func createFileFolder(imageFolderPath string) error { - if _, err := os.Stat(imageFolderPath); os.IsNotExist(err) { - errMkDir := os.Mkdir(imageFolderPath, 0755) - if errMkDir != nil { - appLogger.ErrorLogger.Print("Failed to create folder: " + errMkDir.Error()) - return errMkDir - } - } - return nil -} - -func saveFile(sourcePath string, file []byte) error { - f, err := os.OpenFile(sourcePath, os.O_WRONLY|os.O_CREATE, 0777) - if err != nil { - appLogger.ErrorLogger.Print("Cannot open file " + err.Error()) - return err - } - defer f.Close() - - if _, err := f.Write(file); err != nil { - appLogger.ErrorLogger.Print("Cannot write file " + err.Error()) - return err - } - - if err := f.Sync(); err != nil { - appLogger.ErrorLogger.Print("Cannot sync file " + err.Error()) - return err - } - - return nil -} - -func createConvertCommands(config models.ApiConfig, imageFolderPath string) []models.ConvertCommand { - queueList := []models.ConvertCommand{} - queueList = append(queueList, models.ConvertCommand{ - Path: imageFolderPath + "/", - WebP: true, - ConvertRes: false, - }) - - for _, element := range config.Resolutions { - queueList = append(queueList, models.ConvertCommand{ - Path: imageFolderPath + "/", - WebP: true, - ConvertRes: true, - TargetRes: element, - }) - - queueList = append(queueList, models.ConvertCommand{ - Path: imageFolderPath + "/", - WebP: false, - ConvertRes: true, - TargetRes: element, - }) - } - return queueList -} diff --git a/controllers/publicApis/publicApi.go b/controllers/publicApis/publicApi.go deleted file mode 100644 index 8ec8958..0000000 --- a/controllers/publicApis/publicApi.go +++ /dev/null @@ -1,34 +0,0 @@ -package publicapis - -import ( - "errors" - "fmt" - "os" - "strings" - - "github.com/gofiber/fiber/v2" - - biz "easy-image-cdn.pcpl2lab.ovh/biz" - appLogger "easy-image-cdn.pcpl2lab.ovh/utils/logger" -) - -func GetImage(ctx *fiber.Ctx, id string, fileName string) error { - config, err := biz.GetConfig() - if err != nil { - appLogger.ErrorLogger.Print(err) - return ctx.SendStatus(fiber.StatusInternalServerError) - } - - acceptHeader := ctx.Get("Accept") - - fileNameWithEx := fileName - - if strings.Contains(acceptHeader, "image/webp") { - fileNameWithEx = fmt.Sprintf("%s.webp", fileName) - } - filePath := fmt.Sprintf("%s/%s/%s", config.FilesPath, id, fileNameWithEx) - if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) { - return ctx.SendStatus(fiber.StatusNotFound) - } - return ctx.SendFile(filePath, true) -} diff --git a/controllers/utils/httpUtils.go b/controllers/utils/httpUtils.go deleted file mode 100644 index 08aa889..0000000 --- a/controllers/utils/httpUtils.go +++ /dev/null @@ -1,11 +0,0 @@ -package utils - -import ( - "github.com/gofiber/fiber/v2" - - models "easy-image-cdn.pcpl2lab.ovh/models" -) - -func ValidateAuth(ctx *fiber.Ctx, config models.ApiConfig) bool { - return ctx.Get(config.APIKeyHeader) == config.APIKey -} diff --git a/controllers/utils/utils.go b/controllers/utils/utils.go deleted file mode 100644 index 260131d..0000000 --- a/controllers/utils/utils.go +++ /dev/null @@ -1,11 +0,0 @@ -package utils - -func DeleteEmpty(s []string) []string { - var r []string - for _, str := range s { - if str != "" { - r = append(r, str) - } - } - return r -} diff --git a/dockerfile b/dockerfile index 0c9e189..0c4cdcf 100644 --- a/dockerfile +++ b/dockerfile @@ -1,17 +1,18 @@ -FROM golang:1.24.2-bullseye AS builder +FROM rust:1.86.0-alpine AS builder ARG App_Version -RUN apt-get update && apt-get --no-install-recommends -y install musl musl-dev musl-tools +RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconf git +# Set `SYSROOT` to a dummy path (default is /usr) because pkg-config-rs *always* +# links those located in that path dynamically but we want static linking, c.f. +# https://github.com/rust-lang/pkg-config-rs/blob/54325785816695df031cef3b26b6a9a203bbc01b/src/lib.rs#L613 +ENV SYSROOT=/dummy WORKDIR /build COPY . . -RUN go env -w CGO_ENABLED=1 GOOS=linux CC=/usr/bin/musl-gcc -RUN go get -d -v -RUN go build -v -ldflags="-linkmode external -extldflags=-static -w -s -X 'easy-image-cdn.pcpl2lab.ovh/app/build.Version=${App_Version}' -X 'easy-image-cdn.pcpl2lab.ovh/app/build.Time=$(date)'" -o imageCdn . -RUN go test -v ./imageConverter +RUN cargo build --bins --release RUN mkdir -p images RUN touch images/dontRemoveMe.txt @@ -23,24 +24,20 @@ FROM busybox:1.37.0 AS builder-user RUN addgroup -g 10002 appUser && \ adduser -D -u 10003 -G appUser appUser -FROM gcr.io/distroless/static-debian11 -COPY --from=builder --chown=10003:10002 /build/imageCdn / +FROM scratch + +COPY --from=builder --chown=10003:10002 /build/target/release/EasyImageCdn / COPY --from=builder-user /etc/passwd /etc/passwd COPY --from=builder --chown=10003:10002 /build/logs /var/log/eic/ -COPY --from=builder --chown=10003:10002 /build/images /var/lib/images/ +COPY --from=builder --chown=10003:10002 /build/images /output ENV IN_DOCKER=1 \ - API_KEY="00000000-0000-0000-0000-000000000000" \ - API_KEY_HEADER="key" \ CONVERT_TO_RES="1024x720,800x600" \ MAX_FILE_SIZE=10 \ - CACHE_TIME=30 \ - EXPVAR_ENABLED=0 \ - PPROF_ENABLED=0 + CACHE_TIME=30 EXPOSE 9324 EXPOSE 9555 -EXPOSE 9125 USER appUser -ENTRYPOINT ["/imageCdn"] +ENTRYPOINT ["/EasyImageCdn"] diff --git a/go.mod b/go.mod deleted file mode 100644 index 0df2cae..0000000 --- a/go.mod +++ /dev/null @@ -1,26 +0,0 @@ -module easy-image-cdn.pcpl2lab.ovh - -go 1.24 - -require ( - github.com/gofiber/fiber/v2 v2.52.6 - github.com/joho/godotenv v1.5.1 - github.com/pcpl2/go-webp v0.0.1 - golang.org/x/image v0.26.0 -) - -require ( - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/klauspost/compress v1.17.9 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/tinylib/msgp v1.2.5 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.55.0 // indirect - github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/sys v0.28.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 08cb18f..0000000 --- a/go.sum +++ /dev/null @@ -1,38 +0,0 @@ -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= -github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/pcpl2/go-webp v0.0.1 h1:JidBFbqo+irRNZtVbp1Rhgg1XIiNey/xvXV9kd5y98Y= -github.com/pcpl2/go-webp v0.0.1/go.mod h1:ggV3YXOJZLnl2pcYsCgBdcwq6DxrEw5oKKPUBWkLwME= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= -github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= -github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8= -github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= -golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/imageConverter/imageProcess.go b/imageConverter/imageProcess.go deleted file mode 100644 index be689f4..0000000 --- a/imageConverter/imageProcess.go +++ /dev/null @@ -1,83 +0,0 @@ -package imageconverter - -import ( - "bytes" - "image" - "os" - "strconv" - - _ "image/jpeg" - png "image/png" - - "github.com/pcpl2/go-webp" - "golang.org/x/image/draw" - - models "easy-image-cdn.pcpl2lab.ovh/models" - appLogger "easy-image-cdn.pcpl2lab.ovh/utils/logger" -) - -func ConvertImage(imagePath string, command []models.ConvertCommand) { - orginalImage, err := openFile(imagePath) - if err != nil { - appLogger.ErrorLogger.Println(err) - return - } - - for _, cmd := range command { - if cmd.ConvertRes { - org := *orginalImage - resized := image.NewNRGBA(image.Rect(0, 0, cmd.TargetRes.Width, cmd.TargetRes.Height)) - draw.Draw(resized, resized.Bounds(), image.White, image.Point{}, draw.Src) - draw.ApproxBiLinear.Scale(resized, resized.Bounds(), org, org.Bounds(), draw.Src, nil) - _ = saveFile(cmd.Path, strconv.Itoa(cmd.TargetRes.Width)+"x"+strconv.Itoa(cmd.TargetRes.Height), resized, cmd.WebP) - } else { - _ = saveFile(cmd.Path, "source", *orginalImage, cmd.WebP) - } - } -} - -func saveFile(filePath string, fileName string, image image.Image, toWebp bool) error { - path := filePath + fileName - b := new(bytes.Buffer) - if toWebp { - if err := webp.Encode(b, image, &webp.Options{Quality: 80}); err != nil { - appLogger.ErrorLogger.Println(err) - return err - } - path = path + ".webp" - } else { - if err := png.Encode(b, image); err != nil { - appLogger.ErrorLogger.Println(err) - return err - } - } - newFile, err := os.Create(path) - if err != nil { - return err - } - - defer newFile.Close() - - _, err = b.WriteTo(newFile) - - return err -} - -func openFile(filePath string) (*image.Image, error) { - file, err := os.Open(filePath) - - if err != nil { - appLogger.ErrorLogger.Println("Cannot open file: " + err.Error()) - return nil, err - } - defer file.Close() - - decodedImage, _, err := image.Decode(file) - - if err != nil && decodedImage == nil { - appLogger.ErrorLogger.Println("Cannot read file: " + err.Error()) - return nil, nil - } - - return &decodedImage, nil -} diff --git a/imageConverter/imageProcess_test.go b/imageConverter/imageProcess_test.go deleted file mode 100644 index 3e91664..0000000 --- a/imageConverter/imageProcess_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package imageconverter - -import ( - "crypto/sha256" - "encoding/hex" - "os" - "testing" - - models "easy-image-cdn.pcpl2lab.ovh/models" -) - -func TestOpenImage(t *testing.T) { - _, err := openFile("../testImages/350x150.png") - if err != nil { - t.Fatalf("%s", err.Error()) - } -} - -func TestConvertImageTo64X64(t *testing.T) { - ConvertImage("../testImages/350x150.png", []models.ConvertCommand{ - { - Path: "../testImages" + "/", - WebP: false, - ConvertRes: true, - TargetRes: models.ResElement{Width: 64, Height: 64}, - }, - }) - - checksum, err := calcuateCheckSumForFile("../testImages/64x64") - if err != nil { - t.Fatalf("%s", err.Error()) - } - - if *checksum != "75f2b66fc23eae1022f73979c76c6f4c3bc1f46f920b3a8cbfab48f68221e991" { - t.Failed() - } - - cleanAfterTest("../testImages/64x64") -} - -func TestConvertImageTo64X64WebP(t *testing.T) { - ConvertImage("../testImages/350x150.png", []models.ConvertCommand{ - { - Path: "../testImages" + "/", - WebP: true, - ConvertRes: true, - TargetRes: models.ResElement{Width: 64, Height: 64}, - }, - }) - - checksum, err := calcuateCheckSumForFile("../testImages/64x64.webp") - if err != nil { - t.Fatalf("%s", err.Error()) - } - - if *checksum != "2cb03ec0c8d424fad03a355a930efe7548f1f6b1f729cccabbf0ddc676025eeb" { - t.Failed() - } - - cleanAfterTest("../testImages/64x64.webp") -} - -func calcuateCheckSumForFile(filePath string) (*string, error) { - s, err := os.ReadFile(filePath) - hasher := sha256.New() - hasher.Write(s) - if err != nil { - return nil, err - } - - hash := (hex.EncodeToString(hasher.Sum(nil))) - - return &hash, nil -} - -func cleanAfterTest(filePath string) { - os.Remove(filePath) -} diff --git a/main.go b/main.go deleted file mode 100644 index 336f40c..0000000 --- a/main.go +++ /dev/null @@ -1,130 +0,0 @@ -package main - -import ( - "os" - "strings" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cache" - "github.com/gofiber/fiber/v2/middleware/compress" - "github.com/gofiber/fiber/v2/middleware/etag" - expvarmw "github.com/gofiber/fiber/v2/middleware/expvar" - "github.com/gofiber/fiber/v2/middleware/logger" - "github.com/gofiber/fiber/v2/middleware/pprof" - - "easy-image-cdn.pcpl2lab.ovh/app/build" - biz "easy-image-cdn.pcpl2lab.ovh/biz" - appLogger "easy-image-cdn.pcpl2lab.ovh/utils/logger" - - aApi "easy-image-cdn.pcpl2lab.ovh/controllers/adminApis" - pApi "easy-image-cdn.pcpl2lab.ovh/controllers/publicApis" - utils "easy-image-cdn.pcpl2lab.ovh/controllers/utils" -) - -func main() { - appLogger.StartLogger() - appLogger.InfoLogger.Print("Loading config..") - biz.InitConfiguration() - config, err := biz.GetConfig() - if err != nil { - appLogger.ErrorLogger.Fatal(err) - } - - if config.APIKey == "" || config.APIKey == "00000000-0000-0000-0000-000000000000" { - appLogger.ErrorLogger.Fatalln("*ERROR* The application will not start without setting the value API_KEY") - } - - appLogger.InfoLogger.Print("Configuration loaded.") - - fiberLogger := logger.New(logger.Config{ - Format: "INFO: ${time} [${ip}]:${port} ${status}${latency} - ${method} ${path} ${ua}\n", - TimeFormat: "2006/01/02 15:04:05", - Output: appLogger.LoggerWritter, - }) - - adminApp := fiber.New(fiber.Config{ - BodyLimit: config.MaxFileSize * 1024 * 1024, - DisableStartupMessage: true, - ServerHeader: "", - }) - - adminApp.Use(fiberLogger) - - adminApp.Post("/v1/newImage", func(c *fiber.Ctx) error { - return aApi.PostNewImage(c) - }) - - adminApp.Post("/v1/newImageMp", func(c *fiber.Ctx) error { - return aApi.PostNewImageMP(c) - }) - - appLogger.InfoLogger.Printf("Starting HTTP server on 0.0.0.0:9324") - go func() { - if err := adminApp.Listen("0.0.0.0:9324"); err != nil { - appLogger.ErrorLogger.Fatalf("error in adminApp.Listen: %s", err) - } - }() - - publicApp := fiber.New(fiber.Config{ - ServerHeader: "", - DisableStartupMessage: true, - }) - - publicApp.Use(fiberLogger) - publicApp.Use(etag.New()) - publicApp.Use(compress.New(compress.Config{ - Level: compress.LevelBestCompression, - })) - - publicApp.Use(cache.New(cache.Config{ - Next: func(c *fiber.Ctx) bool { - return c.Query("refresh") == "true" - }, - Expiration: time.Duration(config.CacheTime) * time.Minute, - CacheControl: true, - })) - - publicApp.Get("/*", func(c *fiber.Ctx) error { - spath := utils.DeleteEmpty(strings.Split(c.Path(), "/")) - fileName := "source" - if len(spath) < 1 { - return c.SendStatus(fiber.StatusNotFound) - } else if len(spath) == 2 { - fileName = spath[1] - } - - return pApi.GetImage(c, spath[0], fileName) - }) - - appLogger.InfoLogger.Printf("Starting HTTP server on 0.0.0.0:9555") - go func() { - if err := publicApp.Listen("0.0.0.0:9555"); err != nil { - appLogger.ErrorLogger.Fatalf("error in publicApp.Listen: %s", err) - } - }() - - //App monitoring - if os.Getenv("EXPVAR_ENABLED") == "1" || os.Getenv("PPROF_ENABLED") == "1" { - appMonitoring := fiber.New(fiber.Config{ - DisableStartupMessage: true, - ServerHeader: "", - }) - if os.Getenv("EXPVAR_ENABLED") == "1" { - appMonitoring.Use(expvarmw.New()) - } - if os.Getenv("PPROF_ENABLED") == "1" { - appMonitoring.Use(pprof.New()) - } - go func() { - appLogger.InfoLogger.Printf("App Monitoring started on 0.0.0.0:9125") - if err := appMonitoring.Listen(":9125"); err != nil { - appLogger.ErrorLogger.Fatalf("error in expvarHttp.Listen: %s", err) - } - }() - } - - appLogger.InfoLogger.Printf("Started EasyImageCdn %s (Builded at %s)", build.Version, build.Time) - - select {} -} diff --git a/models/models.go b/models/models.go deleted file mode 100644 index 72d26ee..0000000 --- a/models/models.go +++ /dev/null @@ -1,22 +0,0 @@ -package models - -type ApiConfig struct { - APIKey string - APIKeyHeader string - FilesPath string - MaxFileSize int - CacheTime int - Resolutions []ResElement -} - -type ResElement struct { - Width int - Height int -} - -type ConvertCommand struct { - Path string - WebP bool - ConvertRes bool - TargetRes ResElement -} diff --git a/models/payloads.go b/models/payloads.go deleted file mode 100644 index ccf8a82..0000000 --- a/models/payloads.go +++ /dev/null @@ -1,6 +0,0 @@ -package models - -type ImagePayload struct { - ID string `json:"id"` - Image string `json:"image"` -} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..a58ca7f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,52 @@ +use dotenv::dotenv; +use std::env; + +use crate::models::{Config, TargetFormat}; + +pub fn read_env() -> Config { + // Check if running in Docker + let in_docker = env::var("IN_DOCKER").unwrap_or_else(|_| "0".to_string()) == "1"; + + // Load .env if not in Docker + if !in_docker { + dotenv().ok(); + } + + // Read environment variables + let api_key = env::var("API_KEY").expect("API_KEY is not set"); + let api_key_header = env::var("API_KEY_HEADER").expect("API_KEY_HEADER is not set"); + let convert_to_res = env::var("CONVERT_TO_RES").expect("CONVERT_TO_RES is not set"); + let max_file_size: u32 = env::var("MAX_FILE_SIZE") + .expect("MAX_FILE_SIZE is not set") + .parse() + .expect("MAX_FILE_SIZE must be a valid u32"); + let target_formats = env::var("TARGET_FORMATS").unwrap_or_else(|_| "jpg".to_string()); + + // Parse resolutions + let convert_to_res: Vec<(u32, u32)> = convert_to_res + .split(',') + .map(|res| { + let parts: Vec<&str> = res.split('x').collect(); + if parts.len() != 2 { + panic!("Invalid resolution format: {}", res); + } + let width = parts[0].parse().expect("Invalid width in resolution"); + let height = parts[1].parse().expect("Invalid height in resolution"); + (width, height) + }) + .collect(); + + // Parse target formats + let target_formats: Vec = target_formats + .split(',') + .map(|format| TargetFormat::from_str(format).expect("Invalid target format")) + .collect(); + + Config { + api_key, + api_key_header, + convert_to_res, + max_file_size, + target_formats, + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..ce67132 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,63 @@ +use actix_web::{http::StatusCode, HttpResponse, ResponseError}; +use derive_more::Display; + +#[derive(Debug, Display)] +pub enum AppError { + #[display("Bad Request: {}", _0)] + BadRequest(String), + #[display("Internal Server Error: {}", _0)] + InternalError(String), + #[display("Failed to queue job: {}", _0)] + QueueError(String), + // #[display("Image processing error: {}", _0)] + // ImageProcessingError(String), + #[display("Multipart error: {}", _0)] + MultipartError(String), +} + +impl ResponseError for AppError { + fn status_code(&self) -> StatusCode { + match *self { + AppError::BadRequest(_) => StatusCode::BAD_REQUEST, + AppError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + AppError::QueueError(_) => StatusCode::INTERNAL_SERVER_ERROR, + //AppError::ImageProcessingError(_) => StatusCode::INTERNAL_SERVER_ERROR, + AppError::MultipartError(_) => StatusCode::BAD_REQUEST, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()) + .json(serde_json::json!({ "error": self.to_string() })) + } +} + +impl From for AppError { + fn from(err: base64::DecodeError) -> Self { + AppError::BadRequest(format!("Invalid base64 data: {}", err)) + } +} + +impl From> for AppError { + fn from(err: tokio::sync::mpsc::error::SendError) -> Self { + AppError::QueueError(format!("Failed to send job to queue: {}", err)) + } +} + +impl From for AppError { + fn from(err: actix_multipart::MultipartError) -> Self { + AppError::MultipartError(format!("Multipart stream error: {}", err)) + } +} + +impl From for AppError { + fn from(err: std::io::Error) -> Self { + AppError::InternalError(format!("IO Error: {}", err)) + } +} + +impl From for AppError { + fn from(err: anyhow::Error) -> Self { + AppError::InternalError(format!("Processing failed: {}", err)) + } +} diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..8d90289 --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,534 @@ +use actix_files as fs; +use actix_multipart::Multipart; +use actix_web::{error::ErrorNotFound, Result}; +use actix_web::{web, Error as ActixError, HttpRequest, HttpResponse, Responder}; +use base64::{engine::general_purpose::STANDARD as base64_standard, Engine as _}; +use futures_util::stream::TryStreamExt; +use mime::Mime; +use qstring::QString; +use std::path::Path; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; +use uuid::Uuid; + +use actix::{Actor, ActorContext, AsyncContext}; + +use actix_web_actors::ws; +use bytes::Bytes; +use futures_util::stream::unfold; +use tokio::sync::mpsc as TokioMpsc; + +use crate::errors::AppError; +use crate::image_processing::generate_output_path; +use crate::models::{ + AppState, Config, ImageIdQuery, ImageJob, JobQueuedResponse, JobState, JobStatus, + NewImageRequest, TargetFormat, +}; + +fn verify_apikey(req: &HttpRequest, config: Arc) -> Result<(), HttpResponse> { + if let Some(apikey) = req.headers().get(config.api_key_header.as_str()) { + if apikey == config.api_key.as_str() { + return Ok(()); + } else { + return Err(HttpResponse::Unauthorized().body("Invalid API Key")); + } + } + Err(HttpResponse::BadRequest().body("Missing API Key")) +} + +pub fn escape_file_identifier(identifier: &str) -> Option { + if identifier.is_empty() { + return None; + } + + let sanitized: String = identifier + .chars() + .filter(|&c| c.is_alphanumeric() || c == '-' || c == '_') + .collect(); + + if sanitized.is_empty() { + return None; + } + + let path = Path::new(&sanitized); + if path + .components() + .any(|comp| matches!(comp, std::path::Component::ParentDir)) + { + return None; + } + + Some(sanitized) +} + +pub async fn new_image_json( + req: HttpRequest, + state: web::Data, + payload: web::Json, +) -> Result { + if let Err(error_response) = verify_apikey(&req, state.config.clone()) { + return Ok(error_response); + } + + tracing::info!("Received new image request via JSON for id: {}", payload.id); + let image_data = base64_standard.decode(&payload.image)?; + let job_id = Uuid::new_v4(); + + let image_id = escape_file_identifier(&payload.id); + if image_id.is_none() { + return Ok(HttpResponse::BadRequest().body("Invalid image id")); + } + + let job = ImageJob { + job_id, + image_id: image_id.unwrap(), + image_data, + target_resolutions: state.config.convert_to_res.clone(), + target_formats: state.config.target_formats.clone(), + }; + + state.job_statuses.insert( + job_id, + JobState { + status: JobStatus::Queued, + }, + ); + // ----------------------------------------- + + state.job_sender.send(job).await?; + tracing::info!("Job {} queued for image id {}", job_id, payload.id); + Ok(HttpResponse::Accepted().json(JobQueuedResponse { + job_id, + image_id: payload.id.clone(), + status: "Queued".to_string(), //TODO Change to JobStatus::Queued + })) +} + +pub async fn new_image_multipart( + req: HttpRequest, + state: web::Data, + query: web::Query, + mut payload: Multipart, +) -> Result { + if let Err(error_response) = verify_apikey(&req, state.config.clone()) { + return Ok(error_response); + } + let mut image_data: Option> = None; + + let image_id = escape_file_identifier(query.into_inner().image_id.as_str()); + if image_id.is_none() { + return Ok(HttpResponse::BadRequest().body("Invalid image id")); + } + + tracing::info!( + "Received new image request via Multipart for id: {}", + image_id.clone().unwrap() + ); + + while let Some(mut field) = payload.try_next().await? { + let disposition_opt = field.content_disposition(); + let field_name = disposition_opt + .as_ref() + .and_then(|d| d.get_name()) + .unwrap_or(""); + if field_name == "imageFile" { + let mut field_data = Vec::new(); + while let Some(chunk) = field.try_next().await? { + field_data.extend_from_slice(&chunk); + } + image_data = Some(field_data); + } else { + tracing::warn!("Ignoring unexpected multipart field: {}", field_name); + while field.try_next().await?.is_some() {} + } + } + let image_data = + image_data.ok_or_else(|| AppError::BadRequest("Missing 'imageFile' field".to_string()))?; + // ------------------------------------------------------ + + let job_id = Uuid::new_v4(); + let job = ImageJob { + job_id, + image_id: image_id.clone().unwrap(), + image_data, + target_resolutions: state.config.convert_to_res.clone(), + target_formats: state.config.target_formats.clone(), + }; + + state.job_statuses.insert( + job_id, + JobState { + status: JobStatus::Queued, + }, + ); + // ----------------------------------------- + + state.job_sender.send(job).await?; + tracing::info!( + "Job {} queued for image id {}", + job_id, + image_id.clone().unwrap() + ); + Ok(HttpResponse::Accepted().json(JobQueuedResponse { + job_id, + image_id: image_id.unwrap(), + status: "Queued".to_string(), + })) +} + +pub async fn get_job_status( + req: HttpRequest, + state: web::Data, + path: web::Path, +) -> Result { + if let Err(error_response) = verify_apikey(&req, state.config.clone()) { + return Ok(error_response); + } + + let job_id = path.into_inner(); + tracing::debug!("Checking status for job: {}", job_id); + + match state.job_statuses.get(&job_id) { + Some(job_state_entry) => { + let job_state = job_state_entry.value().clone(); + tracing::info!("Status for job {}: {:?}", job_id, job_state.status); + Ok(HttpResponse::Ok().json(job_state)) + } + None => { + tracing::warn!("Job not found: {}", job_id); + Err(AppError::BadRequest(format!( + "Job with id {} not found", + job_id + ))) + } + } +} + +struct JobStatusWs { + job_id: Uuid, + state: web::Data, +} + +impl JobStatusWs { + fn check_status(&self, ctx: &mut ws::WebsocketContext) { + match self.state.job_statuses.get(&self.job_id) { + Some(entry) => { + let state = entry.value().clone(); + let status_json = serde_json::to_string(&state).unwrap_or_else(|e| { + tracing::error!("Failed to serialize job status for {}: {}", self.job_id, e); + format!("{{\"error\":\"Failed to serialize status: {}\"}}", e) + }); + + ctx.text(status_json); + + match state.status { + JobStatus::Completed | JobStatus::Failed(_) => { + tracing::info!( + "Job {} finished ({:?}). Closing WebSocket.", + self.job_id, + state.status + ); + ctx.stop(); + } + JobStatus::Queued | JobStatus::Processing => {} + } + } + None => { + tracing::warn!( + "Job {} not found during WS check. Closing WebSocket.", + self.job_id + ); + let error_msg = format!("{{\"error\":\"Job with id {} not found\"}}", self.job_id); + ctx.text(error_msg); + ctx.stop(); + } + } + } +} + +impl Actor for JobStatusWs { + type Context = ws::WebsocketContext; + fn started(&mut self, ctx: &mut Self::Context) { + tracing::info!("WebSocket connection started for job: {}", self.job_id); + + ctx.run_interval(Duration::from_secs(1), |act, ctx_interval| { + act.check_status(ctx_interval); + }); + self.check_status(ctx); + } + + fn stopped(&mut self, _ctx: &mut Self::Context) { + tracing::info!("WebSocket connection stopped for job: {}", self.job_id); + } +} + +impl actix::StreamHandler> for JobStatusWs { + fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { + match msg { + Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), + Ok(ws::Message::Text(text)) => { + tracing::debug!( + "Received WS message from client for job {}: {}", + self.job_id, + text + ); + ctx.text(format!("{{\"message\":\"Received your text: {}\"}}", text)); + } + Ok(ws::Message::Close(reason)) => { + tracing::info!( + "Client closed WebSocket for job {}: {:?}", + self.job_id, + reason + ); + ctx.close(reason); + ctx.stop(); + } + _ => (), + } + } +} + +pub async fn websocket_job_status( + req: HttpRequest, + stream: web::Payload, + state: web::Data, + path: web::Path, +) -> Result { + if let Err(error_response) = verify_apikey(&req, state.config.clone()) { + return Ok(error_response); + } + + let job_id = path.into_inner(); + tracing::info!("Initiating WebSocket connection for job: {}", job_id); + if !state.job_statuses.contains_key(&job_id) { + tracing::error!( + "Attempted WebSocket connection for non-existent job: {}", + job_id + ); + return Ok(HttpResponse::NotFound().body(format!("Job with id {} not found", job_id))); + } + + ws::start( + JobStatusWs { + job_id, + state: state.clone(), + }, + &req, + stream, + ) +} + +pub async fn sse_job_status( + req: HttpRequest, + state: web::Data, + path: web::Path, +) -> Result { + if let Err(error_response) = verify_apikey(&req, state.config.clone()) { + return Ok(error_response); + } + + let job_id = path.into_inner(); + tracing::info!("Initiating SSE connection for job: {}", job_id); + + if !state.job_statuses.contains_key(&job_id) { + tracing::error!("Attempted SSE connection for non-existent job: {}", job_id); + return Ok(HttpResponse::NotFound().body(format!("Job with id {} not found", job_id))); + } + + let (tx, rx) = TokioMpsc::channel::(10); + let shared_statuses = state.job_statuses.clone(); + + tokio::spawn(async move { + let mut last_status_sent: Option = None; + + loop { + let current_state = match shared_statuses.get(&job_id) { + Some(entry) => entry.value().clone(), + None => { + let error_state = JobState { + status: JobStatus::Failed(format!( + "Job {} disappeared unexpectedly", + job_id + )), + }; + let _ = tx.send(error_state).await; + tracing::error!( + "Job {} disappeared from state map during SSE monitoring.", + job_id + ); + break; + } + }; + + if last_status_sent.as_ref() != Some(¤t_state.status) { + if tx.send(current_state.clone()).await.is_err() { + tracing::info!( + "SSE receiver closed for job {}. Stopping monitor task.", + job_id + ); + break; + } + last_status_sent = Some(current_state.status.clone()); + tracing::debug!( + "Sent status update via SSE for job {}: {:?}", + job_id, + current_state.status + ); + } + + if matches!( + current_state.status, + JobStatus::Completed | JobStatus::Failed(_) + ) { + tracing::info!( + "Job {} finished ({:?}). Stopping SSE monitor task.", + job_id, + current_state.status + ); + break; + } + + tokio::time::sleep(Duration::from_secs(1)).await; + } + }); + + let initial_state = (rx, job_id); + + let body = unfold(initial_state, |(mut rx, job_id)| async move { + rx.recv().await.map(|received_state| { + let json = serde_json::to_string(&received_state).unwrap_or_else(|e| { + tracing::error!("SSE serialization error for job {}: {}", job_id, e); + "{\"error\":\"Serialization failed\"}".to_string() + }); + let event_bytes = Bytes::from(format!("data: {}\n\n", json)); + (Ok::<_, ActixError>(event_bytes), (rx, job_id)) + }) + }); + + Ok(HttpResponse::Ok() + .content_type("text/event-stream") + .insert_header(("Cache-Control", "no-cache")) + .insert_header(("Connection", "keep-alive")) + .streaming(body)) +} + +fn get_best_image_extension( + accept: &str, + force: &str, + target_formats: Vec, +) -> (&'static str, &'static str) { + let active_formats: Vec = target_formats.clone(); + let preferred_formats = ["image/avif", "image/webp", "image/jpeg"]; + if force != "" { + return match TargetFormat::from_str(force) { + Ok(format) if active_formats.contains(&format) => ( + format.extension(), + match format { + TargetFormat::Avif => "image/avif", + TargetFormat::WebP => "image/webp", + TargetFormat::Jpeg => "image/jpeg", + }, + ), + _ => ("jpg", "image/jpeg"), + }; + } + + let accept_parts: Vec<&str> = accept + .split(',') + .map(|part| part.trim().split(';').next().unwrap_or("")) + .collect(); + + for &preferred in &preferred_formats { + if accept_parts.contains(&preferred) { + let target_format = match preferred { + "image/avif" => TargetFormat::Avif, + "image/webp" => TargetFormat::WebP, + "image/jpeg" => TargetFormat::Jpeg, + _ => TargetFormat::Jpeg, + }; + if active_formats.contains(&target_format) { + return ( + target_format.extension(), + match target_format { + TargetFormat::Avif => "image/avif", + TargetFormat::WebP => "image/webp", + TargetFormat::Jpeg => "image/jpeg", + }, + ); + } + } + } + ("jpg", "image/jpeg") +} + +fn parse_resolution(resolution_str: &str) -> Option<(u32, u32)> { + if resolution_str.contains("..") + || resolution_str.contains('/') + || resolution_str.contains('\\') + || resolution_str == "" + { + return None; + } + + let parts: Vec<&str> = resolution_str.split('x').collect(); + + if parts.len() != 2 { + return None; + } + + let x = parts[0].parse::().ok()?; + let y = parts[1].parse::().ok()?; + + Some((x, y)) +} + +pub async fn get_file( + req: HttpRequest, + config: web::Data>, +) -> Result { + //TODO Get from cache + //TODO Validate referer + let qs = QString::from(req.query_string()); + let image_id = escape_file_identifier(req.match_info().query("image_id")); + if image_id.is_none() { + return Ok(HttpResponse::BadRequest().body("Invalid image id")); + } + let resolution_str = req.match_info().query("resolution"); + let accept = req + .headers() + .get("accept") + .and_then(|val| val.to_str().ok()) + .unwrap_or(""); + let force_type = qs.get("extension").unwrap_or_default(); + let selected_ext = get_best_image_extension(accept, force_type, config.target_formats.clone()); + + //let cache_key = format!("{}_{}.{}", image_id, resolution_str, selected_ext.0); + + let name = match parse_resolution(resolution_str) { + Some((x, y)) => format!( + "{}_{}x{}.{}", + image_id.clone().unwrap(), + x, + y, + selected_ext.0 + ), + None => format!("{}_orginal.{}", image_id.clone().unwrap(), selected_ext.0), + }; + + let base_path = + generate_output_path(image_id.clone().unwrap().as_str()).map_err(ErrorNotFound)?; + let output_path = base_path.join(name); + + tracing::info!("Get file: {:?}", output_path,); + + let file = fs::NamedFile::open(&output_path).map_err(|_| ErrorNotFound("File not found"))?; + + let response = file + .use_etag(true) + .use_last_modified(true) + .set_content_type(Mime::from_str(selected_ext.1).unwrap()) + .disable_content_disposition() + .into_response(&req); + Ok(response) +} diff --git a/src/image_processing.rs b/src/image_processing.rs new file mode 100644 index 0000000..4fe409f --- /dev/null +++ b/src/image_processing.rs @@ -0,0 +1,207 @@ +use crate::models::{ImageJob, TargetFormat}; +use anyhow::{Context, Result}; +use image::{DynamicImage, EncodableLayout, GenericImageView, ImageFormat}; +use std::{ + fs::{self, File}, + io::Write, + path::PathBuf, + time::Instant, +}; + +use xxhash_rust::const_xxh3::xxh3_64 as const_xxh3; +const OUTPUT_BASE_DIR: &'static str = "output"; // TODO: Move to config + +pub fn process_image(job: ImageJob) -> Result<()> { + tracing::debug!("Starting image processing for job: {}", job.job_id); + + let img = image::load_from_memory(&job.image_data) + .with_context(|| format!("Failed to load image from memory for job {}", job.job_id))?; + let (original_width, original_height) = img.dimensions(); + tracing::debug!("Image loaded: {}x{}", original_width, original_height); + + let output_dir = generate_output_path(&job.image_id)?; + + fs::create_dir_all(&output_dir) + .with_context(|| format!("Failed to create output directory: {:?}", output_dir))?; + + match image::guess_format(&job.image_data) { + Ok(format) => { + if let Some(extension) = format.extensions_str().first() { + let original_filename = format!("{}_source.{}", job.image_id, extension); + let original_output_path = output_dir.join(&original_filename); + + tracing::debug!( + "Attempting to save original file (format: {:?}) to: {:?}", + format, + original_output_path + ); + + fs::write(&original_output_path, &job.image_data).with_context(|| { + format!( + "Failed to write original image to {:?}", + original_output_path + ) + })?; + + tracing::info!( + "Successfully saved original file: {:?}", + original_output_path + ); + } else { + tracing::warn!( + "Could not determine a file extension for the original image format: {:?}. Original file not saved.", + format + ); + } + } + Err(e) => { + tracing::warn!( + "Could not guess format of the original image data for job {}: {}. Original file not saved.", + job.job_id, + e + ); + } + } + + let filename_base = job + .image_id + .split(|c| c == '/' || c == '\\') + .last() + .unwrap_or(&job.image_id); + + for (target_width, target_height) in &job.target_resolutions { + // TODO: Dodać logikę skalowania (np. zachowanie proporcji, nie powiększanie) + // Proste skalowanie: + // let resized_img = img.resize_exact(*target_width, *target_height, imageops::FilterType::Lanczos3); + //tracing::debug!("Resized image to {}x{}", *target_width, *target_height); + let resized_img = img.thumbnail(*target_width, *target_height); + let (actual_width, actual_height) = resized_img.dimensions(); + tracing::debug!( + "Image resized from {}x{} to {}x{} (target: {}x{}) while preserving aspect ratio", + original_width, + original_height, + actual_width, + actual_height, + target_width, + target_height + ); + + for format in &job.target_formats { + let file_name_prefix = format!("{}_{}x{}", filename_base, target_width, target_height); + + let _ = save_image_to_format(file_name_prefix, format, &resized_img, &output_dir); + } + } + + //Save orginal size + for format in &job.target_formats { + let file_name_prefix = format!("{}_orginal", filename_base,); + let _ = save_image_to_format(file_name_prefix, format, &img, &output_dir); + } + + Ok(()) +} + +fn save_image_to_format( + file_name_prefix: String, + format: &TargetFormat, + resized_img: &DynamicImage, + output_dir: &PathBuf, +) -> Result<()> { + let start = Instant::now(); + let filename = format!("{}.{}", file_name_prefix, format.extension()); + let output_path = output_dir.join(&filename); + + tracing::debug!("Saving image to: {:?} in format {:?}", output_path, format); + + let saveres = match format { + TargetFormat::WebP => { + let rgb_image = match resized_img { + DynamicImage::ImageRgb8(_) => resized_img.clone(), + DynamicImage::ImageRgba8(_) => resized_img.clone(), + DynamicImage::ImageLumaA8(_) => DynamicImage::ImageRgba8(resized_img.to_rgba8()), + DynamicImage::ImageLuma8(_) => DynamicImage::ImageRgb8(resized_img.to_rgb8()), + _ => DynamicImage::ImageRgb8(resized_img.to_rgb8()), + }; + + // rgb_image + // .save_with_format(&output_path, ImageFormat::WebP) + // .with_context(|| { + // format!( + // "Failed to save WebP using 'image' crate to {:?}", + // output_path + // ) + // }) + + let encoder = webp::Encoder::from_image(&rgb_image).unwrap(); + let memory = encoder.encode(80.0); + + let mut f = File::create(&output_path)?; + f.write_all(memory.as_bytes()).with_context(|| { + format!( + "Failed to save WebP using 'image' crate to {:?}", + output_path + ) + }) + } + + TargetFormat::Avif => resized_img + .save_with_format(&output_path, ImageFormat::Avif) + .with_context(|| { + format!( + "Failed to save AVIF using 'image' crate to {:?}", + output_path + ) + }), + + TargetFormat::Jpeg => { + let rgb_image = match resized_img { + DynamicImage::ImageRgb8(_) => resized_img.clone(), + DynamicImage::ImageRgba8(_) => DynamicImage::ImageRgb8(resized_img.to_rgb8()), + DynamicImage::ImageLuma8(_) | DynamicImage::ImageLumaA8(_) => { + DynamicImage::ImageRgb8(resized_img.to_rgb8()) + } + _ => DynamicImage::ImageRgb8(resized_img.to_rgb8()), + }; + rgb_image + .save_with_format(&output_path, ImageFormat::Jpeg) + .with_context(|| { + format!( + "Failed to save JPEG using 'image' crate to {:?}", + output_path + ) + }) + } + }; + + let duration = start.elapsed(); + + match saveres { + Ok(_) => { + tracing::info!("Successfully saved: {:?} in {:?}", output_path, duration); + Ok(()) + } + Err(e) => { + tracing::error!("Error saving image: {:?}", e); + Err(e) + } + } +} + +pub fn generate_output_path(image_id: &str) -> Result { + let hash = const_xxh3(image_id.as_bytes()); + let hash_hex = format!("{:016x}", hash); + + if hash_hex.len() < 6 { + return Err(anyhow::anyhow!("Hash too short to generate path structure")); + } + + let first_3 = &hash_hex[0..3]; + let last_3 = &hash_hex[hash_hex.len() - 3..]; + + let path = PathBuf::from(OUTPUT_BASE_DIR) + .join(first_3) + .join(last_3) + .join(image_id); + Ok(path) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7d79d1b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,109 @@ +use actix_web::{middleware::Logger, web, App, HttpServer}; +use config::read_env; +use dashmap::DashMap; +use futures_util::future::try_join; +use std::sync::Arc; +use tokio::sync::mpsc; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +use uuid::Uuid; + +mod config; +mod errors; +mod handlers; +mod image_processing; +mod models; +mod worker; + +use models::{AppState, ImageJob, JobState}; + +const SERVER_HOST: &str = "0.0.0.0"; +const IMAGE_SERVER_PORT: u16 = 9555; +const ADMIN_SERVER_PORT: u16 = 9324; + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let config = read_env(); + let my_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + tracing_subscriber::registry() + .with(fmt::layer()) + .with(my_filter) + .init(); + + // TODO: Buffer size add to config + let (job_sender, job_receiver) = mpsc::channel::(100); + + let job_statuses = Arc::new(DashMap::::new()); + let worker_statuses = job_statuses.clone(); + + tokio::spawn(worker::run_worker(job_receiver, worker_statuses)); + + let app_state = web::Data::new(AppState { + job_sender: job_sender.clone(), + job_statuses: job_statuses.clone(), + config: Arc::new(config), + }); + + let app_state_admin = app_state.clone(); + + let main_server = HttpServer::new(move || { + App::new() + .app_data(app_state_admin.clone()) + .app_data(web::PayloadConfig::new( + (app_state_admin.config.max_file_size.clone() as usize) * 1024 * 1024, + )) + .wrap(Logger::default()) + .service( + web::scope("/v1") + .route("/newImage", web::post().to(handlers::new_image_json)) + .route("/newImageMp", web::post().to(handlers::new_image_multipart)) + .route( + "/job/{job_id}/status", + web::get().to(handlers::get_job_status), + ) + .route( + "/job/{job_id}/ws", + web::get().to(handlers::websocket_job_status), + ) + .route("/job/{job_id}/sse", web::get().to(handlers::sse_job_status)), + ) + }) + .bind((SERVER_HOST, ADMIN_SERVER_PORT))? + .run(); + + let image_server = HttpServer::new(move || { + App::new() + .app_data(web::Data::new(app_state.config.clone())) + .route("/{image_id}", web::get().to(handlers::get_file)) + .route("/{image_id}/", web::get().to(handlers::get_file)) + .route( + "/{image_id}/{resolution}", + web::get().to(handlers::get_file), + ) + }) + .bind((SERVER_HOST, IMAGE_SERVER_PORT))? + .run(); + + tracing::info!( + "Starting main API server on http://localhost:{}", + ADMIN_SERVER_PORT + ); + tracing::info!( + "Starting image server on http://localhost:{}", + IMAGE_SERVER_PORT + ); + + match try_join(main_server, image_server).await { + Ok((_, _)) => { + tracing::info!("Main server finished"); + tracing::info!("Image server finished"); + Ok(()) + } + Err(e) => { + tracing::error!("A server encountered an error: {}", e); + Err(e) + } + } +} diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..e27206b --- /dev/null +++ b/src/models.rs @@ -0,0 +1,90 @@ +use dashmap::DashMap; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::sync::mpsc; +use uuid::Uuid; + +#[derive(Deserialize, Debug)] +pub struct NewImageRequest { + pub id: String, + pub image: String, +} + +#[derive(Deserialize, Debug)] +pub struct ImageIdQuery { + #[serde(rename = "imageId")] + pub image_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum TargetFormat { + WebP, + Avif, + Jpeg, +} + +impl TargetFormat { + pub fn extension(&self) -> &'static str { + match self { + TargetFormat::WebP => "webp", + TargetFormat::Avif => "avif", + TargetFormat::Jpeg => "jpg", + } + } + + pub fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "webp" => Ok(TargetFormat::WebP), + "avif" => Ok(TargetFormat::Avif), + "jpg" | "jpeg" => Ok(TargetFormat::Jpeg), + _ => Err(format!("Invalid target format: {}", s)), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum JobStatus { + Queued, + Processing, + Completed, + Failed(String), +} + +#[derive(Debug, Clone, Serialize)] +pub struct JobState { + pub status: JobStatus, +} + +#[derive(Debug)] +pub struct ImageJob { + pub job_id: Uuid, + pub image_id: String, + pub image_data: Vec, + //pub original_filename: Option, + // TODO: move to configuration + pub target_resolutions: Vec<(u32, u32)>, + pub target_formats: Vec, +} + +#[derive(Serialize)] +pub struct JobQueuedResponse { + pub job_id: Uuid, + pub image_id: String, + pub status: String, +} + +#[derive(Clone)] +pub struct AppState { + pub job_sender: mpsc::Sender, + pub job_statuses: Arc>, + pub config: Arc, +} + +#[derive(Debug)] +pub struct Config { + pub api_key: String, + pub api_key_header: String, + pub convert_to_res: Vec<(u32, u32)>, + pub max_file_size: u32, + pub target_formats: Vec, +} diff --git a/src/worker.rs b/src/worker.rs new file mode 100644 index 0000000..bfd2402 --- /dev/null +++ b/src/worker.rs @@ -0,0 +1,84 @@ +use crate::image_processing; +use crate::models::{ImageJob, JobState, JobStatus}; +use tokio::sync::mpsc; + +use dashmap::DashMap; +use std::sync::Arc; +use uuid::Uuid; + +// Zmieniamy sygnaturę, aby przyjmował AppState +pub async fn run_worker( + mut receiver: mpsc::Receiver, + // Potrzebujemy dostępu do mapy statusów + job_statuses: Arc>, +) { + tracing::info!("Image processing worker started"); + while let Some(job) = receiver.recv().await { + let job_id = job.job_id; + let image_id = job.image_id.clone(); + tracing::info!("Processing job: {} for image_id: {}", job_id, image_id); + + // --- ZMIANA: Aktualizuj status na Processing --- + job_statuses.entry(job_id).and_modify(|state| { + state.status = JobStatus::Processing; + // state.started_at = Some(chrono::Utc::now()); // Opcjonalnie + tracing::debug!("Job {} status updated to Processing", job_id); + }); + // --------------------------------------------- + + let statuses_clone = job_statuses.clone(); + + let result = tokio::task::spawn_blocking(move || { + image_processing::process_image(job) + }) + .await; + + // --- ZMIANA: Aktualizuj status po zakończeniu --- + match result { + Ok(Ok(())) => { + statuses_clone.entry(job_id).and_modify(|state| { + state.status = JobStatus::Completed; + // state.finished_at = Some(chrono::Utc::now()); // Opcjonalnie + tracing::info!("Job {} status updated to Completed", job_id); + }); + tracing::info!( + "Job {} (image_id: {}) completed successfully", + job_id, + image_id + ); + } + Ok(Err(e)) => { + // Błąd zwrócony przez process_image (anyhow::Error) + let error_msg = format!("{}", e); // Konwertuj błąd na string + statuses_clone.entry(job_id).and_modify(|state| { + state.status = JobStatus::Failed(error_msg.clone()); // Klonujemy string + // state.finished_at = Some(chrono::Utc::now()); // Opcjonalnie + tracing::error!("Job {} status updated to Failed", job_id); + }); + tracing::error!( + "Job {} (image_id: {}) failed during processing: {}", + job_id, + image_id, + error_msg + ); + } + Err(e) => { + // Błąd paniki w spawn_blocking + let error_msg = format!("Task panicked: {}", e); + statuses_clone.entry(job_id).and_modify(|state| { + state.status = JobStatus::Failed(error_msg.clone()); + // state.finished_at = Some(chrono::Utc::now()); // Opcjonalnie + tracing::error!("Job {} status updated to Failed due to panic", job_id); + }); + tracing::error!( + "Job {} (image_id: {}) panicked during processing: {}", + job_id, + image_id, + error_msg + ); + } + } + // ------------------------------------------------- + } + tracing::info!("Image processing worker stopped"); +} diff --git a/testImages/rest/admin-apis.http b/testImages/rest/admin-apis.http new file mode 100644 index 0000000..3627246 --- /dev/null +++ b/testImages/rest/admin-apis.http @@ -0,0 +1,18 @@ +@base_url = http://127.0.0.1:9324 + +POST {{base_url}}/v1/newImage HTTP/1.1 +Content-Type: application/json +Accept: */* +cdn_api_key: 10000000-0000-0000-0000-000000000000 + +{ + "id": "56774", + "image": "iVBORw0KGgoAAAANSUhEUgAAAV4AAACWBAMAAABkyf1EAAAAG1BMVEXMzMyWlpacnJyqqqrFxcWxsbGjo6O3t7e+vr6He3KoAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAEcElEQVR4nO2aTW/bRhCGh18ij1zKknMkbbf2UXITIEeyMhIfRaF1exQLA/JRclslRykO+rs7s7s0VwytNmhJtsA8gHZEcox9PTs7uysQgGEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmGYr2OWRK/ReIKI8Zt7Hb19wTcQ0uTkGh13bQupcw7gPOvdo12/5CzNtNR7xLUtNtT3CGBQ6g3InjY720pvofUec22LJPr8PhEp2OMPyI40PdwWUdronCu9yQpdPx53bQlfLKnfOVhlnDYRBXve4Ov+IZTeMgdedm0NR+xoXJeQvdJ3CvziykSukwil16W/Oe7aGjIjqc/9ib4jQlJy0uArtN4A0+cvXFvDkmUJ47sJ1Y1ATLDNVXZkNPIepQzxy1ki9fqiwbUj/I+64zxWNzyZnPuhvohJ9K70VvXBixpcu2SAHU+Xd9EKdEJDNpYP3AQr3bQSpPQ6Y6/4dl1z7ZDbArsszjA7L0g7ibB0CDcidUWVoErvIMKZh2Xs0LUzcLW6V5NfiUgNEbaYmAVL6bXl0nJRc+1S72ua/D/cTjGPlQj7eUqd7A096rYlRjdPYlhz7VIvxpVG3cemDKF+WAwLY/6XelOZKTXXzsC4xvDjjtSN6kHLhLke6PrwM8h1raf40qjrGO7H9aTEbduucjS04ZrYU/4iuS5Z2Hdt0rvCLFdmLEXcU30AGddST62o+sLcf5l6k7CP+ru4pLYqX/VFyxbm/utQbx/r22ZEbTb2f5I2kns1Y1OQR8ZyofX+TjJxj1Rz7QQVnf1QzR26Oth0ueJVYcRP6ZUPac/Rx/5M6ixO1dhSrT3Y1DpiYmx3tF4ZUdpz9LD/dSg9PXES0LB71BwcGjKROuV28lnvnv7HHJsezheBGH5+X2CfSfRbMKW+5aGs3JFjMrjGibJc0S7TJzqjHrh2hDybj9XRXNZa89Aro55XBdbW5wti2c/5WJ7jJ1RolVUn/HWpb0I58Tziup6Rx7Dm2hnbRP1GM9PW/NFmQ4PtVRVN63Wvxfmu5sowDMMwDMMwDMMwDMMwDMMwDMMwzL+CpT//F/6beoV8zb2Jmt4Qryx6lTUCsENQ75HOkhXAO3EPVgyQtKtUy3C/e+FJg17Zjnew1Xrdb9InbG4WqfUAftG+WhLwPVyfg536+MU7m4C1CMk4ZznpXZzDYI1PDL2nS1hpvc5cNd7E2sJg05Fe7/7d3Fln8Cvc3bwB616auxsKl4WPghjemHrDqyDWeu1UNW5s2btPnSQ75oOdunEwWazfwgVG0kqluYCM9OIjWOGnfA2b9G4Ha63XKpvQ8perTvTifJNhi6+WMWmi7smEZf6G8MmhlyGq+NqP8GV84TLuJr7UIQVx+bDEoEpRZIz42gs40OuN4Mv8hXzelV7KX1isH+ewTWckikyVv+CfHuqVF7I16gN0VKypX6wPsE+zFPzkinolU9UH8OMGvSpnZqKsv13p/RsMun6X5x/y2LeAr8O66lsBwzBMP/wJfyGq8pgBk6IAAAAASUVORK5CYII=" +} + +### + +GET {{base_url}}/v1/job/8874efc3-4fc2-48ec-97d2-e0f577cdc267/status HTTP/1.1 +Content-Type: application/json +Accept: */* +cdn_api_key: 10000000-0000-0000-0000-000000000000 \ No newline at end of file diff --git a/testImages/rest/public-apis.http b/testImages/rest/public-apis.http new file mode 100644 index 0000000..cce3a86 --- /dev/null +++ b/testImages/rest/public-apis.http @@ -0,0 +1,8 @@ +@base_url = http://localhost:9555 + +GET {{base_url}}/56774/342x324 HTTP/1.1 +Accept: image/avif,image/webp,image/apng + + +### +GET {{base_url}}/56774 HTTP/1.1 diff --git a/utils/logger/logger.go b/utils/logger/logger.go deleted file mode 100644 index 67c774a..0000000 --- a/utils/logger/logger.go +++ /dev/null @@ -1,37 +0,0 @@ -package logger - -import ( - "io" - "log" - "os" -) - -var ( - logFlags = log.Ldate | log.Ltime - LoggerWritter io.Writer = os.Stderr - WarningLogger *log.Logger = log.New(LoggerWritter, "WARNING: ", logFlags) - InfoLogger *log.Logger = log.New(LoggerWritter, "INFO: ", logFlags) - ErrorLogger *log.Logger = log.New(LoggerWritter, "ERROR: ", logFlags) -) - -const logPath = "/var/log/eic" - -func StartLogger() { - if _, err := os.Stat(logPath); os.IsNotExist(err) { - errMkDir := os.Mkdir(logPath, 0777) - if errMkDir != nil { - ErrorLogger.Println("Cannot create log's directory " + errMkDir.Error()) - } - } - - file, err := os.OpenFile(logPath+"/app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) - if err != nil { - ErrorLogger.Println("Cannot save log file " + err.Error()) - } else { - LoggerWritter = io.MultiWriter(os.Stdout, file) - } - - InfoLogger = log.New(LoggerWritter, "INFO: ", logFlags) - WarningLogger = log.New(LoggerWritter, "WARNING: ", logFlags) - ErrorLogger = log.New(LoggerWritter, "ERROR: ", logFlags) -}