diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index f6dbfaff7..342647327 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,5 +1,49 @@ # Changelog + +## 0.0.6 (2024-09-06) + +### Added + +- ✨ Broader image support for file serving ([#419](https://github.com/stumpapp/stump/issues/419)) [[38ffe05](https://github.com/stumpapp/stump/commit/38ffe0511e413eb7d8388a21ae3a49989287f3a8)] +- 👷‍♂️ Use `buildah` in docker CI workflows ([#425](https://github.com/stumpapp/stump/issues/425)) [[afe389c](https://github.com/stumpapp/stump/commit/afe389ca8259d02fce7cb9415e2fe4de22aeae66)] +- 👷‍♂️ Switch to `podman` and introduce better caching ([#416](https://github.com/stumpapp/stump/issues/416)) [[6a16235](https://github.com/stumpapp/stump/commit/6a16235bd4f1552d285c39e4b150c7b5a189ad32)] +- ✨ Avif file serving support ([#417](https://github.com/stumpapp/stump/issues/417)) [[7df8738](https://github.com/stumpapp/stump/commit/7df8738a8c9e6bc60f25468ee29d39f9e4d53d69)] +- 👷‍♂️ Restore self-hosted runner for certain CI tasks ([#405](https://github.com/stumpapp/stump/issues/405)) [[4cdf471](https://github.com/stumpapp/stump/commit/4cdf471f0672c1a2d71c7ed6543412e1fd6f6fcd)] +- ✨ Support AVIF image format ([#385](https://github.com/stumpapp/stump/issues/385)) [[09ca1a9](https://github.com/stumpapp/stump/commit/09ca1a9a543789e3bf6abf2b8325c3ee2513be59)] + +### Changed + +- ⬇️ Downgrade `zip` crate to `1.1.3` ([#435](https://github.com/stumpapp/stump/issues/435)) [[fe35819](https://github.com/stumpapp/stump/commit/fe35819dc01208673dada3e485fca2acd2f65ba1)] +- ⬆️ Update dependencies ([#433](https://github.com/stumpapp/stump/issues/433)) [[ebbb8e5](https://github.com/stumpapp/stump/commit/ebbb8e509237d4ed4aa5f0b83300dc8e77212da7)] +- 📌 Pin `@icons-pack/react-simple-icons` to `9.1.0` ([#403](https://github.com/stumpapp/stump/issues/403)) [[6bc7792](https://github.com/stumpapp/stump/commit/6bc7792e9bbee9b55b34c961dda6dd59113e5d96)] +- ⏪ Revert AVIF image support ([#409](https://github.com/stumpapp/stump/issues/409)) [[3ea2804](https://github.com/stumpapp/stump/commit/3ea2804ee822b641966e02ea72a4873238447ecb)] +- ♻️ Implement a config macro to simplify `stump_config.rs` ([#397](https://github.com/stumpapp/stump/issues/397)) [[3a480f3](https://github.com/stumpapp/stump/commit/3a480f3441ed890aad9823f2e05cbaf5ca00a38a)] +- 💄 Improve light theme palette and refactor design system ([#402](https://github.com/stumpapp/stump/issues/402)) [[8102321](https://github.com/stumpapp/stump/commit/8102321fd35c5b0145bd44d0f754da126d3b99ee)] + +### Fixed + +- 💚 Fix multi-tag docker builds [[d663f5c](https://github.com/stumpapp/stump/commit/d663f5c621ca2c252381459345890bfdabc01565)] +- 🐛 Fix max-depth for collection-priority libraries ([#432](https://github.com/stumpapp/stump/issues/432)) [[558721a](https://github.com/stumpapp/stump/commit/558721a8f07739c38d8d828a4589442f8f148fad)] +- 🐛 Fix emailer form validation and update endpoint ([#430](https://github.com/stumpapp/stump/issues/430)) [[ce173a2](https://github.com/stumpapp/stump/commit/ce173a21d83fbf4666097ae84373cad3442f5c46)] +- 💚 Fix docker build push ([#424](https://github.com/stumpapp/stump/issues/424)) [[bef46c8](https://github.com/stumpapp/stump/commit/bef46c809a75fa3dd8559dbee763b9d4ec50804b)] +- 🐛 Fix regression in scanner for root-level series ([#423](https://github.com/stumpapp/stump/issues/423)) [[03ff5e0](https://github.com/stumpapp/stump/commit/03ff5e0faf0e44e58714788cceeb92bb42458e19)] +- 🐛 Fix OPDS thumbnail endpoint ([#414](https://github.com/stumpapp/stump/issues/414)) [[77b4635](https://github.com/stumpapp/stump/commit/77b4635c4ddb7fb1035929d4c0da86751a777f2f)] +- 🐛 Fix book titles in entity cards ([#412](https://github.com/stumpapp/stump/issues/412)) [[96ea14f](https://github.com/stumpapp/stump/commit/96ea14f95b5ee0ecea4d3e9c796e5f063e4bc149)] +- 💚 Fix `runs-on` for docker build workflows ([#407](https://github.com/stumpapp/stump/issues/407)) [[afd42e9](https://github.com/stumpapp/stump/commit/afd42e92dd6f4983ea76fdeb678413c07ccfdc6e)] +- 🐛 Fix invalid SQL for library stats query ([#401](https://github.com/stumpapp/stump/issues/401)) [[362e85f](https://github.com/stumpapp/stump/commit/362e85f304704d6add7f06c55a6dbda8887d360f)] + +### Miscellaneous + +- 🌐 Update translations ([#434](https://github.com/stumpapp/stump/issues/434)) [[ceb2aa1](https://github.com/stumpapp/stump/commit/ceb2aa1621a46b7629c4decb829424f97ab480bf)] +- 🌐 Update translations ([#418](https://github.com/stumpapp/stump/issues/418)) [[c1d20fe](https://github.com/stumpapp/stump/commit/c1d20feee75e42900bf7fe9acca2e42105460411)] +- 📝 Update docs for broken demo ([#421](https://github.com/stumpapp/stump/issues/421)) [[a5fa8f5](https://github.com/stumpapp/stump/commit/a5fa8f53bd890a7ef488cc6c5b4e1e61108ea9f3)] +- 🌐 New translations ([#411](https://github.com/stumpapp/stump/issues/411)) [[7d6b704](https://github.com/stumpapp/stump/commit/7d6b704c60f2bd6c6a444bb22df5e06e0c98717c)] +- 📝 Add binary links to documentation site ([#404](https://github.com/stumpapp/stump/issues/404)) [[17ef33b](https://github.com/stumpapp/stump/commit/17ef33b6e5a517031d460d421e56ff80e73088df)] +- 🔨 Fix docker `dav1d` shared library linking ([#408](https://github.com/stumpapp/stump/issues/408)) [[a8ef2a4](https://github.com/stumpapp/stump/commit/a8ef2a48d895e3beb007d31c401a71d41e9f3977)] +- 🌐 New translations ([#398](https://github.com/stumpapp/stump/issues/398)) [[862ff63](https://github.com/stumpapp/stump/commit/862ff63a8cdac700170d26ffc264591767954ae6)] + + ## 0.0.5 (2024-08-14) diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index d0f82a78b..3d64dfcc3 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -17,7 +17,7 @@ inputs: tags: description: 'List of tags to assigned to the image' default: 'nightly' - platforms: + archs: description: 'List of platforms to build' required: true discord-webhook: @@ -33,17 +33,30 @@ runs: - name: Format tags run: | - echo "TAGS=$(echo ${{ inputs.tags }} | sed -e 's/,/,aaronleopold\/stump:/g' | sed -e 's/^/aaronleopold\/stump:/')" >> $GITHUB_ENV + echo "FORMATTED_TAGS=$(echo ${{ inputs.tags }} | sed -e 's/,/ /g')" >> $GITHUB_ENV + echo "LOCAL_IMAGES=$(echo ${{ inputs.tags }} | sed -e 's/,/,localhost\/stump:/g' | sed -e 's/^/localhost\/stump:/')" >> $GITHUB_ENV + shell: bash + + - name: Sanity check + run: | + echo "TAGS=${{ inputs.tags }}" + echo "FORMATTED_TAGS=${{ env.FORMATTED_TAGS }}" + echo "LOCAL_IMAGES=${{ env.LOCAL_IMAGES }}" + echo "GIT_REV=${{ env.GIT_REV }}" shell: bash - name: Setup rust uses: ./.github/actions/setup-rust + with: + # Note: until some sort of local caching is implemented, we don't want to cache dependencies + # because the network overhead is too high and eats up lots of time + cache-dependencies: ${{ runner.environment != 'self-hosted' }} # We only need QEMU when an arm* platform is targeted - name: Check QEMU requirement id: check-qemu run: | - if [[ ${{ inputs.platforms }} == *"arm"* ]]; then + if [[ ${{ inputs.archs }} == *"arm"* ]]; then echo "SETUP_QEMU=1" >> $GITHUB_OUTPUT else echo "SETUP_QEMU=0" >> $GITHUB_OUTPUT @@ -53,30 +66,54 @@ runs: - name: Set up QEMU uses: docker/setup-qemu-action@v2 if: ${{ steps.check-qemu.outputs.SETUP_QEMU == '1' }} + with: + platforms: linux/arm64 + + - name: Install podman and buildah + if: runner.environment != 'self-hosted' + run: | + sudo apt-get update + sudo apt-get install -y podman buildah + shell: bash + + - name: Remove existing images + run: | + podman rmi ${{ env.LOCAL_IMAGES }} || true + shell: bash - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + - name: Run buildah build + id: build + uses: redhat-actions/buildah-build@v2 + with: + image: stump + tags: ${{ env.FORMATTED_TAGS }} + archs: ${{ inputs.archs }} + build-args: | + GIT_REV=${{ env.GIT_REV }} + RUN_PRISMA_GENERATE=false + containerfiles: | + ./docker/Dockerfile + + - name: Echo build outputs + run: | + echo "${{ toJSON(steps.build.outputs) }}" + shell: bash - - name: Login to Docker Hub - uses: docker/login-action@v2 - # if both inputs are empty, we assume we're running on a fork and don't need to login - if: ${{ inputs.username != '' && inputs.password != '' }} + - name: Push to registry + id: push + if: ${{ success() && inputs.push == 'true' }} + uses: redhat-actions/push-to-registry@v2 with: + image: ${{ steps.build.outputs.image }} + tags: ${{ steps.build.outputs.tags }} username: ${{ inputs.username }} password: ${{ inputs.password }} + registry: docker.io/aaronleopold - - name: Run buildx build - uses: docker/build-push-action@v4 - with: - context: . - build-args: | - "GIT_REV=${{ env.GIT_REV }}" - "TAGS=${{ env.TAGS }}" - file: docker/Dockerfile - platforms: ${{ inputs.platforms }} - load: ${{ inputs.load }} - push: ${{ inputs.push }} - tags: ${{ env.TAGS }} + - name: Echo push outputs + run: | + echo "${{ toJSON(steps.push.outputs) }}" + shell: bash - name: Discord notification if: ${{ success() && inputs.push == 'true' && inputs.discord-webhook != '' }} @@ -84,4 +121,4 @@ runs: DISCORD_WEBHOOK: ${{ inputs.discord-webhook }} uses: 'Ilshidur/action-discord@0.3.2' with: - args: 'Successfully pushed the following image tags to registry: ${{ env.TAGS }}' + args: 'Successfully pushed the following image tags to registry: ${{ steps.build.outputs.tags }}' diff --git a/.github/actions/build-web/action.yml b/.github/actions/build-web/action.yml index 73d54961d..02322f4a3 100644 --- a/.github/actions/build-web/action.yml +++ b/.github/actions/build-web/action.yml @@ -7,27 +7,8 @@ runs: - name: Checkout project uses: actions/checkout@v3 - - uses: actions/setup-node@v4 - with: - node-version: '20.0.0' - - - name: Install yarn - shell: bash - run: npm install -g yarn - - - uses: actions/setup-node@v4 - with: - node-version: '20.0.0' - cache: 'yarn' - - - name: Install yarn - shell: bash - run: npm install -g yarn - - - name: Install dependencies - shell: bash - run: yarn install - working-directory: apps/web + - name: Setup Node and Yarn + uses: ./.github/actions/setup-yarn - name: Build app shell: bash diff --git a/.github/actions/coverage/action.yml b/.github/actions/coverage/action.yml new file mode 100644 index 000000000..b58426084 --- /dev/null +++ b/.github/actions/coverage/action.yml @@ -0,0 +1,34 @@ +name: Coverage CI +description: Run code coverage checks + +inputs: + token: + description: 'Codecov token' + required: true + cache-dependencies: + description: 'Whether to cache dependencies. This does not affect the Prisma client cache.' + required: false + default: 'true' + +runs: + using: composite + steps: + - name: Setup rust + uses: ./.github/actions/setup-rust + with: + cache-dependencies: ${{ inputs.cache-dependencies }} + + - name: Install cargo-llvm-cov + run: cargo install cargo-llvm-cov + shell: bash + + - name: Generate code coverage data + shell: bash + run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ inputs.token }} + files: lcov.info + fail_ci_if_error: true diff --git a/.github/actions/setup-dav1d/action.yml b/.github/actions/setup-dav1d/action.yml new file mode 100644 index 000000000..1064d0653 --- /dev/null +++ b/.github/actions/setup-dav1d/action.yml @@ -0,0 +1,86 @@ +name: Dav1d +description: Compile and install dav1d + +runs: + using: composite + steps: + # Linux Section + - name: Install nasm + if: runner.os == 'Linux' + uses: ilammy/setup-nasm@v1 + + - name: Install Python 3.9 + if: runner.os == 'Linux' + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install pip packages + if: runner.os == 'Linux' + shell: bash + run: | + pip install -U pip + pip install -U wheel setuptools + pip install -U meson ninja + + - name: Build dav1d + if: runner.os == 'Linux' + env: + DAV1D_DIR: dav1d_dir + LIB_PATH: lib/x86_64-linux-gnu + shell: bash + run: | + git clone --branch 1.3.0 --depth 1 https://code.videolan.org/videolan/dav1d.git + cd dav1d + meson build -Dprefix=$HOME/$DAV1D_DIR -Denable_tools=false -Denable_examples=false --buildtype release + ninja -C build + ninja -C build install + echo "PKG_CONFIG_PATH=$HOME/$DAV1D_DIR/$LIB_PATH/pkgconfig" >> $GITHUB_ENV + echo "LD_LIBRARY_PATH=$HOME/$DAV1D_DIR/$LIB_PATH" >> $GITHUB_ENV + + # Windows setup + - name: Install nasm + if: runner.os == 'Windows' + uses: ilammy/setup-nasm@v1 + + - name: Install Python 3.9 + if: runner.os == 'Windows' + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install pip packages + if: runner.os == 'Windows' + shell: powershell + run: | + pip install -U pip + pip install -U wheel setuptools + pip install -U meson ninja + + - name: Setting up environment + if: runner.os == 'Windows' + shell: bash + run: | + echo "PKG_CONFIG=c:\build\bin\pkg-config.exe" >> $GITHUB_ENV + echo "PKG_CONFIG_PATH=C:\build\lib\pkgconfig" >> $GITHUB_ENV + echo "C:\build\bin" >> $GITHUB_PATH + + - name: Build pkg-config + if: runner.os == 'Windows' + shell: powershell + run: | + git clone --branch meson-glib-subproject --depth 1 https://gitlab.freedesktop.org/tpm/pkg-config.git + cd pkg-config + meson build -Dprefix=C:\build --buildtype release + ninja -C build + ninja -C build install + + - name: Build dav1d + if: runner.os == 'Windows' + shell: powershell + run: | + git clone --branch 1.3.0 --depth 1 https://code.videolan.org/videolan/dav1d.git + cd dav1d + meson build -Dprefix=C:\build -Denable_tools=false -Denable_examples=false --buildtype release + ninja -C build + ninja -C build install diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index 03ae2875f..f99c1fb2f 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -1,6 +1,12 @@ name: Setup system dependencies description: Install system dependencies and setup cache +inputs: + cache-dependencies: + description: 'Cache dependencies. This does not affect the Prisma client cache.' + required: false + default: 'true' + runs: using: composite steps: @@ -13,11 +19,13 @@ runs: components: rustfmt, clippy - name: Configure environment - if: ${{ runner.name != 'oromei-ubuntu' && runner.os != 'Windows' }} + if: ${{ runner.environment != 'self-hosted' && runner.os != 'Windows' }} shell: bash run: CHECK_NODE=0 CHECK_CARGO=0 DEV_SETUP=0 ./scripts/system-setup.sh + # See https://github.com/Swatinem/rust-cache/issues/194 - name: Cache Rust Dependencies + if: ${{ inputs.cache-dependencies == 'true' }} uses: Swatinem/rust-cache@v2 with: shared-key: stump-rust-cache @@ -37,8 +45,7 @@ runs: - name: Save Prisma client id: cache-prisma-save if: ${{ steps.cache-prisma-restore.outputs.cache-hit != 'true' }} - uses: actions/cache/save@v3 + uses: actions/cache/save@v4 with: path: core/src/prisma.rs key: ${{ runner.os }}-prisma-${{ hashFiles('**/schema.prisma') }} - restore-keys: ${{ runner.os }}-prisma-${{ hashFiles('**/schema.prisma') }} diff --git a/.github/actions/setup-yarn/action.yml b/.github/actions/setup-yarn/action.yml new file mode 100644 index 000000000..c335da9a6 --- /dev/null +++ b/.github/actions/setup-yarn/action.yml @@ -0,0 +1,31 @@ +name: Setup Yarn +description: Install Yarn and setup cache + +inputs: + cache-dependencies: + description: 'Cache dependencies. This does not affect the Prisma client cache.' + required: false + default: 'true' + +runs: + using: composite + steps: + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: '20.0.0' + + - name: Install yarn + shell: bash + run: npm install -g yarn + + - name: Setup node (yarn cache) + if: ${{ inputs.cache-dependencies == 'true' }} + uses: actions/setup-node@v4 + with: + node-version: '20.0.0' + cache: 'yarn' + + - name: Install dependencies + run: yarn install + shell: bash diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3c7111657..14df86a6e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,32 +10,41 @@ jobs: code-changes-check: runs-on: [ubuntu-22.04] outputs: - changes: ${{steps.filter.outputs.changes}} + changes: ${{ steps.filter.outputs.changes }} + frontend-changed: ${{ steps.filter.outputs.frontend == 'true' }} + rust-changed: ${{ steps.filter.outputs.rust == 'true' }} steps: - uses: actions/checkout@v4 - uses: dorny/paths-filter@v2 id: filter with: filters: | - apps: ./apps/** - crates: ./crates/** + frontend: + - './apps/web/**' + - './apps/desktop/src/**' + - './packages/**' + expo: + - './apps/expo/**' + rust: + - './apps/server/**' + - './apps/desktop/src-tauri/**' + - './core/**' + - './crates/**' docker: ./docker/** - packages: ./packages/** check-rust: needs: code-changes-check - if: "!contains(github.event.pull_request.head.ref, 'release/v') && needs.code-changes-check.outputs.changes != '[]'" + if: "!contains(github.event.pull_request.head.ref, 'release/v') && needs.code-changes-check.outputs.rust-changed == 'true'" name: Rust checks - runs-on: [ubuntu-22.04] - strategy: - matrix: - os: [ubuntu-22.04, windows-latest] + runs-on: [self-hosted] steps: - name: Checkout repository uses: actions/checkout@v3 - name: Setup rust uses: ./.github/actions/setup-rust + with: + cache-dependencies: false - name: Run cargo checks run: | @@ -45,50 +54,40 @@ jobs: - name: Run tests run: cargo test - # TODO: move coverage to a separate job instead of conditionally running it - # on the ubuntu-22.04 runner as a workaround for only running it once - - name: Install cargo-llvm-cov - if: matrix.os == 'ubuntu-22.04' - run: cargo install cargo-llvm-cov + - name: Run codegen + run: cargo codegen -- --skip-prisma + - name: Verify up-to-date bindings + run: | + git diff --exit-code || \ + (echo "Please generate updated bindings with \`cargo codegen -- --skip-prisma\`" \ + && exit 1) - - name: Generate code coverage data - if: matrix.os == 'ubuntu-22.04' - run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info + check-coverage: + if: "!contains(github.event.pull_request.head.ref, 'release/v') && needs.code-changes-check.outputs.rust-changed == 'true'" + name: Coverage checks + needs: [code-changes-check, check-rust] + runs-on: [self-hosted] + steps: + - name: Checkout repository + uses: actions/checkout@v3 - - name: Upload coverage to Codecov - if: matrix.os == 'ubuntu-22.04' - uses: codecov/codecov-action@v4 + - name: Run coverage + uses: ./.github/actions/coverage with: token: ${{ secrets.CODECOV_TOKEN }} - files: lcov.info - fail_ci_if_error: true + cache-dependencies: false check-typescript: needs: code-changes-check - if: "!contains(github.event.pull_request.head.ref, 'release/v') && needs.code-changes-check.outputs.changes != '[]'" + if: "!contains(github.event.pull_request.head.ref, 'release/v') && needs.code-changes-check.outputs.frontend-changed == 'true'" name: TypeScript checks runs-on: [ubuntu-22.04] steps: - name: Checkout repository uses: actions/checkout@v3 - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: '20.0.0' - - - name: Install yarn - shell: bash - run: npm install -g yarn - - - name: Setup node (yarn cache) - uses: actions/setup-node@v4 - with: - node-version: '20.0.0' - cache: 'yarn' - - - name: Install dependencies - run: yarn install + - name: Setup Node and Yarn + uses: ./.github/actions/setup-yarn - name: Run TypeScript lints run: yarn lint diff --git a/.github/workflows/experimental.yml b/.github/workflows/experimental.yml index 3cc510d13..cc1ed6840 100644 --- a/.github/workflows/experimental.yml +++ b/.github/workflows/experimental.yml @@ -29,14 +29,18 @@ jobs: base: 'experimental' filters: | apps: ./apps/** + core: ./core/** crates: ./crates/** docker: ./docker/** packages: ./packages/** + workflow: + - './.github/workflows/experimental.yml' + - './.github/actions/**' nightly-docker-build: needs: code-changes-check name: Build docker image - runs-on: [ubuntu-22.04] + runs-on: [self-hosted] if: ${{ needs.code-changes-check.outputs.changes != '[]' }} steps: - name: Checkout repository @@ -50,6 +54,5 @@ jobs: tags: 'experimental' load: ${{ github.event_name == 'pull_request' }} push: ${{ github.event_name == 'push' }} - platforms: 'linux/amd64' - # platforms: 'linux/arm64/v8,linux/arm/v7,linux/amd64' + archs: 'linux/amd64' discord-webhook: ${{ secrets.DISCORD_WEBHOOK }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 0732ab04c..b185f719d 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -29,15 +29,19 @@ jobs: base: 'develop' filters: | apps: ./apps/** + core: ./core/** crates: ./crates/** docker: ./docker/** packages: ./packages/** + workflow: + - './.github/workflows/nightly.yml' + - './.github/actions/**' nightly-docker-build: needs: code-changes-check if: "!contains(github.event.pull_request.head.ref, 'release/v') && needs.code-changes-check.outputs.changes != '[]'" name: Build docker image - runs-on: [ubuntu-22.04] + runs-on: [self-hosted] steps: - name: Checkout repository uses: actions/checkout@v3 @@ -50,9 +54,9 @@ jobs: echo "PUSH=${{ github.event_name == 'push' }}" >> $GITHUB_ENV if [[ ${{ github.event_name }} == 'pull_request' ]]; then - echo "PLATFORMS=linux/amd64" >> $GITHUB_ENV + echo "ARCHS=amd64" >> $GITHUB_ENV else - echo "PLATFORMS=linux/arm64/v8,linux/amd64" >> $GITHUB_ENV + echo "ARCHS=arm64,amd64" >> $GITHUB_ENV fi - name: Setup and build docker image @@ -63,5 +67,5 @@ jobs: tags: 'nightly' load: ${{ env.LOAD }} push: ${{ env.PUSH }} - platforms: ${{ env.PLATFORMS }} + archs: ${{ env.ARCHS }} discord-webhook: ${{ secrets.DISCORD_WEBHOOK }} diff --git a/.github/workflows/release.yml b/.github/workflows/release_docker.yml similarity index 88% rename from .github/workflows/release.yml rename to .github/workflows/release_docker.yml index 369bcab26..64d584eac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release_docker.yml @@ -1,4 +1,4 @@ -name: Release CI +name: Docker Release CI # This workflow triggers when a PR is merged into `main`, but jobs have conditions to only run when: # - A PR is closed, merged into `main`, from a branch that matches the pattern `release/v*.*.*` @@ -46,7 +46,7 @@ jobs: outputs: load: ${{ steps.configure.outputs.load }} push: ${{ steps.configure.outputs.push }} - platforms: ${{ steps.configure.outputs.platforms }} + archs: ${{ steps.configure.outputs.archs }} steps: - name: Configure environment id: configure @@ -54,23 +54,23 @@ jobs: if [[ ${{ github.event.pull_request.merged }} == true ]]; then echo "push=true" >> $GITHUB_OUTPUT echo "load=false" >> $GITHUB_OUTPUT - echo "platforms=linux/arm64/v8,linux/amd64" >> $GITHUB_OUTPUT + echo "archs=arm64,amd64" >> $GITHUB_OUTPUT else echo "push=false" >> $GITHUB_OUTPUT echo "load=true" >> $GITHUB_OUTPUT - echo "platforms=linux/amd64" >> $GITHUB_OUTPUT + echo "archs=amd64" >> $GITHUB_OUTPUT fi - name: Print the configuration run: | - echo "Platforms: ${{ steps.configure.outputs.platforms }}" + echo "Archs: ${{ steps.configure.outputs.archs }}" echo "Load: ${{ steps.configure.outputs.load }}" echo "Push: ${{ steps.configure.outputs.push }}" build-stable-docker: if: contains(github.event.pull_request.head.ref, 'release/v') name: Build docker image - runs-on: [ubuntu-22.04] + runs-on: [self-hosted] needs: [parse-semver, push-or-load] steps: - name: Checkout repository @@ -84,5 +84,5 @@ jobs: tags: ${{ needs.parse-semver.outputs.tags }} load: ${{ needs.push-or-load.outputs.load }} push: ${{ needs.push-or-load.outputs.push }} - platforms: ${{ needs.push-or-load.outputs.platforms }} + archs: ${{ needs.push-or-load.outputs.archs }} discord-webhook: ${{ secrets.DISCORD_WEBHOOK }} diff --git a/.gitignore b/.gitignore index 0a7ee8b96..8825b4b71 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ logs/ build/ coverage/ +!.github/actions/coverage cjs/ **/*/dist/**/* !**/*/dist/.placeholder diff --git a/Cargo.lock b/Cargo.lock index be101afcd..35244eb42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -176,9 +176,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" @@ -237,7 +237,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -293,18 +293,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -328,7 +328,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.3", + "system-deps 6.2.2", ] [[package]] @@ -426,7 +426,7 @@ dependencies = [ "serde_path_to_error", "serde_urlencoded", "sha1", - "sync_wrapper", + "sync_wrapper 0.1.2", "tokio", "tokio-tungstenite", "tower", @@ -481,7 +481,7 @@ dependencies = [ "heck 0.4.0", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -535,9 +535,9 @@ checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bcrypt" @@ -545,7 +545,7 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e65938ed058ef47d92cf8b346cc76ef48984572ade631927e9937b5ffc7662c7" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "blowfish", "getrandom 0.2.11", "subtle", @@ -590,9 +590,9 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", - "syn 2.0.58", + "syn 2.0.75", "which", ] @@ -755,9 +755,9 @@ checksum = "102087e286b4677862ea56cf8fc58bb2cdfa8725c40ffb80fe3a008eb7f2fc83" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder-lite" @@ -771,27 +771,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.11+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "cairo-rs" version = "0.15.12" @@ -813,7 +792,7 @@ checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" dependencies = [ "glib-sys", "libc", - "system-deps 6.0.3", + "system-deps 6.2.2", ] [[package]] @@ -896,7 +875,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" dependencies = [ "byteorder", "fnv", - "uuid 1.8.0", + "uuid 1.10.0", ] [[package]] @@ -910,11 +889,12 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.11.0" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0357a6402b295ca3a86bc148e84df46c02e41f41fef186bda662557ef6328aa" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", + "target-lexicon", ] [[package]] @@ -925,9 +905,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -935,7 +915,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.0", + "windows-targets 0.52.6", ] [[package]] @@ -998,9 +978,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" dependencies = [ "clap_builder", "clap_derive", @@ -1008,9 +988,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstream", "anstyle", @@ -1020,14 +1000,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -1038,7 +1018,7 @@ checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "cli" -version = "0.0.2" +version = "0.0.6" dependencies = [ "bcrypt", "clap", @@ -1084,7 +1064,7 @@ dependencies = [ [[package]] name = "codegen" -version = "0.0.5" +version = "0.0.6" [[package]] name = "codespan-reporting" @@ -1291,21 +1271,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.4.2" @@ -1668,12 +1633,6 @@ dependencies = [ "adler32", ] -[[package]] -name = "deflate64" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" - [[package]] name = "deranged" version = "0.3.10" @@ -1692,7 +1651,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -1748,7 +1707,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.3", "crypto-common", - "subtle", ] [[package]] @@ -1840,17 +1798,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" -[[package]] -name = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.58", -] - [[package]] name = "dml" version = "0.1.0" @@ -1866,7 +1813,7 @@ dependencies = [ "schema-ast", "serde", "serde_json", - "uuid 1.8.0", + "uuid 1.10.0", ] [[package]] @@ -1934,7 +1881,7 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "email" -version = "0.0.5" +version = "0.0.6" dependencies = [ "handlebars", "lettre", @@ -2307,7 +2254,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -2394,7 +2341,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.3", + "system-deps 6.2.2", ] [[package]] @@ -2411,7 +2358,7 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps 6.0.3", + "system-deps 6.2.2", ] [[package]] @@ -2423,7 +2370,7 @@ dependencies = [ "gdk-sys", "glib-sys", "libc", - "system-deps 6.0.3", + "system-deps 6.2.2", "x11", ] @@ -2516,7 +2463,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.3", + "system-deps 6.2.2", "winapi", ] @@ -2562,7 +2509,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" dependencies = [ "libc", - "system-deps 6.0.3", + "system-deps 6.2.2", ] [[package]] @@ -2601,7 +2548,7 @@ checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" dependencies = [ "glib-sys", "libc", - "system-deps 6.0.3", + "system-deps 6.2.2", ] [[package]] @@ -2652,7 +2599,7 @@ dependencies = [ "gobject-sys", "libc", "pango-sys", - "system-deps 6.0.3", + "system-deps 6.2.2", ] [[package]] @@ -2822,9 +2769,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -2832,15 +2779,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "hostname" version = "0.3.1" @@ -2991,19 +2929,20 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.26.0" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", "hyper 1.2.0", "hyper-util", - "rustls", + "rustls 0.23.7", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tower-service", + "webpki-roots", ] [[package]] @@ -3283,7 +3222,7 @@ dependencies = [ "async-trait", "dotenv", "lettre", - "reqwest 0.12.3", + "reqwest 0.12.7", "serde_json", "thiserror", "tokio", @@ -3297,7 +3236,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -3327,7 +3266,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "rustix", "windows-sys 0.48.0", ] @@ -3342,28 +3281,28 @@ dependencies = [ ] [[package]] -name = "iter_tools" -version = "0.1.4" +name = "itertools" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531cafdc99b3b3252bb32f5620e61d56b19415efc19900b12d1b2e7483854897" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ - "itertools 0.10.5", + "either", ] [[package]] name = "itertools" -version = "0.10.5" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -3579,11 +3518,11 @@ dependencies = [ "nom", "percent-encoding", "quoted_printable", - "rustls", + "rustls 0.22.2", "rustls-pemfile", "socket2 0.5.5", "tokio", - "tokio-rustls", + "tokio-rustls 0.25.0", "tracing", "url", "webpki-roots", @@ -3661,12 +3600,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.0" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-targets 0.52.6", ] [[package]] @@ -3733,9 +3672,9 @@ checksum = "f9275e0933cf8bb20f008924c0cb07a0692fe54d8064996520bf998de9eb79aa" [[package]] name = "local-ip-address" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136ef34e18462b17bf39a7826f8f3bbc223341f8e83822beb8b77db9a3d49696" +checksum = "b435d7dd476416a905f9634dff8c330cee8d3168fdd1fbd472a17d1a75c00c3e" dependencies = [ "libc", "neli", @@ -3754,12 +3693,6 @@ dependencies = [ "serde", ] -[[package]] -name = "lockfree-object-pool" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" - [[package]] name = "log" version = "0.4.21" @@ -3812,16 +3745,6 @@ dependencies = [ "url", ] -[[package]] -name = "lzma-rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" -dependencies = [ - "byteorder", - "crc", -] - [[package]] name = "mac" version = "0.1.1" @@ -4123,6 +4046,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + [[package]] name = "mobc" version = "0.7.3" @@ -4288,7 +4223,7 @@ dependencies = [ "inotify", "kqueue", "libc", - "mio", + "mio 0.8.10", "walkdir", "windows-sys 0.45.0", ] @@ -4373,7 +4308,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -4675,7 +4610,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.0.3", + "system-deps 6.2.2", ] [[package]] @@ -4756,16 +4691,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac", -] - [[package]] name = "pdf" version = "0.8.1" @@ -4805,9 +4730,9 @@ dependencies = [ [[package]] name = "pdfium-render" -version = "0.8.16" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b19ea0c0c7816b6a29fae48f4c5dece59814394e272a7e80ea8f8e8eb37a6e" +checksum = "9cf21aa9bd11aa175e8755e0dbc613affe885e149c4b3ee4ac6d2c183260e727" dependencies = [ "bindgen", "bitflags 2.4.0", @@ -4817,9 +4742,9 @@ dependencies = [ "console_error_panic_hook", "console_log", "image 0.25.2", - "iter_tools", + "itertools 0.13.0", "js-sys", - "libloading 0.8.0", + "libloading 0.8.5", "log", "maybe-owned", "once_cell", @@ -5145,7 +5070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -5200,7 +5125,7 @@ dependencies = [ "tokio", "tracing", "user-facing-errors", - "uuid 1.8.0", + "uuid 1.10.0", ] [[package]] @@ -5283,7 +5208,7 @@ dependencies = [ "regex", "serde", "serde_json", - "uuid 1.8.0", + "uuid 1.10.0", ] [[package]] @@ -5329,9 +5254,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -5352,7 +5277,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" dependencies = [ "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -5430,7 +5355,7 @@ dependencies = [ "tracing", "tracing-core", "url", - "uuid 1.8.0", + "uuid 1.10.0", ] [[package]] @@ -5466,7 +5391,7 @@ dependencies = [ "serde_json", "thiserror", "user-facing-errors", - "uuid 1.8.0", + "uuid 1.10.0", ] [[package]] @@ -5510,7 +5435,7 @@ dependencies = [ "tracing-subscriber", "url", "user-facing-errors", - "uuid 1.8.0", + "uuid 1.10.0", ] [[package]] @@ -5545,6 +5470,54 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.0.0", + "rustls 0.23.7", + "socket2 0.5.5", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" +dependencies = [ + "bytes", + "rand 0.8.5", + "ring", + "rustc-hash 2.0.0", + "rustls 0.23.7", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" +dependencies = [ + "libc", + "once_cell", + "socket2 0.5.5", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.36" @@ -5709,7 +5682,7 @@ dependencies = [ "rust_hawktracer", "rustc_version 0.4.0", "simd_helpers", - "system-deps 6.0.3", + "system-deps 6.2.2", "thiserror", "v_frame", "wasm-bindgen", @@ -5744,7 +5717,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "simd_helpers", - "system-deps 6.0.3", + "system-deps 6.2.2", "thiserror", "v_frame", ] @@ -5834,9 +5807,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick 1.1.2", "memchr", @@ -5942,16 +5915,16 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg 0.50.0", + "winreg", ] [[package]] name = "reqwest" -version = "0.12.3" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "bytes", "futures-core", "futures-util", @@ -5968,22 +5941,23 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls", + "quinn", + "rustls 0.23.7", "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.1", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "webpki-roots", - "winreg 0.52.0", + "windows-registry", ] [[package]] @@ -6089,7 +6063,7 @@ dependencies = [ "quote", "rust-embed-utils", "shellexpand", - "syn 2.0.58", + "syn 2.0.75", "walkdir", ] @@ -6137,6 +6111,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc_version" version = "0.3.3" @@ -6182,13 +6162,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "rustls-pki-types", ] @@ -6368,9 +6362,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.197" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] @@ -6409,13 +6403,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -6433,12 +6427,13 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "indexmap 2.2.6", "itoa 1.0.5", + "memchr", "ryu", "serde", ] @@ -6478,9 +6473,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -6645,12 +6640,6 @@ 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" @@ -6877,7 +6866,7 @@ dependencies = [ "tracing-futures", "url", "user-facing-errors", - "uuid 1.8.0", + "uuid 1.10.0", ] [[package]] @@ -6909,7 +6898,7 @@ dependencies = [ "tracing-futures", "tracing-opentelemetry", "user-facing-errors", - "uuid 1.8.0", + "uuid 1.10.0", ] [[package]] @@ -7041,9 +7030,23 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "stump-config-gen" +version = "0.0.6" +dependencies = [ + "itertools 0.13.0", + "proc-macro2", + "quote", + "serde", + "syn 2.0.75", + "temp-env", + "thiserror", + "toml 0.8.19", +] + [[package]] name = "stump_core" -version = "0.0.5" +version = "0.0.6" dependencies = [ "alphanumeric-sort", "async-channel", @@ -7059,7 +7062,7 @@ dependencies = [ "globset", "image 0.25.2", "infer 0.16.0", - "itertools 0.12.1", + "itertools 0.13.0", "libc", "pdf", "pdfium-render", @@ -7073,11 +7076,12 @@ dependencies = [ "serde_json", "simple_crypt", "specta", + "stump-config-gen", "temp-env", "tempfile", "thiserror", "tokio", - "toml 0.8.12", + "toml 0.8.19", "tracing", "tracing-appender", "tracing-subscriber", @@ -7085,11 +7089,11 @@ dependencies = [ "unrar", "urlencoding", "utoipa", - "uuid 1.8.0", + "uuid 1.10.0", "walkdir", "webp", "xml-rs", - "zip 2.1.6", + "zip 1.1.3", ] [[package]] @@ -7107,14 +7111,14 @@ dependencies = [ [[package]] name = "stump_server" -version = "0.0.5" +version = "0.0.6" dependencies = [ "async-stream", "async-trait", "axum", "axum-extra", "axum-macros", - "base64 0.22.0", + "base64 0.22.1", "bcrypt", "chrono", "cli", @@ -7126,7 +7130,7 @@ dependencies = [ "openssl", "prisma-client-rust", "rand 0.8.5", - "reqwest 0.12.3", + "reqwest 0.12.7", "serde", "serde-untagged", "serde_json", @@ -7165,9 +7169,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.58" +version = "2.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" dependencies = [ "proc-macro2", "quote", @@ -7180,6 +7184,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.12.6" @@ -7228,15 +7241,15 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.0.3" +version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ - "cfg-expr 0.11.0", - "heck 0.4.0", + "cfg-expr 0.15.8", + "heck 0.5.0", "pkg-config", - "toml 0.5.10", - "version-compare 0.1.1", + "toml 0.8.19", + "version-compare 0.2.0", ] [[package]] @@ -7279,7 +7292,7 @@ dependencies = [ "scopeguard", "serde", "unicode-segmentation", - "uuid 1.8.0", + "uuid 1.10.0", "windows 0.39.0", "windows-implement", "x11-dl", @@ -7296,6 +7309,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tauri" version = "1.2.5" @@ -7343,7 +7362,7 @@ dependencies = [ "thiserror", "tokio", "url", - "uuid 1.8.0", + "uuid 1.10.0", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -7387,7 +7406,7 @@ dependencies = [ "tauri-utils", "thiserror", "time", - "uuid 1.8.0", + "uuid 1.10.0", "walkdir", ] @@ -7421,7 +7440,7 @@ dependencies = [ "tauri-utils", "thiserror", "url", - "uuid 1.8.0", + "uuid 1.10.0", "webview2-com", "windows 0.39.0", ] @@ -7440,7 +7459,7 @@ dependencies = [ "tauri-runtime", "tauri-utils", "url", - "uuid 1.8.0", + "uuid 1.10.0", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -7507,14 +7526,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -7556,22 +7576,22 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -7661,32 +7681,31 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.37.0" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.2", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2 0.5.5", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -7705,7 +7724,18 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls", + "rustls 0.22.2", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.7", "rustls-pki-types", "tokio", ] @@ -7747,9 +7777,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", @@ -7759,18 +7789,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap 2.2.6", "serde", @@ -7890,7 +7920,7 @@ dependencies = [ "tower-cookies", "tower-layer", "tower-service", - "uuid 1.8.0", + "uuid 1.10.0", ] [[package]] @@ -7925,7 +7955,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -8140,9 +8170,9 @@ dependencies = [ [[package]] name = "unrar" -version = "0.5.3" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4994bfae776d5c2ee22493a00742c77d58bfa5adbe10febe83d1ba7aff2ebdc" +checksum = "c99d6a7735a222f2119ca4572e713fb468b5c3a17a4fb90b3cac3e28a8680c29" dependencies = [ "bitflags 1.3.2", "regex", @@ -8152,9 +8182,9 @@ dependencies = [ [[package]] name = "unrar_sys" -version = "0.3.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f691c507016acf0a56fae074981ce30f13f8b035c8f80aa878f41905d96e390" +checksum = "3f8325103479fffa0e31b41fd11267446b355037115ae184a63a9fd3f192f3da" dependencies = [ "cc", "libc", @@ -8261,7 +8291,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -8291,9 +8321,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.11", "serde", @@ -8339,9 +8369,9 @@ checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" [[package]] name = "version-compare" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "version_check" @@ -8513,7 +8543,7 @@ dependencies = [ "pango-sys", "pkg-config", "soup2-sys", - "system-deps 6.0.3", + "system-deps 6.2.2", ] [[package]] @@ -8689,6 +8719,36 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.42.0" @@ -8728,7 +8788,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -8763,17 +8832,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -8796,9 +8866,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -8826,9 +8896,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -8856,9 +8926,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +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" @@ -8886,9 +8962,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -8916,9 +8992,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -8934,9 +9010,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -8964,15 +9040,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] @@ -8987,16 +9063,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winres" version = "0.1.12" @@ -9076,9 +9142,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" +checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601" [[package]] name = "zerocopy" @@ -9097,7 +9163,7 @@ checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.75", ] [[package]] @@ -9105,20 +9171,6 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.58", -] [[package]] name = "zip" @@ -9134,73 +9186,15 @@ dependencies = [ [[package]] name = "zip" -version = "2.1.6" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40dd8c92efc296286ce1fbd16657c5dbefff44f1b4ca01cc5f517d8b7b3d3e2e" +checksum = "2e6cb8909b2e8e6733c9ef67d35be1a27105644d362aafb5f8b2ba395727adf6" dependencies = [ - "aes", "arbitrary 1.3.2", - "bzip2", - "constant_time_eq", + "byteorder", "crc32fast", "crossbeam-utils", - "deflate64", - "displaydoc", "flate2", - "hmac", - "indexmap 2.2.6", - "lzma-rs", - "memchr", - "pbkdf2", - "rand 0.8.5", - "sha1", - "thiserror", - "time", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zopfli" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" -dependencies = [ - "bumpalo", - "crc32fast", - "lockfree-object-pool", - "log", - "once_cell", - "simd-adler32", -] - -[[package]] -name = "zstd" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" -dependencies = [ - "cc", - "pkg-config", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 44ee159b7..4f0334f85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,15 +8,17 @@ members = [ ] [workspace.package] -version = "0.0.5" +version = "0.0.6" rust-version = "1.79.0" [workspace.dependencies] -async-trait = "0.1.80" +async-trait = "0.1.81" async-stream = "0.3.5" bcrypt = "0.15.1" +chrono = "0.4.38" futures = "0.3.30" futures-util = "0.3.30" +itertools = "0.13.0" lettre = { version = "0.11.4", default-features = false, features = [ "builder", "hostname", @@ -37,14 +39,14 @@ prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client- "mocking" ], default-features = false } rand = "0.8.5" -reqwest = { version = "0.12.3", default-features = false, features = [ "json", "rustls-tls" ] } -serde = { version = "1.0.197", features = ["derive"] } -serde_json = "1.0.115" +reqwest = { version = "0.12.7", default-features = false, features = [ "json", "rustls-tls" ] } +serde = { version = "1.0.209", features = ["derive"] } +serde_json = "1.0.127" simple_crypt = "0.2.3" specta = "1.0.5" -tempfile = "3.10.1" -thiserror = "1.0.58" -tokio = { version = "1.37.0", features = [ +tempfile = "3.12.0" +thiserror = "1.0.63" +tokio = { version = "1.40.0", features = [ # Provides sender/reciever channels "sync", # Tells the Tokio runtime to use the multi-thread scheduler. @@ -52,5 +54,6 @@ tokio = { version = "1.37.0", features = [ # Allows handling shutdown signals (e.g., ctrl+c) "signal", ] } +toml = "0.8.19" tracing = "0.1.40" urlencoding = "2.1.3" diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e2691904b..c93f7809f 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@stump/desktop", - "version": "0.0.5", + "version": "0.0.6", "description": "", "license": "MIT", "scripts": { diff --git a/apps/expo/package.json b/apps/expo/package.json index 279f51853..1f51371a8 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -51,6 +51,6 @@ "tailwindcss": "3.3.2" }, "name": "@stump/mobile", - "version": "0.0.5", + "version": "0.0.6", "private": true } diff --git a/apps/expo/src/components/book/BookListItem.tsx b/apps/expo/src/components/book/BookListItem.tsx index 42b8300d2..f5502e113 100644 --- a/apps/expo/src/components/book/BookListItem.tsx +++ b/apps/expo/src/components/book/BookListItem.tsx @@ -24,7 +24,7 @@ export const BookListItem = React.memo(({ book, navigate }: BookListItemProps) = style={{ height: 50, objectFit: 'scale-down', width: 50 / (3 / 2) }} /> - {book.name} + {book.metadata?.title || book.name} )) diff --git a/apps/expo/src/components/primitives/Text.tsx b/apps/expo/src/components/primitives/Text.tsx index 5b01feacd..ca9b51a92 100644 --- a/apps/expo/src/components/primitives/Text.tsx +++ b/apps/expo/src/components/primitives/Text.tsx @@ -47,7 +47,7 @@ export const textVariants = cva('', { // danger: 'text-red-600 dark:text-red-400', // default: 'dark:text-white text-black', // label: - // 'font-medium leading-none text-contrast-200 peer-disabled:cursor-not-allowed peer-disabled:opacity-70', + // 'font-medium leading-none text-foreground-subtle peer-disabled:cursor-not-allowed peer-disabled:opacity-70', // muted: 'text-gray-400 dark:text-gray-300', // }, }, diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index c41878bd4..b47da282b 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -17,14 +17,14 @@ axum-extra = { version = "0.5.0", features = [ "spa", "query" ] } -base64 = "0.22.0" +base64 = "0.22.1" bcrypt = { workspace = true } cli = { path = "../../crates/cli" } futures-util = { workspace = true } hyper = "0.14.27" jsonwebtoken = "9.3.0" linemux = { git = "https://github.com/jmagnuson/linemux.git", rev = "acaafc602afac5d7a9cd3e087dafc937cac1e364" } -local-ip-address = "0.6.1" +local-ip-address = "0.6.2" prisma-client-rust = { workspace = true } rand = "0.8.5" reqwest = { workspace = true } @@ -52,7 +52,7 @@ utoipa = { version = "3.5.0", features = ["axum_extras"] } utoipa-swagger-ui = { version = "3.1.5", features = ["axum"] } [build-dependencies] -chrono = "0.4.37" +chrono = { workspace = true } [target.aarch64-unknown-linux-musl.dependencies] openssl = { version = "0.10.61", features = ["vendored"] } diff --git a/apps/server/package.json b/apps/server/package.json index 62de6e5c4..63456396e 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,7 +1,7 @@ { "name": "@stump/server", "private": true, - "version": "0.0.5", + "version": "0.0.6", "scripts": { "lint": "cargo clippy --package stump_server -- -D warnings", "format": "cargo fmt --package stump_server", diff --git a/apps/server/src/middleware/auth.rs b/apps/server/src/middleware/auth.rs index 1216fcfe2..57dc5b3e1 100644 --- a/apps/server/src/middleware/auth.rs +++ b/apps/server/src/middleware/auth.rs @@ -180,7 +180,7 @@ async fn handle_basic_auth( /// /// ## Example: /// -/// ```rust +/// ```no_run /// use axum::{Router, middleware::{from_extractor, from_extractor_with_state}}; /// use stump_core::{Ctx, config::StumpConfig}; /// use stump_server::{ diff --git a/apps/server/src/routers/api/v1/emailer.rs b/apps/server/src/routers/api/v1/emailer.rs index e3eab8164..b9466a30a 100644 --- a/apps/server/src/routers/api/v1/emailer.rs +++ b/apps/server/src/routers/api/v1/emailer.rs @@ -191,11 +191,16 @@ async fn create_emailer( config.sender_email, config.sender_display_name, config.username, - config.encrypted_password, + config + .encrypted_password + .ok_or(APIError::InternalServerError( + "Encrypted password is missing".to_string(), + ))?, config.smtp_host.to_string(), config.smtp_port.into(), vec![ emailer::is_primary::set(payload.is_primary), + emailer::tls_enabled::set(config.tls_enabled), emailer::max_attachment_size_bytes::set(config.max_attachment_size_bytes), ], ) @@ -230,22 +235,33 @@ async fn update_emailer( ) -> APIResult> { enforce_session_permissions(&session, &[UserPermission::EmailerManage])?; + if payload.config.password.is_some() { + tracing::warn!(?id, "The password for the emailer is being updated!"); + } + let client = &ctx.db; let config = EmailerConfig::from_client_config(payload.config, &ctx).await?; let updated_emailer = client .emailer() .update( emailer::id::equals(id), - vec![ - emailer::name::set(payload.name), - emailer::sender_email::set(config.sender_email), - emailer::sender_display_name::set(config.sender_display_name), - emailer::username::set(config.username), - emailer::encrypted_password::set(config.encrypted_password), - emailer::smtp_host::set(config.smtp_host.to_string()), - emailer::smtp_port::set(config.smtp_port.into()), - emailer::max_attachment_size_bytes::set(config.max_attachment_size_bytes), - ], + chain_optional_iter( + [ + emailer::name::set(payload.name), + emailer::sender_email::set(config.sender_email), + emailer::sender_display_name::set(config.sender_display_name), + emailer::username::set(config.username), + emailer::smtp_host::set(config.smtp_host.to_string()), + emailer::smtp_port::set(config.smtp_port.into()), + emailer::tls_enabled::set(config.tls_enabled), + emailer::max_attachment_size_bytes::set( + config.max_attachment_size_bytes, + ), + ], + [config + .encrypted_password + .map(emailer::encrypted_password::set)], + ), ) .exec() .await?; diff --git a/apps/server/src/routers/api/v1/library.rs b/apps/server/src/routers/api/v1/library.rs index 5a9d81dc5..e9f3bbd5b 100644 --- a/apps/server/src/routers/api/v1/library.rs +++ b/apps/server/src/routers/api/v1/library.rs @@ -18,8 +18,8 @@ use stump_core::{ db::{ entity::{ library_series_ids_media_ids_include, library_thumbnails_deletion_include, - FileStatus, Library, LibraryOptions, LibraryScanMode, LibraryStats, Media, - Series, Tag, User, UserPermission, + macros::series_or_library_thumbnail, FileStatus, Library, LibraryOptions, + LibraryScanMode, LibraryStats, Media, Series, Tag, User, UserPermission, }, query::pagination::{Pageable, Pagination, PaginationQuery}, PrismaCountTrait, @@ -293,7 +293,6 @@ pub struct LibraryStatsParams { all_users: bool, } -// TODO(historical-read-session): refactor query #[utoipa::path( get, path = "/api/v1/libraries/stats", @@ -331,27 +330,13 @@ async fn get_libraries_stats( ), progress_counts AS ( SELECT - IFNULL(SUM( - CASE WHEN rp.is_completed THEN - 1 - ELSE - 0 - END), - 0) AS completed_books, - IFNULL(SUM( - CASE WHEN rp.is_completed THEN - 0 - ELSE - 1 - END), - 0) AS in_progress_books + COUNT(frs.id) AS completed_books, + COUNT(rs.id) AS in_progress_books FROM media m - INNER JOIN read_progresses rp ON rp.media_id = m.id - WHERE - rp.is_completed AND ( - {} IS TRUE OR rp.user_id = {} - ) + LEFT JOIN finished_reading_sessions frs ON frs.media_id = m.id + LEFT JOIN reading_sessions rs ON rs.media_id = m.id + WHERE {} IS TRUE OR (rs.user_id = {} OR frs.user_id = {}) ) SELECT * @@ -360,6 +345,7 @@ async fn get_libraries_stats( INNER JOIN progress_counts; "#, PrismaValue::Boolean(params.all_users), + PrismaValue::String(user.id.clone()), PrismaValue::String(user.id) )) .exec() @@ -579,29 +565,27 @@ async fn get_library_media( } pub(crate) fn get_library_thumbnail( - library: &library::Data, - first_series: &series::Data, - first_book: &media::Data, + id: &str, + first_series: series_or_library_thumbnail::Data, + first_book: Option, image_format: Option, config: &StumpConfig, ) -> APIResult<(ContentType, Vec)> { - let library_id = library.id.clone(); - if let Some(format) = image_format.clone() { let extension = format.extension(); let path = config .get_thumbnails_dir() - .join(format!("{}.{}", library_id, extension)); + .join(format!("{}.{}", id, extension)); if path.exists() { - tracing::trace!(?path, library_id, "Found generated library thumbnail"); + tracing::trace!(?path, id, "Found generated library thumbnail"); return Ok((ContentType::from(format), read_entire_file(path)?)); } } - if let Some(path) = get_unknown_thumnail(&library_id, config.get_thumbnails_dir()) { - tracing::debug!(path = ?path, library_id, "Found library thumbnail that does not align with config"); + if let Some(path) = get_unknown_thumnail(id, config.get_thumbnails_dir()) { + tracing::debug!(path = ?path, id, "Found library thumbnail that does not align with config"); let FileParts { extension, .. } = path.file_parts(); return Ok(( ContentType::from_extension(extension.as_str()), @@ -609,7 +593,7 @@ pub(crate) fn get_library_thumbnail( )); } - get_series_thumbnail(first_series, first_book, image_format, config) + get_series_thumbnail(&first_series.id, first_book, image_format, config) } // TODO: ImageResponse for utoipa @@ -638,55 +622,40 @@ async fn get_library_thumbnail_handler( let user = get_session_user(&session)?; let age_restriction = user.age_restriction.as_ref(); + let series_filters = chain_optional_iter( + [ + series::library_id::equals(Some(id.clone())), + series::library::is(vec![library_not_hidden_from_user_filter(&user)]), + ], + [age_restriction + .map(|ar| apply_series_age_restriction(ar.age, ar.restrict_on_unset))], + ); + let book_filters = chain_optional_iter( + [], + [age_restriction + .as_ref() + .map(|ar| apply_media_age_restriction(ar.age, ar.restrict_on_unset))], + ); + let first_series = db .series() - // Find the first series in the library which satisfies the age restriction - .find_first(chain_optional_iter( - [ - series::library_id::equals(Some(id.clone())), - series::library::is(vec![library_not_hidden_from_user_filter(&user)]), - ], - [age_restriction - .map(|ar| apply_series_age_restriction(ar.age, ar.restrict_on_unset))], - )) - .with( - // Then load the first media in that series which satisfies the age restriction - series::media::fetch(chain_optional_iter( - [], - [age_restriction - .as_ref() - .map(|ar| apply_media_age_restriction(ar.age, ar.restrict_on_unset))], - )) - .take(1) - .order_by(media::name::order(Direction::Asc)), - ) - .with(series::library::fetch().with(library::library_options::fetch())) + .find_first(series_filters) .order_by(series::name::order(Direction::Asc)) + .select(series_or_library_thumbnail::select(book_filters)) .exec() .await? .ok_or(APIError::NotFound("Library has no series".to_string()))?; + let first_book = first_series.media.first().cloned(); - let library = first_series - .library()? - .ok_or(APIError::Unknown(String::from("Failed to load library")))?; - let image_format = library - .library_options() - .map(LibraryOptions::from)? - .thumbnail_config - .map(|config| config.format); - - let first_book = first_series.media()?.first().ok_or(APIError::NotFound( - "Library has no media to get thumbnail from".to_string(), - ))?; - - get_library_thumbnail( - library, - &first_series, - first_book, - image_format, - &ctx.config, - ) - .map(ImageResponse::from) + let library_options = first_series + .library + .as_ref() + .map(|l| l.library_options.clone()) + .map(LibraryOptions::from); + let image_format = library_options.and_then(|o| o.thumbnail_config.map(|c| c.format)); + + get_library_thumbnail(&id, first_series, first_book, image_format, &ctx.config) + .map(ImageResponse::from) } #[derive(Deserialize, ToSchema, specta::Type)] @@ -1673,27 +1642,13 @@ async fn get_library_stats( ), progress_counts AS ( SELECT - IFNULL(SUM( - CASE WHEN rp.is_completed THEN - 1 - ELSE - 0 - END), - 0) AS completed_books, - IFNULL(SUM( - CASE WHEN rp.is_completed THEN - 0 - ELSE - 1 - END), - 0) AS in_progress_books + COUNT(frs.id) AS completed_books, + COUNT(rs.id) AS in_progress_books FROM media m - INNER JOIN read_progresses rp ON rp.media_id = m.id - WHERE - rp.is_completed AND ( - {} IS TRUE OR rp.user_id = {} - ) + LEFT JOIN finished_reading_sessions frs ON frs.media_id = m.id + LEFT JOIN reading_sessions rs ON rs.media_id = m.id + WHERE {} IS TRUE OR (rs.user_id = {} OR frs.user_id = {}) ) SELECT * @@ -1703,6 +1658,7 @@ async fn get_library_stats( "#, PrismaValue::String(id), PrismaValue::Boolean(params.all_users), + PrismaValue::String(user.id.clone()), PrismaValue::String(user.id) )) .exec() @@ -1710,7 +1666,7 @@ async fn get_library_stats( .into_iter() .next() .ok_or(APIError::InternalServerError( - "Failed to compute stats for libraries".to_string(), + "Failed to compute stats for library".to_string(), ))?; Ok(Json(stats)) diff --git a/apps/server/src/routers/api/v1/media.rs b/apps/server/src/routers/api/v1/media.rs index ff8165073..80afd2c81 100644 --- a/apps/server/src/routers/api/v1/media.rs +++ b/apps/server/src/routers/api/v1/media.rs @@ -20,7 +20,8 @@ use stump_core::{ db::{ entity::{ macros::{ - finished_reading_session_with_book_pages, reading_session_with_book_pages, + finished_reading_session_with_book_pages, media_thumbnail, + reading_session_with_book_pages, }, ActiveReadingSession, FinishedReadingSession, LibraryOptions, Media, PageDimension, PageDimensionsEntity, ProgressUpdateReturn, User, @@ -40,7 +41,7 @@ use stump_core::{ read_entire_file, ContentType, FileParts, PathUtils, }, prisma::{ - active_reading_session, finished_reading_session, library, library_options, + active_reading_session, finished_reading_session, library, media::{self, OrderByParam as MediaOrderByParam, WhereParam}, media_metadata, series, series_metadata, tag, user, PrismaClient, }, @@ -204,8 +205,6 @@ pub(crate) fn apply_media_library_not_hidden_for_user_filter( ])])] } -// FIXME: hidden libraries introduced a bug here, need to fix! - pub(crate) fn apply_media_filters_for_user( filters: MediaFilter, user: &User, @@ -974,50 +973,26 @@ pub(crate) async fn get_media_thumbnail_by_id( [age_restrictions], ); - let result = db - ._transaction() - .run(|client| async move { - let book = client - .media() - .find_first(where_params) - .order_by(media::name::order(Direction::Asc)) - .with(media::series::fetch()) - .exec() - .await?; + let book = db + .media() + .find_first(where_params) + .select(media_thumbnail::select()) + .exec() + .await? + .ok_or_else(|| APIError::NotFound("Book not found".to_string()))?; - if let Some(book) = book { - let library_id = match book.series() { - Ok(Some(series)) => Some(series.library_id.clone()), - _ => None, - } - .flatten(); + let library_options = book + .series + .and_then(|s| s.library.map(|l| l.library_options)) + .map(LibraryOptions::from); + let image_format = library_options.and_then(|o| o.thumbnail_config.map(|c| c.format)); - client - .library_options() - .find_first(vec![library_options::library_id::equals(library_id)]) - .exec() - .await - .map(|options| (Some(book), options.map(LibraryOptions::from))) - } else { - Ok((None, None)) - } - }) - .await?; - tracing::trace!(?result, "get_media_thumbnail transaction completed"); - - match result { - (Some(book), Some(options)) => get_media_thumbnail( - &book, - options.thumbnail_config.map(|config| config.format), - config, - ), - (Some(book), None) => get_media_thumbnail(&book, None, config), - _ => Err(APIError::NotFound(String::from("Media not found"))), - } + get_media_thumbnail(&book.id, &book.path, image_format, config) } pub(crate) fn get_media_thumbnail( - media: &media::Data, + id: &str, + path: &str, target_format: Option, config: &StumpConfig, ) -> APIResult<(ContentType, Vec)> { @@ -1025,19 +1000,19 @@ pub(crate) fn get_media_thumbnail( let extension = format.extension(); let thumbnail_path = config .get_thumbnails_dir() - .join(format!("{}.{}", media.id, extension)); + .join(format!("{}.{}", id, extension)); if thumbnail_path.exists() { - tracing::trace!(path = ?thumbnail_path, media_id = ?media.id, "Found generated media thumbnail"); + tracing::trace!(path = ?thumbnail_path, media_id = id, "Found generated media thumbnail"); return Ok((ContentType::from(format), read_entire_file(thumbnail_path)?)); } - } else if let Some(path) = - get_unknown_thumnail(&media.id, config.get_thumbnails_dir()) - { + } + + if let Some(path) = get_unknown_thumnail(id, config.get_thumbnails_dir()) { // If there exists a file that starts with the media id in the thumbnails dir, // then return it. This might happen if a user manually regenerates thumbnails // via the API without updating the thumbnail config... - tracing::debug!(path = ?path, media_id = ?media.id, "Found media thumbnail that does not align with config"); + tracing::debug!(path = ?path, media_id = id, "Found media thumbnail that does not align with config"); let FileParts { extension, .. } = path.file_parts(); return Ok(( ContentType::from_extension(extension.as_str()), @@ -1045,7 +1020,7 @@ pub(crate) fn get_media_thumbnail( )); } - Ok(get_page(media.path.as_str(), 1, config)?) + Ok(get_page(path, 1, config)?) } // TODO: ImageResponse as body type diff --git a/apps/server/src/routers/api/v1/series.rs b/apps/server/src/routers/api/v1/series.rs index fc156aaad..708a7d78d 100644 --- a/apps/server/src/routers/api/v1/series.rs +++ b/apps/server/src/routers/api/v1/series.rs @@ -14,8 +14,10 @@ use stump_core::{ config::StumpConfig, db::{ entity::{ - macros::finished_reading_session_series_complete, LibraryOptions, Media, - Series, User, UserPermission, + macros::{ + finished_reading_session_series_complete, series_or_library_thumbnail, + }, + LibraryOptions, Media, Series, User, UserPermission, }, query::{ ordering::QueryOrder, @@ -505,24 +507,25 @@ async fn get_recently_added_series_handler( } pub(crate) fn get_series_thumbnail( - series: &series::Data, - first_book: &media::Data, + id: &str, + first_book: Option, image_format: Option, config: &StumpConfig, ) -> APIResult<(ContentType, Vec)> { let thumbnails_dir = config.get_thumbnails_dir(); - let series_id = series.id.clone(); if let Some(format) = image_format.clone() { let extension = format.extension(); - let path = thumbnails_dir.join(format!("{}.{}", series_id, extension)); + let path = thumbnails_dir.join(format!("{}.{}", id, extension)); if path.exists() { - tracing::trace!(?path, series_id, "Found generated series thumbnail"); + tracing::trace!(?path, id, "Found generated series thumbnail"); return Ok((ContentType::from(format), read_entire_file(path)?)); } - } else if let Some(path) = get_unknown_thumnail(&series_id, thumbnails_dir) { - tracing::debug!(path = ?path, series_id, "Found series thumbnail that does not align with config"); + } + + if let Some(path) = get_unknown_thumnail(id, thumbnails_dir) { + tracing::debug!(path = ?path, id, "Found series thumbnail that does not align with config"); let FileParts { extension, .. } = path.file_parts(); return Ok(( ContentType::from_extension(extension.as_str()), @@ -530,7 +533,13 @@ pub(crate) fn get_series_thumbnail( )); } - get_media_thumbnail(first_book, image_format, config) + if let Some(first_book) = first_book { + get_media_thumbnail(&first_book.id, &first_book.path, image_format, config) + } else { + Err(APIError::NotFound( + "Series does not have a thumbnail".to_string(), + )) + } } // TODO: ImageResponse type for body @@ -560,52 +569,36 @@ async fn get_series_thumbnail_handler( let age_restriction = user.age_restriction.as_ref(); let series_age_restriction = age_restriction .map(|ar| apply_series_age_restriction(ar.age, ar.restrict_on_unset)); - let where_params = chain_optional_iter( + let series_filters = chain_optional_iter( [series::id::equals(id.clone())] .into_iter() .chain(apply_series_library_not_hidden_for_user_filter(&user)) .collect::>(), [series_age_restriction], ); + let book_filters = chain_optional_iter( + [], + [age_restriction + .map(|ar| apply_media_age_restriction(ar.age, ar.restrict_on_unset))], + ); let series = db .series() - // Find the first series in the library which satisfies the age restriction - .find_first(where_params) - .with( - // Then load the first media in that series which satisfies the age restriction - series::media::fetch(chain_optional_iter( - [], - [age_restriction - .map(|ar| apply_media_age_restriction(ar.age, ar.restrict_on_unset))], - )) - .take(1) - .order_by(media::name::order(Direction::Asc)), - ) - .with(series::library::fetch().with(library::library_options::fetch())) + .find_first(series_filters) .order_by(series::name::order(Direction::Asc)) + .select(series_or_library_thumbnail::select(book_filters)) .exec() .await? .ok_or(APIError::NotFound("Series not found".to_string()))?; + let first_book = series.media.into_iter().next(); - let library = series - .library()? - .ok_or(APIError::NotFound(String::from("Library relation missing")))?; - - let first_book = series - .media()? - .first() - .ok_or(APIError::NotFound(String::from( - "Series does not have any media", - )))?; - - let image_format = library - .library_options() - .map(LibraryOptions::from)? - .thumbnail_config - .map(|config| config.format); + let library_options = series + .library + .map(|l| l.library_options) + .map(LibraryOptions::from); + let image_format = library_options.and_then(|o| o.thumbnail_config.map(|c| c.format)); - get_series_thumbnail(&series, first_book, image_format, &ctx.config) + get_series_thumbnail(&id, first_book, image_format, &ctx.config) .map(ImageResponse::from) } diff --git a/apps/server/src/routers/opds/v1_2.rs b/apps/server/src/routers/opds/v1_2.rs index fde533b9b..382320d1b 100644 --- a/apps/server/src/routers/opds/v1_2.rs +++ b/apps/server/src/routers/opds/v1_2.rs @@ -20,13 +20,14 @@ use stump_core::{ prisma::{active_reading_session, library, media, series, user}, }; use tower_sessions::Session; -use tracing::{debug, trace, warn}; +use tracing::{debug, trace}; use crate::{ config::state::AppState, errors::{APIError, APIResult}, filter::chain_optional_iter, middleware::auth::Auth, + routers::api::v1::media::get_media_thumbnail_by_id, utils::{ enforce_session_permissions, get_session_user, http::{ImageResponse, NamedFile, Xml}, @@ -513,24 +514,26 @@ async fn get_series_by_id( } } +// TODO: support something like `STRICT_OPDS` to enforce OPDS compliance conditionally fn handle_opds_image_response( content_type: ContentType, image_buffer: Vec, ) -> APIResult { if content_type.is_opds_legacy_image() { - trace!("OPDS legacy image detected, returning as-is"); Ok(ImageResponse::new(content_type, image_buffer)) - } else { - warn!( - ?content_type, - "Unsupported image for OPDS detected, converting to JPEG" - ); - // let jpeg_buffer = image::jpeg_from_bytes(&image_buffer)?; + } else if content_type.is_decodable_image() { + tracing::debug!("Converting image to JPEG for legacy OPDS compatibility"); let jpeg_buffer = GenericImageProcessor::generate( &image_buffer, ImageProcessorOptions::jpeg(), )?; Ok(ImageResponse::new(ContentType::JPEG, jpeg_buffer)) + } else { + tracing::warn!( + ?content_type, + "Encountered image which does not conform to legacy OPDS image requirements" + ); + Ok(ImageResponse::new(content_type, image_buffer)) } } @@ -540,24 +543,9 @@ async fn get_book_thumbnail( State(ctx): State, session: Session, ) -> APIResult { - let db = &ctx.db; - let user = get_session_user(&session)?; - let age_restrictions = user - .age_restriction - .as_ref() - .map(|ar| apply_media_age_restriction(ar.age, ar.restrict_on_unset)); - - let book = db - .media() - .find_first(chain_optional_iter( - [media::id::equals(id.clone())], - [age_restrictions], - )) - .exec() - .await? - .ok_or(APIError::NotFound(String::from("Book not found")))?; + let (content_type, image_buffer) = + get_media_thumbnail_by_id(id, &ctx.db, &session, &ctx.config).await?; - let (content_type, image_buffer) = get_page(book.path.as_str(), 1, &ctx.config)?; handle_opds_image_response(content_type, image_buffer) } diff --git a/apps/web/package.json b/apps/web/package.json index aaa2be33e..474a2222b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@stump/web", - "version": "0.0.5", + "version": "0.0.6", "description": "", "license": "MIT", "scripts": { diff --git a/core/Cargo.toml b/core/Cargo.toml index 681d50a06..da8795eea 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,9 +14,9 @@ email = { path = "../crates/email" } epub = { git = "https://github.com/stumpapp/epub-rs", rev = "38e091abe96875952556ab7dec195022d0230e14" } futures = { workspace = true } globset = "0.4.14" -image = "0.25.2" +image = { version = "0.25.2" } infer = "0.16.0" -itertools = "0.12.1" +itertools = { workspace = true } prisma-client-rust = { workspace = true } rand = { workspace = true } serde = { workspace = true } @@ -24,27 +24,29 @@ serde-xml-rs = "0.6.0" # Support for XML serialization/deserialization serde_json = { workspace = true } simple_crypt = { workspace = true } specta = { workspace = true } +stump-config-gen = { path = "../crates/stump-config-gen"} tokio = { workspace = true } -toml = "0.8.8" +toml = { workspace = true } trash = "3.1.2" # pdf = "0.8.1" pdf = { git = "https://github.com/pdf-rs/pdf", rev = "3bc9e636d31b1846e51b58c7429914e640866f53" } # TODO: revert back to crates.io once fix(es) release -pdfium-render = "0.8.16" -rayon = "1.8.0" -regex = "1.10.4" +pdfium-render = "0.8.24" +rayon = "1.10.0" +regex = "1.10.6" ring = "0.17.8" thiserror = { workspace = true } tracing = { workspace = true } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-appender = "0.2.3" -unrar = { version = "0.5.3" } +unrar = { version = "0.5.6" } urlencoding = { workspace = true } utoipa = { version = "3.5.0" } -uuid = "1.8.0" -walkdir = "2.4.0" +uuid = "1.10.0" +walkdir = "2.5.0" webp = "0.3.0" -xml-rs = "0.8.20" # XML reader/writer -zip = "2.1.6" +xml-rs = "0.8.21" # XML reader/writer +# Note: We need to keep this downgraded for the time being. See https://github.com/stumpapp/stump/issues/427#issuecomment-2332857700 +zip = { version = "=1.1.3", features = ["deflate"], default-features = false } [dev-dependencies] temp-env = "0.3.6" @@ -52,7 +54,7 @@ tempfile = { workspace = true } criterion = { version = "0.5.1", features = ["html_reports", "async_tokio"] } [build-dependencies] -chrono = "0.4.37" +chrono = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] libc = "0.2.152" diff --git a/core/integration-tests/data/book-image-format-test.zip b/core/integration-tests/data/book-image-format-test.zip new file mode 100644 index 000000000..aa42ae8fb Binary files /dev/null and b/core/integration-tests/data/book-image-format-test.zip differ diff --git a/core/src/config/stump_config.rs b/core/src/config/stump_config.rs index 32f4de675..e57826648 100644 --- a/core/src/config/stump_config.rs +++ b/core/src/config/stump_config.rs @@ -5,12 +5,14 @@ //! configuration is used to determine the log file path and verbosity level. This means that any //! logging that occurs during the construction of the [StumpConfig] should be done using the //! standard `println!` or `eprintln!` macros. + use std::{env, path::PathBuf}; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use crate::error::{CoreError, CoreResult}; +use crate::{CoreError, CoreResult}; +use stump_config_gen::StumpConfigGenerator; pub mod env_keys { pub const CONFIG_DIR_KEY: &str = "STUMP_CONFIG_DIR"; @@ -65,219 +67,93 @@ use defaults::*; /// let core = StumpCore::new(config).await; /// } /// ``` -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(StumpConfigGenerator, Serialize, Deserialize, Debug, Clone, PartialEq)] +#[config_file_location(self.get_config_dir().join("Stump.toml"))] pub struct StumpConfig { /// The "release" | "debug" profile with which the application is running. + #[default_value("release".to_string())] + #[debug_value("debug".to_string())] + #[env_key(PROFILE_KEY)] + #[validator(do_validate_profile)] pub profile: String, + /// The port from which to serve the application (default: 10801). + #[default_value(10801)] + #[env_key(PORT_KEY)] pub port: u16, + /// The verbosity with which to log errors (default: 0). + #[default_value(0)] + #[env_key(VERBOSITY_KEY)] pub verbosity: u64, + /// Whether or not to pretty print logs. + #[default_value(true)] + #[env_key(PRETTY_LOGS_KEY)] pub pretty_logs: bool, + /// An optional custom path for the database. + #[default_value(None)] + #[env_key(DB_PATH_KEY)] pub db_path: Option, + /// The client directory. + #[default_value("./client".to_string())] + #[debug_value(env!("CARGO_MANIFEST_DIR").to_string() + "/../web/dist")] + #[env_key(CLIENT_KEY)] pub client_dir: String, + /// An optional custom path for the templates directory. + #[default_value(None)] + #[debug_value(Some(env!("CARGO_MANIFEST_DIR").to_string() + "/../../crates/email/templates"))] + #[env_key("EMAIL_TEMPLATES_DIR")] pub custom_templates_dir: Option, - /// The configuration root for the Stump application, cotains thumbnails, cache, and logs. + + /// The configuration root for the Stump application, contains thumbnails, cache, and logs. + #[debug_value(super::get_default_config_dir())] + #[env_key(CONFIG_DIR_KEY)] + #[required_by_new] pub config_dir: String, + /// A list of origins for CORS. + #[default_value(vec![])] + #[env_key(ORIGINS_KEY)] pub allowed_origins: Vec, + /// Path to the PDFium binary for PDF support. + #[default_value(None)] + #[env_key(PDFIUM_KEY)] pub pdfium_path: Option, + /// Indicates if the Swagger UI should be disabled. + #[default_value(false)] + #[env_key(DISABLE_SWAGGER_KEY)] pub disable_swagger: bool, + /// Password hash cost + #[default_value(DEFAULT_PASSWORD_HASH_COST)] + #[env_key(HASH_COST_KEY)] pub password_hash_cost: u32, + /// The time in seconds that a login session will be valid for. + #[default_value(DEFAULT_SESSION_TTL)] + #[env_key(SESSION_TTL_KEY)] pub session_ttl: i64, + /// The interval at which automatic deleted session cleanup is performed. + #[default_value(DEFAULT_SESSION_EXPIRY_CLEANUP_INTERVAL)] + #[env_key(SESSION_EXPIRY_INTERVAL_KEY)] pub expired_session_cleanup_interval: u64, + /// The size of chunks to use throughout scanning the filesystem. This is used to /// limit the number of files that are processed at once. Realistically, you are bound /// by I/O constraints, but perhaps you can squeeze out some performance by tweaking this. + #[default_value(DEFAULT_SCANNER_CHUNK_SIZE)] + #[env_key(SCANNER_CHUNK_SIZE_KEY)] pub scanner_chunk_size: usize, } impl StumpConfig { - /// Create a new `StumpConfig` instance with a given `config_dir` as - /// the configuration root and default values for other variables. - pub fn new(config_dir: String) -> Self { - Self { - profile: String::from("debug"), - port: 10801, - verbosity: 0, - pretty_logs: true, - db_path: None, - client_dir: String::from("./dist"), - config_dir, - custom_templates_dir: None, - allowed_origins: vec![], - pdfium_path: None, - disable_swagger: false, - password_hash_cost: DEFAULT_PASSWORD_HASH_COST, - session_ttl: DEFAULT_SESSION_TTL, - expired_session_cleanup_interval: DEFAULT_SESSION_EXPIRY_CLEANUP_INTERVAL, - scanner_chunk_size: DEFAULT_SCANNER_CHUNK_SIZE, - } - } - - /// Create a debug version of `StumpConfig` with `config_dir` - /// automatically set using `get_default_config_dir()` and `client_dir` set - /// to `env!("CARGO_MANIFEST_DIR").to_string() + "/../web/dist"`. - pub fn debug() -> Self { - Self { - profile: String::from("debug"), - port: 10801, - verbosity: 0, - pretty_logs: true, - db_path: None, - client_dir: env!("CARGO_MANIFEST_DIR").to_string() + "/../web/dist", - config_dir: super::get_default_config_dir(), - custom_templates_dir: None, - allowed_origins: vec![], - pdfium_path: None, - disable_swagger: false, - password_hash_cost: DEFAULT_PASSWORD_HASH_COST, - session_ttl: DEFAULT_SESSION_TTL, - expired_session_cleanup_interval: DEFAULT_SESSION_EXPIRY_CLEANUP_INTERVAL, - scanner_chunk_size: DEFAULT_SCANNER_CHUNK_SIZE, - } - } - - /// Looks for Stump.toml at `self.config_dir`, loading its contents and replacing - /// stored configuration variables with those contents. If Stump.toml doesn't exist, - /// the stored variables remain unchanged and the function returns `Ok`. - pub fn with_config_file(mut self) -> CoreResult { - let stump_toml = self.get_config_dir().join("Stump.toml"); - - // The config file may not exist (e.g. on first startup), - // this isn't an error, so we just return early. - if !stump_toml.exists() { - return Ok(self); - } - - let toml_content_str = std::fs::read_to_string(stump_toml)?; - let toml_configs = toml::from_str::(&toml_content_str) - .map_err(|e| { - eprintln!("Failed to parse Stump.toml: {}", e); - CoreError::InitializationError(e.to_string()) - })?; - - toml_configs.apply_to_config(&mut self); - Ok(self) - } - - /// Loads configuration variables from the environment, replacing stored - /// values with the environment values. - pub fn with_environment(mut self) -> CoreResult { - let mut env_configs = PartialStumpConfig::empty(); - - if let Ok(profile) = env::var(PROFILE_KEY) { - if profile == "release" || profile == "debug" { - env_configs.profile = Some(profile); - } else { - eprintln!("Invalid PROFILE value: {}", profile); - } - } - - if let Ok(port) = env::var(PORT_KEY) { - let port_u16 = port.parse::().map_err(|e| { - eprintln!("Failed to parse provided STUMP_PORT: {}", e); - CoreError::InitializationError(e.to_string()) - })?; - env_configs.port = Some(port_u16); - } - - if let Ok(verbosity) = env::var(VERBOSITY_KEY) { - let verbosity_u64 = verbosity.parse::().map_err(|e| { - eprintln!("Failed to parse provided STUMP_VERBOSITY: {}", e); - CoreError::InitializationError(e.to_string()) - })?; - env_configs.verbosity = Some(verbosity_u64); - } - - if let Ok(pretty_logs) = env::var(PRETTY_LOGS_KEY) { - let pretty_logs_bool = pretty_logs.parse::().map_err(|e| { - eprintln!("Failed to parse provided STUMP_PRETTY_LOGS: {}", e); - CoreError::InitializationError(e.to_string()) - })?; - self.pretty_logs = pretty_logs_bool; - } - - if let Ok(db_path) = env::var(DB_PATH_KEY) { - env_configs.db_path = Some(db_path); - } - - if let Ok(client_dir) = env::var(CLIENT_KEY) { - env_configs.client_dir = Some(client_dir); - } - - if let Ok(config_dir) = env::var(CONFIG_DIR_KEY) { - env_configs.config_dir = Some(config_dir); - } - - if let Ok(allowed_origins) = env::var(ORIGINS_KEY) { - if !allowed_origins.is_empty() { - env_configs.allowed_origins = Some( - allowed_origins - .split(',') - .map(|val| val.trim().to_string()) - .collect_vec(), - ) - } - }; - - if let Ok(pdfium_path) = env::var(PDFIUM_KEY) { - env_configs.pdfium_path = Some(pdfium_path); - } - - if let Ok(custom_templates_dir) = env::var("EMAIL_TEMPLATES_DIR") { - self.custom_templates_dir = Some(custom_templates_dir); - } - - if let Ok(hash_cost) = env::var(HASH_COST_KEY) { - if let Ok(val) = hash_cost.parse() { - env_configs.password_hash_cost = Some(val); - } - } - - if let Ok(disable_swagger) = env::var(DISABLE_SWAGGER_KEY) { - if let Ok(val) = disable_swagger.parse() { - env_configs.disable_swagger = Some(val); - } - } - - if let Ok(session_ttl) = env::var(SESSION_TTL_KEY) { - match session_ttl.parse() { - Ok(val) => env_configs.session_ttl = Some(val), - Err(e) => eprintln!("Failed to parse provided SESSION_TTL: {}", e), - } - } - - if let Ok(session_expiry_interval) = env::var(SESSION_EXPIRY_INTERVAL_KEY) { - match session_expiry_interval.parse() { - Ok(val) => env_configs.expired_session_cleanup_interval = Some(val), - Err(e) => eprintln!( - "Failed to parse provided SESSION_EXPIRY_CLEANUP_INTERVAL: {}", - e - ), - } - } - - if let Ok(scanner_chunk_size) = env::var(SCANNER_CHUNK_SIZE_KEY) { - match scanner_chunk_size.parse() { - Ok(val) => self.scanner_chunk_size = val, - Err(e) => eprintln!("Failed to parse provided SCANNER_CHUNK_SIZE: {}", e), - } - } - - env_configs.apply_to_config(&mut self); - Ok(self) - } - /// Ensures that the configuration directory exists and saves the `StumpConfig`'s current values /// to Stump.toml in the configuration directory. /// @@ -376,257 +252,21 @@ impl StumpConfig { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct PartialStumpConfig { - pub profile: Option, - pub port: Option, - pub verbosity: Option, - pub pretty_logs: Option, - pub db_path: Option, - pub client_dir: Option, - pub config_dir: Option, - pub allowed_origins: Option>, - pub pdfium_path: Option, - pub disable_swagger: Option, - pub password_hash_cost: Option, - pub session_ttl: Option, - pub expired_session_cleanup_interval: Option, - pub scanner_chunk_size: Option, -} - -impl PartialStumpConfig { - pub fn empty() -> Self { - Self { - profile: None, - port: None, - verbosity: None, - pretty_logs: None, - db_path: None, - client_dir: None, - config_dir: None, - allowed_origins: None, - pdfium_path: None, - disable_swagger: None, - password_hash_cost: None, - session_ttl: None, - expired_session_cleanup_interval: None, - scanner_chunk_size: None, - } +fn do_validate_profile(profile: &String) -> bool { + if profile == "release" || profile == "debug" { + return true; } - pub fn apply_to_config(self, config: &mut StumpConfig) { - if let Some(port) = self.port { - config.port = port; - } - if let Some(verbosity) = self.verbosity { - config.verbosity = verbosity; - } - if let Some(pretty_logs) = self.pretty_logs { - config.pretty_logs = pretty_logs; - } - if let Some(db_path) = self.db_path { - config.db_path = Some(db_path); - } - if let Some(client_dir) = self.client_dir { - config.client_dir = client_dir; - } - if let Some(config_dir) = self.config_dir { - config.config_dir = config_dir; - } - if let Some(disable_swagger) = self.disable_swagger { - config.disable_swagger = disable_swagger; - } - if let Some(hash_cost) = self.password_hash_cost { - config.password_hash_cost = hash_cost; - } - if let Some(session_ttl) = self.session_ttl { - config.session_ttl = session_ttl; - } - if let Some(cleanup_interval) = self.expired_session_cleanup_interval { - config.expired_session_cleanup_interval = cleanup_interval; - } - - // Profile - validate profile selection - if let Some(profile) = self.profile { - if profile == "release" || profile == "debug" { - config.profile = profile; - } else { - eprintln!("Invalid PROFILE value: {}", profile); - } - } - - // Allowed Origins - merge lists - if let Some(origins) = self.allowed_origins { - let orig_origins = config.allowed_origins.clone(); - config - .allowed_origins - .extend(origins.into_iter().filter(|x| !orig_origins.contains(x))); - } - - // Pdfium Path - Merge if not None - if let Some(pdfium_path) = self.pdfium_path { - config.pdfium_path = Some(pdfium_path); - } - - if let Some(scanner_chunk_size) = self.scanner_chunk_size { - config.scanner_chunk_size = scanner_chunk_size; - } - } + eprintln!("Invalid profile value: {}", profile); + false } #[cfg(test)] mod tests { - use std::fs; - use tempfile; use super::*; - #[test] - fn test_apply_partial_to_debug() { - let mut config = StumpConfig::debug(); - config.allowed_origins = vec!["origin1".to_string(), "origin2".to_string()]; - - let partial_config = PartialStumpConfig { - profile: Some("release".to_string()), - port: Some(1337), - verbosity: Some(3), - pretty_logs: Some(true), - db_path: Some("not_a_real_path".to_string()), - client_dir: Some("not_a_real_dir".to_string()), - config_dir: Some("also_not_a_real_dir".to_string()), - allowed_origins: Some(vec![ - "origin1".to_string(), - "origin3".to_string(), - "origin2".to_string(), - ]), - pdfium_path: Some("not_a_path_to_pdfium".to_string()), - disable_swagger: Some(true), - password_hash_cost: Some(24), - session_ttl: Some(3600 * 24), - expired_session_cleanup_interval: Some(60 * 60 * 8), - scanner_chunk_size: Some(300), - }; - - // Apply the partial configuration - partial_config.apply_to_config(&mut config); - - // Check that values are as expected - assert_eq!( - config, - StumpConfig { - profile: "release".to_string(), - port: 1337, - verbosity: 3, - pretty_logs: true, - db_path: Some("not_a_real_path".to_string()), - client_dir: "not_a_real_dir".to_string(), - config_dir: "also_not_a_real_dir".to_string(), - custom_templates_dir: None, - allowed_origins: vec![ - "origin1".to_string(), - "origin2".to_string(), - "origin3".to_string() - ], - pdfium_path: Some("not_a_path_to_pdfium".to_string()), - disable_swagger: true, - password_hash_cost: 24, - session_ttl: 3600 * 24, - expired_session_cleanup_interval: 60 * 60 * 8, - scanner_chunk_size: 300, - } - ); - } - - #[test] - fn test_getting_config_from_environment() { - temp_env::with_vars( - [ - (PROFILE_KEY, Some("release")), - (PORT_KEY, Some("1337")), - (VERBOSITY_KEY, Some("2")), - (DB_PATH_KEY, Some("not_a_real_path")), - (CLIENT_KEY, Some("not_a_real_dir")), - (CONFIG_DIR_KEY, Some("also_not_a_real_dir")), - (DISABLE_SWAGGER_KEY, Some("true")), - (HASH_COST_KEY, Some("24")), - (SESSION_TTL_KEY, Some(&(3600 * 24).to_string())), - ( - SESSION_EXPIRY_INTERVAL_KEY, - Some(&(60 * 60 * 8).to_string()), - ), - ], - || { - // Create a new StumpConfig and load values from the environment. - let config = StumpConfig::new("not_a_dir".to_string()) - .with_environment() - .unwrap(); - - // Confirm values are as expected - assert_eq!( - config, - StumpConfig { - profile: "release".to_string(), - port: 1337, - verbosity: 2, - pretty_logs: true, - db_path: Some("not_a_real_path".to_string()), - client_dir: "not_a_real_dir".to_string(), - config_dir: "also_not_a_real_dir".to_string(), - allowed_origins: vec![], - pdfium_path: None, - disable_swagger: true, - password_hash_cost: 24, - session_ttl: 3600 * 24, - expired_session_cleanup_interval: 60 * 60 * 8, - scanner_chunk_size: DEFAULT_SCANNER_CHUNK_SIZE, - custom_templates_dir: None, - } - ); - }, - ); - } - - #[test] - fn test_getting_config_from_toml() { - // Create temporary directory and place a copy of our mock Stump.toml in it - let tempdir = tempfile::tempdir().expect("Failed to create temporary directory"); - let temp_config_file_path = tempdir.path().join("Stump.toml"); - fs::write(temp_config_file_path, get_mock_config_file()) - .expect("Failed to write temporary Stump.toml"); - - // Now we can create a StumpConfig rooted at the temporary directory and load the values - let config_dir = tempdir.path().to_string_lossy().to_string(); - let config = StumpConfig::new(config_dir).with_config_file().unwrap(); - - // Check that values are as expected - assert_eq!( - config, - StumpConfig { - profile: "release".to_string(), - port: 1337, - verbosity: 3, - pretty_logs: true, - db_path: Some("not_a_real_path".to_string()), - client_dir: "not_a_real_dir".to_string(), - config_dir: "also_not_a_real_dir".to_string(), - custom_templates_dir: None, - allowed_origins: vec!["origin1".to_string(), "origin2".to_string()], - pdfium_path: Some("not_a_path_to_pdfium".to_string()), - disable_swagger: false, - password_hash_cost: DEFAULT_PASSWORD_HASH_COST, - session_ttl: DEFAULT_SESSION_TTL, - expired_session_cleanup_interval: DEFAULT_SESSION_EXPIRY_CLEANUP_INTERVAL, - scanner_chunk_size: DEFAULT_SCANNER_CHUNK_SIZE, - } - ); - - // Ensure that the temporary directory is deleted - tempdir - .close() - .expect("Failed to delete temporary directory"); - } - #[test] fn test_writing_to_config_dir() { let tempdir = tempfile::tempdir().expect("Failed to create temporary directory"); @@ -643,6 +283,7 @@ mod tests { pretty_logs: Some(true), db_path: Some("not_a_real_path".to_string()), client_dir: Some("not_a_real_dir".to_string()), + custom_templates_dir: None, config_dir: None, allowed_origins: Some(vec!["origin1".to_string(), "origin2".to_string()]), pdfium_path: Some("not_a_path_to_pdfium".to_string()), @@ -674,6 +315,7 @@ mod tests { db_path: Some("not_a_real_path".to_string()), client_dir: Some("not_a_real_dir".to_string()), config_dir: Some(config_dir), + custom_templates_dir: None, allowed_origins: Some(vec!["origin1".to_string(), "origin2".to_string()]), pdfium_path: Some("not_a_path_to_pdfium".to_string()), disable_swagger: Some(false), @@ -715,12 +357,12 @@ mod tests { assert_eq!( generated, StumpConfig { - profile: "debug".to_string(), + profile: "release".to_string(), port: 1337, verbosity: 2, pretty_logs: true, db_path: None, - client_dir: "./dist".to_string(), + client_dir: "./client".to_string(), config_dir, allowed_origins: vec![], pdfium_path: None, @@ -736,11 +378,4 @@ mod tests { }, ); } - - fn get_mock_config_file() -> String { - let mock_config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("integration-tests/data/mock-stump.toml"); - - fs::read_to_string(mock_config_path).expect("Failed to fetch mock config file") - } } diff --git a/core/src/context.rs b/core/src/context.rs index a6e8ec336..796f6354f 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -34,7 +34,7 @@ impl Ctx { /// core can send events to the consumer. /// /// ## Example - /// ```rust + /// ```no_run /// use stump_core::{Ctx, config::StumpConfig}; /// use tokio::sync::mpsc::unbounded_channel; /// @@ -85,7 +85,7 @@ impl Ctx { /// is just a simple utility function. /// /// ## Example - /// ```rust + /// ```no_run /// use stump_core::{Ctx, config::StumpConfig}; /// use std::sync::Arc; /// @@ -117,7 +117,7 @@ impl Ctx { /// Emits a [CoreEvent] to the client event channel. /// /// ## Example - /// ```rust + /// ```no_run /// use stump_core::{Ctx, config::StumpConfig, CoreEvent}; /// /// #[tokio::main] diff --git a/core/src/db/entity/emailer/entity.rs b/core/src/db/entity/emailer/entity.rs index 44772f502..7a6cc93d0 100644 --- a/core/src/db/entity/emailer/entity.rs +++ b/core/src/db/entity/emailer/entity.rs @@ -1,4 +1,4 @@ -use email::{EmailerClient, EmailerClientConfig}; +use email::{EmailError, EmailerClient, EmailerClientConfig}; use serde::{Deserialize, Serialize}; use specta::Type; use utoipa::ToSchema; @@ -20,7 +20,7 @@ pub struct EmailerConfig { pub username: String, /// The encrypted password to use for the SMTP server #[serde(skip_serializing)] - pub encrypted_password: String, + pub encrypted_password: Option, /// The SMTP host to use pub smtp_host: String, /// The SMTP port to use @@ -37,12 +37,18 @@ impl EmailerConfig { /// Convert the config into a client config, which is used for the actual sending of emails pub async fn into_client_config(self, ctx: &Ctx) -> CoreResult { let encryption_key = ctx.get_encryption_key().await?; - let password = decrypt_string(&self.encrypted_password, &encryption_key)?; + let password = decrypt_string( + &self + .encrypted_password + .ok_or_else(|| EmailError::NoPassword)?, + &encryption_key, + )?; + Ok(EmailerClientConfig { sender_email: self.sender_email, sender_display_name: self.sender_display_name, username: self.username, - password, + password: Some(password), host: self.smtp_host, port: self.smtp_port, tls_enabled: self.tls_enabled, @@ -51,12 +57,21 @@ impl EmailerConfig { }) } + /// Create an emailer config from a client config, encrypting the password pub async fn from_client_config( config: EmailerClientConfig, ctx: &Ctx, ) -> CoreResult { - let encryption_key = ctx.get_encryption_key().await?; - let encrypted_password = encrypt_string(&config.password, &encryption_key)?; + // Note: The password isn't really optional, but in order to support update operations + // the type is defined as optional. This will error if the password is not set elsewhere. + let encrypted_password = match config.password { + Some(p) if !p.is_empty() => { + let encryption_key = ctx.get_encryption_key().await?; + Some(encrypt_string(&p, &encryption_key)?) + }, + _ => None, + }; + Ok(EmailerConfig { sender_email: config.sender_email, sender_display_name: config.sender_display_name, @@ -117,7 +132,7 @@ impl TryFrom for SMTPEmailer { sender_email: data.sender_email, sender_display_name: data.sender_display_name, username: data.username, - encrypted_password: data.encrypted_password, + encrypted_password: Some(data.encrypted_password), smtp_host: data.smtp_host, smtp_port: data.smtp_port as u16, tls_enabled: data.tls_enabled, diff --git a/core/src/db/entity/library.rs b/core/src/db/entity/library/entity.rs similarity index 99% rename from core/src/db/entity/library.rs rename to core/src/db/entity/library/entity.rs index e3477b881..9f4f36bdb 100644 --- a/core/src/db/entity/library.rs +++ b/core/src/db/entity/library/entity.rs @@ -5,12 +5,11 @@ use specta::Type; use utoipa::ToSchema; use crate::{ + db::entity::{Cursor, Series, Tag}, filesystem::image::ImageProcessorOptions, prisma::{self, library}, }; -use super::{common::Cursor, series::Series, tag::Tag}; - ////////////////////////////////////////////// //////////////// PRISMA MACROS /////////////// ////////////////////////////////////////////// diff --git a/core/src/db/entity/library/mod.rs b/core/src/db/entity/library/mod.rs new file mode 100644 index 000000000..1f0ef440e --- /dev/null +++ b/core/src/db/entity/library/mod.rs @@ -0,0 +1,4 @@ +mod entity; +pub(crate) mod prisma_macros; + +pub use entity::*; diff --git a/core/src/db/entity/library/prisma_macros.rs b/core/src/db/entity/library/prisma_macros.rs new file mode 100644 index 000000000..f794ab0bf --- /dev/null +++ b/core/src/db/entity/library/prisma_macros.rs @@ -0,0 +1,14 @@ +use crate::prisma::library; + +library::select!(library_tags_select { + id + tags: select { + id + name + } +}); + +library::select!(library_path_with_options_select { + path + library_options +}); diff --git a/core/src/db/entity/media/prisma_macros.rs b/core/src/db/entity/media/prisma_macros.rs index 5e367fc44..937dfd75c 100644 --- a/core/src/db/entity/media/prisma_macros.rs +++ b/core/src/db/entity/media/prisma_macros.rs @@ -5,6 +5,16 @@ media::select!(media_path_modified_at_select { modified_at }); +media::select!(media_thumbnail { + id + path + series: select { + library: select { + library_options + } + } +}); + active_reading_session::include!(reading_session_with_book_pages { media: select { pages } }); diff --git a/core/src/db/entity/mod.rs b/core/src/db/entity/mod.rs index 60f9b02c2..f64fb6637 100644 --- a/core/src/db/entity/mod.rs +++ b/core/src/db/entity/mod.rs @@ -39,7 +39,9 @@ pub use common::{ pub mod macros { pub use super::book_club::prisma_macros::*; + pub use super::library::prisma_macros::*; pub use super::media::prisma_macros::*; pub use super::metadata::prisma_macros::*; + pub use super::series::prisma_macros::*; pub use super::smart_list::prisma_macros::*; } diff --git a/core/src/db/entity/series/mod.rs b/core/src/db/entity/series/mod.rs index 135a9f2a7..1f0ef440e 100644 --- a/core/src/db/entity/series/mod.rs +++ b/core/src/db/entity/series/mod.rs @@ -1,3 +1,4 @@ mod entity; +pub(crate) mod prisma_macros; pub use entity::*; diff --git a/core/src/db/entity/series/prisma_macros.rs b/core/src/db/entity/series/prisma_macros.rs new file mode 100644 index 000000000..044048c32 --- /dev/null +++ b/core/src/db/entity/series/prisma_macros.rs @@ -0,0 +1,16 @@ +use prisma_client_rust::Direction; + +use crate::prisma::{media as book, series}; + +// Note: I think the macro is generating code which is conflicting with `media`, because I have +// to alias `media` as `book` to avoid the conflict. FYI for future reference. +series::select!((book_filters: Vec) => series_or_library_thumbnail { + id + media(book_filters).order_by(book::name::order(Direction::Asc)).take(1): select { + id + path + } + library: select { + library_options + } +}); diff --git a/core/src/db/entity/user/permissions.rs b/core/src/db/entity/user/permissions.rs index fdeab9eeb..d04a80cc5 100644 --- a/core/src/db/entity/user/permissions.rs +++ b/core/src/db/entity/user/permissions.rs @@ -107,6 +107,7 @@ impl UserPermission { /// Return a list of permissions, if any, which are inherited by self /// /// For example, UserPermission::CreateNotifier implies UserPermission::ReadNotifier + // TODO: revisit these. I am mixing patterns, e.g. manage vs explicit edit+create+delete. Pick one! pub fn associated(&self) -> Vec { match self { UserPermission::CreateBookClub => vec![UserPermission::AccessBookClub], diff --git a/core/src/error.rs b/core/src/error.rs index a28d86e12..20280d450 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -19,6 +19,8 @@ pub enum CoreError { DecryptionFailed(String), #[error("Failed to initialize Stump core: {0}")] InitializationError(String), + #[error("{0}")] + EmailerError(#[from] email::EmailError), #[error("Query error: {0}")] QueryError(#[from] prisma_client_rust::queries::QueryError), #[error("Invalid query error: {0}")] diff --git a/core/src/filesystem/archive.rs b/core/src/filesystem/archive.rs index a5cd4b5d3..b8655bdb8 100644 --- a/core/src/filesystem/archive.rs +++ b/core/src/filesystem/archive.rs @@ -17,7 +17,7 @@ pub(crate) fn zip_dir( let mut zip_writer = zip::ZipWriter::new(zip_file); - let options: FileOptions<'_, ()> = FileOptions::default() + let options: FileOptions<()> = FileOptions::default() .compression_method(CompressionMethod::Stored) .unix_permissions(0o755); @@ -110,7 +110,7 @@ mod tests { assert_eq!(zip_archive.len(), 1); let mut file = zip_archive.by_index(0).unwrap(); - assert_eq!(file.name(), "file.txt"); + assert_eq!(file.name(), "/file.txt"); let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); diff --git a/core/src/filesystem/common.rs b/core/src/filesystem/common.rs index 6cef35fc8..0454cdb02 100644 --- a/core/src/filesystem/common.rs +++ b/core/src/filesystem/common.rs @@ -9,7 +9,8 @@ use walkdir::WalkDir; use super::{media::is_accepted_cover_name, ContentType, FileError}; -pub const ACCEPTED_IMAGE_EXTENSIONS: [&str; 5] = ["jpg", "png", "jpeg", "webp", "gif"]; +pub const ACCEPTED_IMAGE_EXTENSIONS: [&str; 8] = + ["jpg", "png", "jpeg", "jxl", "webp", "gif", "avif", "heif"]; pub fn read_entire_file>(path: P) -> Result, FileError> { let mut file = File::open(path)?; diff --git a/core/src/filesystem/content_type.rs b/core/src/filesystem/content_type.rs index 6f5b3ff34..0a438c5ee 100644 --- a/core/src/filesystem/content_type.rs +++ b/core/src/filesystem/content_type.rs @@ -20,8 +20,11 @@ pub enum ContentType { COMIC_ZIP, RAR, COMIC_RAR, + AVIF, + HEIF, PNG, JPEG, + JPEG_XL, WEBP, GIF, TXT, @@ -58,7 +61,7 @@ impl ContentType { /// Infer the MIME type of a file extension. /// /// ### Example - /// ```rust + /// ```no_run /// use stump_core::filesystem::ContentType; /// /// let content_type = ContentType::from_extension("png"); @@ -75,9 +78,12 @@ impl ContentType { "cbz" => ContentType::COMIC_ZIP, "rar" => ContentType::RAR, "cbr" => ContentType::COMIC_RAR, + "avif" => ContentType::AVIF, + "heif" => ContentType::HEIF, "png" => ContentType::PNG, "jpg" => ContentType::JPEG, "jpeg" => ContentType::JPEG, + "jxl" => ContentType::JPEG_XL, "webp" => ContentType::WEBP, "gif" => ContentType::GIF, "txt" => ContentType::TXT, @@ -89,7 +95,7 @@ impl ContentType { /// then the file extension is used to determine the content type. /// /// ### Example - /// ```rust + /// ```no_run /// use stump_core::filesystem::ContentType; /// /// let content_type = ContentType::from_file("test.png"); @@ -104,7 +110,7 @@ impl ContentType { /// inferred, then the content type is set to [ContentType::UNKNOWN]. /// /// ### Example - /// ```rust + /// ```no_run /// use stump_core::filesystem::ContentType; /// /// let buf = [0xFF, 0xD8, 0xFF, 0xAA]; @@ -121,7 +127,7 @@ impl ContentType { /// inferred, then the extension is used to determine the content type. /// /// ### Example - /// ```rust + /// ```no_run /// use stump_core::filesystem::ContentType; /// /// // This is NOT a valid PNG buff @@ -148,7 +154,7 @@ impl ContentType { /// then the extension of the path is used to determine the content type. /// /// ### Example - /// ```rust + /// ```no_run /// use stump_core::filesystem::ContentType; /// use std::path::Path; /// @@ -177,7 +183,7 @@ impl ContentType { /// Returns true if the content type is an image. /// /// ## Example - /// ```rust + /// ```no_run /// use stump_core::filesystem::ContentType; /// /// let content_type = ContentType::PNG; @@ -195,23 +201,44 @@ impl ContentType { /// /// ## Example /// - /// ```rust + /// ```no_run /// use stump_core::filesystem::ContentType; /// /// let content_type = ContentType::PNG; /// assert!(content_type.is_opds_legacy_image()); /// ``` pub fn is_opds_legacy_image(&self) -> bool { - self == &ContentType::PNG - || self == &ContentType::JPEG - || self == &ContentType::GIF + matches!( + self, + ContentType::PNG | ContentType::JPEG | ContentType::GIF + ) + } + + /// Returns true if the content type is a decodable image. A decodable image is an image that + /// can be decoded by the `image` crate or by a custom image processor in Stump. + /// + /// See https://github.com/image-rs/image?tab=readme-ov-file#supported-image-formats + /// + /// ## Example + /// + /// ```no_run + /// use stump_core::filesystem::ContentType; + /// + /// let content_type = ContentType::PNG; + /// assert!(content_type.is_decodable_image()); + /// ``` + pub fn is_decodable_image(&self) -> bool { + matches!( + self, + ContentType::PNG | ContentType::JPEG | ContentType::WEBP | ContentType::GIF + ) } /// Returns true if the content type is a ZIP archive. /// /// ## Example /// - /// ```rust + /// ```no_run /// use stump_core::filesystem::ContentType; /// /// let content_type = ContentType::ZIP; @@ -225,7 +252,7 @@ impl ContentType { /// /// ## Example /// - /// ```rust + /// ```no_run /// use stump_core::filesystem::ContentType; /// /// let content_type = ContentType::RAR; @@ -239,7 +266,7 @@ impl ContentType { /// /// ## Example /// - /// ```rust + /// ```no_run /// use stump_core::filesystem::ContentType; /// /// let content_type = ContentType::EPUB_ZIP; @@ -262,9 +289,12 @@ impl ContentType { ContentType::COMIC_ZIP => "cbz", ContentType::RAR => "rar", ContentType::COMIC_RAR => "cbr", + ContentType::HEIF => "heif", ContentType::PNG => "png", ContentType::JPEG => "jpg", + ContentType::JPEG_XL => "jxl", ContentType::WEBP => "webp", + ContentType::AVIF => "avif", ContentType::GIF => "gif", ContentType::TXT => "txt", ContentType::UNKNOWN => "", @@ -288,9 +318,12 @@ impl From<&str> for ContentType { "application/vnd.comicbook+zip" => ContentType::COMIC_ZIP, "application/vnd.rar" => ContentType::RAR, "application/vnd.comicbook-rar" => ContentType::COMIC_RAR, + "image/heif" => ContentType::HEIF, "image/png" => ContentType::PNG, "image/jpeg" => ContentType::JPEG, + "image/jxl" => ContentType::JPEG_XL, "image/webp" => ContentType::WEBP, + "image/avif" => ContentType::AVIF, "image/gif" => ContentType::GIF, _ => ContentType::UNKNOWN, } @@ -309,8 +342,11 @@ impl std::fmt::Display for ContentType { ContentType::COMIC_ZIP => write!(f, "application/vnd.comicbook+zip"), ContentType::RAR => write!(f, "application/vnd.rar"), ContentType::COMIC_RAR => write!(f, "application/vnd.comicbook-rar"), + ContentType::AVIF => write!(f, "image/avif"), + ContentType::HEIF => write!(f, "image/heif"), ContentType::PNG => write!(f, "image/png"), ContentType::JPEG => write!(f, "image/jpeg"), + ContentType::JPEG_XL => write!(f, "image/jxl"), ContentType::WEBP => write!(f, "image/webp"), ContentType::GIF => write!(f, "image/gif"), ContentType::TXT => write!(f, "text/plain"), @@ -327,6 +363,7 @@ impl From for ContentType { // ImageFormat::JpegXl => ContentType::JPEG, ImageFormat::Png => ContentType::PNG, ImageFormat::Webp => ContentType::WEBP, + // ImageFormat::Avif => ContentType::AVIF, } } } @@ -346,8 +383,11 @@ impl TryFrom for image::ImageFormat { // Match values that are compatible with the image crate. Other values should return // an error. match value { + ContentType::AVIF => Err(unsupported_error("ContentType::AVIF")), + ContentType::HEIF => Err(unsupported_error("ContentType::HEIF")), ContentType::PNG => Ok(image::ImageFormat::Png), ContentType::JPEG => Ok(image::ImageFormat::Jpeg), + ContentType::JPEG_XL => Err(unsupported_error("ContentType::JPEG_XL")), ContentType::WEBP => Ok(image::ImageFormat::WebP), ContentType::GIF => Ok(image::ImageFormat::Gif), ContentType::XHTML => Err(unsupported_error("ContentType::XHTML")), @@ -364,3 +404,225 @@ impl TryFrom for image::ImageFormat { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_content_type_from_extension() { + assert_eq!(ContentType::from_extension("xhtml"), ContentType::XHTML); + assert_eq!(ContentType::from_extension("xml"), ContentType::XML); + assert_eq!(ContentType::from_extension("html"), ContentType::HTML); + assert_eq!(ContentType::from_extension("pdf"), ContentType::PDF); + assert_eq!(ContentType::from_extension("epub"), ContentType::EPUB_ZIP); + assert_eq!(ContentType::from_extension("zip"), ContentType::ZIP); + assert_eq!(ContentType::from_extension("cbz"), ContentType::COMIC_ZIP); + assert_eq!(ContentType::from_extension("rar"), ContentType::RAR); + assert_eq!(ContentType::from_extension("cbr"), ContentType::COMIC_RAR); + assert_eq!(ContentType::from_extension("png"), ContentType::PNG); + assert_eq!(ContentType::from_extension("jpg"), ContentType::JPEG); + assert_eq!(ContentType::from_extension("jpeg"), ContentType::JPEG); + assert_eq!(ContentType::from_extension("webp"), ContentType::WEBP); + assert_eq!(ContentType::from_extension("avif"), ContentType::AVIF); + assert_eq!(ContentType::from_extension("gif"), ContentType::GIF); + assert_eq!(ContentType::from_extension("txt"), ContentType::TXT); + assert_eq!(ContentType::from_extension("opf"), ContentType::XML); + assert_eq!(ContentType::from_extension("ncx"), ContentType::XML); + assert_eq!(ContentType::from_extension("unknown"), ContentType::UNKNOWN); + } + + #[test] + fn test_content_type_from_file() { + assert_eq!(ContentType::from_file("test.xhtml"), ContentType::XHTML); + assert_eq!(ContentType::from_file("test.xml"), ContentType::XML); + assert_eq!(ContentType::from_file("test.html"), ContentType::HTML); + assert_eq!(ContentType::from_file("test.pdf"), ContentType::PDF); + assert_eq!(ContentType::from_file("test.epub"), ContentType::EPUB_ZIP); + assert_eq!(ContentType::from_file("test.zip"), ContentType::ZIP); + assert_eq!(ContentType::from_file("test.cbz"), ContentType::COMIC_ZIP); + assert_eq!(ContentType::from_file("test.rar"), ContentType::RAR); + assert_eq!(ContentType::from_file("test.cbr"), ContentType::COMIC_RAR); + assert_eq!(ContentType::from_file("test.png"), ContentType::PNG); + assert_eq!(ContentType::from_file("test.jpg"), ContentType::JPEG); + assert_eq!(ContentType::from_file("test.jpeg"), ContentType::JPEG); + assert_eq!(ContentType::from_file("test.webp"), ContentType::WEBP); + assert_eq!(ContentType::from_file("test.avif"), ContentType::AVIF); + assert_eq!(ContentType::from_file("test.gif"), ContentType::GIF); + assert_eq!(ContentType::from_file("test.txt"), ContentType::TXT); + assert_eq!(ContentType::from_file("test.unknown"), ContentType::UNKNOWN); + } + + #[test] + fn test_content_type_from_bytes() { + let buf = [0xFF, 0xD8, 0xFF, 0xAA]; + assert_eq!(ContentType::from_bytes(&buf), ContentType::JPEG); + } + + #[test] + fn test_content_type_from_bytes_with_fallback() { + let buf = [0xFF, 0xD8, 0xBB, 0xBB]; // Not a valid PNG buffer + assert_eq!( + ContentType::from_bytes_with_fallback(&buf, "png"), + ContentType::PNG + ); + } + + #[test] + fn test_content_type_from_path() { + let path = Path::new("test.xhtml"); + assert_eq!(ContentType::from_path(path), ContentType::XHTML); + + let path = Path::new("test.xml"); + assert_eq!(ContentType::from_path(path), ContentType::XML); + + let path = Path::new("test.html"); + assert_eq!(ContentType::from_path(path), ContentType::HTML); + + let path = Path::new("test.pdf"); + assert_eq!(ContentType::from_path(path), ContentType::PDF); + + let path = Path::new("test.epub"); + assert_eq!(ContentType::from_path(path), ContentType::EPUB_ZIP); + + let path = Path::new("test.zip"); + assert_eq!(ContentType::from_path(path), ContentType::ZIP); + + let path = Path::new("test.cbz"); + assert_eq!(ContentType::from_path(path), ContentType::COMIC_ZIP); + + let path = Path::new("test.rar"); + assert_eq!(ContentType::from_path(path), ContentType::RAR); + + let path = Path::new("test.cbr"); + assert_eq!(ContentType::from_path(path), ContentType::COMIC_RAR); + + let path = Path::new("test.png"); + assert_eq!(ContentType::from_path(path), ContentType::PNG); + + let path = Path::new("test.jpg"); + assert_eq!(ContentType::from_path(path), ContentType::JPEG); + + let path = Path::new("test.jpeg"); + assert_eq!(ContentType::from_path(path), ContentType::JPEG); + + let path = Path::new("test.webp"); + assert_eq!(ContentType::from_path(path), ContentType::WEBP); + + let path = Path::new("test.avif"); + assert_eq!(ContentType::from_path(path), ContentType::AVIF); + + let path = Path::new("test.gif"); + assert_eq!(ContentType::from_path(path), ContentType::GIF); + + let path = Path::new("test.txt"); + assert_eq!(ContentType::from_path(path), ContentType::TXT); + + let path = Path::new("test.unknown"); + assert_eq!(ContentType::from_path(path), ContentType::UNKNOWN); + } + + #[test] + fn test_content_type_mime_type() { + assert_eq!( + ContentType::XHTML.mime_type(), + "application/xhtml+xml".to_string() + ); + assert_eq!(ContentType::XML.mime_type(), "application/xml".to_string()); + assert_eq!(ContentType::HTML.mime_type(), "text/html".to_string()); + assert_eq!(ContentType::PDF.mime_type(), "application/pdf".to_string()); + assert_eq!( + ContentType::EPUB_ZIP.mime_type(), + "application/epub+zip".to_string() + ); + assert_eq!(ContentType::ZIP.mime_type(), "application/zip".to_string()); + assert_eq!( + ContentType::COMIC_ZIP.mime_type(), + "application/vnd.comicbook+zip".to_string() + ); + assert_eq!( + ContentType::RAR.mime_type(), + "application/vnd.rar".to_string() + ); + assert_eq!( + ContentType::COMIC_RAR.mime_type(), + "application/vnd.comicbook-rar".to_string() + ); + assert_eq!(ContentType::PNG.mime_type(), "image/png".to_string()); + assert_eq!(ContentType::JPEG.mime_type(), "image/jpeg".to_string()); + assert_eq!(ContentType::WEBP.mime_type(), "image/webp".to_string()); + assert_eq!(ContentType::AVIF.mime_type(), "image/avif".to_string()); + assert_eq!(ContentType::GIF.mime_type(), "image/gif".to_string()); + assert_eq!(ContentType::TXT.mime_type(), "text/plain".to_string()); + assert_eq!(ContentType::UNKNOWN.mime_type(), "unknown".to_string()); + } + + #[test] + fn test_content_type_is_image() { + // Images + assert!(ContentType::AVIF.is_image()); + assert!(ContentType::HEIF.is_image()); + assert!(ContentType::PNG.is_image()); + assert!(ContentType::JPEG.is_image()); + assert!(ContentType::JPEG_XL.is_image()); + assert!(ContentType::WEBP.is_image()); + assert!(ContentType::AVIF.is_image()); + assert!(ContentType::GIF.is_image()); + assert!(!ContentType::XHTML.is_image()); + + // Not images + assert!(!ContentType::XHTML.is_image()); + assert!(!ContentType::XML.is_image()); + assert!(!ContentType::HTML.is_image()); + assert!(!ContentType::PDF.is_image()); + assert!(!ContentType::EPUB_ZIP.is_image()); + assert!(!ContentType::ZIP.is_image()); + assert!(!ContentType::COMIC_ZIP.is_image()); + assert!(!ContentType::RAR.is_image()); + assert!(!ContentType::COMIC_RAR.is_image()); + assert!(!ContentType::TXT.is_image()); + assert!(!ContentType::UNKNOWN.is_image()); + } + + #[test] + fn test_content_type_is_opds_legacy_image() { + // Is an OPDS 1.2 legacy image + assert!(ContentType::PNG.is_opds_legacy_image()); + assert!(ContentType::JPEG.is_opds_legacy_image()); + assert!(ContentType::GIF.is_opds_legacy_image()); + + // Not an OPDS 1.2 legacy image + assert!(!ContentType::WEBP.is_opds_legacy_image()); + assert!(!ContentType::JPEG_XL.is_opds_legacy_image()); + assert!(!ContentType::AVIF.is_opds_legacy_image()); + } + + #[test] + fn test_content_type_is_zip() { + // ZIP archives + assert!(ContentType::ZIP.is_zip()); + assert!(ContentType::COMIC_ZIP.is_zip()); + // Not ZIP archives + assert!(!ContentType::RAR.is_zip()); + assert!(!ContentType::COMIC_RAR.is_zip()); + } + + #[test] + fn test_content_type_is_rar() { + // RAR archives + assert!(ContentType::RAR.is_rar()); + assert!(ContentType::COMIC_RAR.is_rar()); + // Not RAR archives + assert!(!ContentType::ZIP.is_rar()); + assert!(!ContentType::COMIC_ZIP.is_rar()); + } + + #[test] + fn test_content_type_is_epub() { + // EPUB archives + assert!(ContentType::EPUB_ZIP.is_epub()); + // Not EPUB archives + assert!(!ContentType::ZIP.is_epub()); + assert!(!ContentType::COMIC_ZIP.is_epub()); + } +} diff --git a/core/src/filesystem/error.rs b/core/src/filesystem/error.rs index 8ed0960d5..5d2daaf71 100644 --- a/core/src/filesystem/error.rs +++ b/core/src/filesystem/error.rs @@ -25,7 +25,7 @@ pub enum FileError { #[error("{0}")] PdfError(#[from] pdf::error::PdfError), #[error("{0}")] - PdfRendererError(#[from] pdfium_render::error::PdfiumError), + PdfRendererError(#[from] pdfium_render::prelude::PdfiumError), #[error("Stump is not properly configured to render PDFs")] PdfConfigurationError, #[error("Failed to process PDF file: {0}")] diff --git a/core/src/filesystem/image/generic.rs b/core/src/filesystem/image/generic.rs index 39099f8e0..eb2a8eb43 100644 --- a/core/src/filesystem/image/generic.rs +++ b/core/src/filesystem/image/generic.rs @@ -25,7 +25,16 @@ impl ImageProcessor for GenericImageProcessor { } let format = match options.format { - process::ImageFormat::Jpeg => Ok(ImageFormat::Jpeg), + process::ImageFormat::Jpeg => { + if image.color().has_alpha() { + if image.color().has_color() { + image = image::DynamicImage::from(image.into_rgb8()); + } else { + image = image::DynamicImage::from(image.into_luma8()); + } + } + Ok(ImageFormat::Jpeg) + }, process::ImageFormat::Png => Ok(ImageFormat::Png), // TODO: change error kind _ => Err(FileError::UnknownError(String::from( @@ -58,6 +67,8 @@ mod tests { ImageFormat, ImageProcessorOptions, }; + //JPG -> other Tests + //JPG -> JPG #[test] fn test_generate_jpg_to_jpg() { let jpg_path = get_test_jpg_path(); @@ -119,37 +130,37 @@ mod tests { assert_eq!(dimensions.1, 100); } + //JPG -> PNG #[test] - fn test_generate_png_to_jpg() { - let png_path = get_test_png_path(); + fn test_generate_jpg_to_png() { + let jpg_path = get_test_jpg_path(); let options = ImageProcessorOptions { - format: ImageFormat::Jpeg, + format: ImageFormat::Png, ..Default::default() }; - let buffer = GenericImageProcessor::generate_from_path(&png_path, options) + let buffer = GenericImageProcessor::generate_from_path(&jpg_path, options) .expect("Failed to generate image buffer"); assert!(!buffer.is_empty()); - // should be a valid JPEG + // should be a valid PNG assert!( - image::load_from_memory_with_format(&buffer, image::ImageFormat::Jpeg) - .is_ok() + image::load_from_memory_with_format(&buffer, image::ImageFormat::Png).is_ok() ); } #[test] - fn test_generate_png_to_jpg_with_rescale() { - let png_path = get_test_png_path(); + fn test_generate_jpg_to_png_with_rescale() { + let jpg_path = get_test_jpg_path(); let options = ImageProcessorOptions { - format: ImageFormat::Jpeg, + format: ImageFormat::Png, resize_options: Some(ImageResizeOptions::scaled(0.5, 0.5)), ..Default::default() }; let current_dimensions = - image::image_dimensions(&png_path).expect("Failed to get dimensions"); + image::image_dimensions(&jpg_path).expect("Failed to get dimensions"); - let buffer = GenericImageProcessor::generate_from_path(&png_path, options) + let buffer = GenericImageProcessor::generate_from_path(&jpg_path, options) .expect("Failed to generate image buffer"); let new_dimensions = image::load_from_memory(&buffer) @@ -161,15 +172,15 @@ mod tests { } #[test] - fn test_generate_png_to_jpg_with_resize() { - let png_path = get_test_png_path(); + fn test_generate_jpg_to_png_with_resize() { + let jpg_path = get_test_jpg_path(); let options = ImageProcessorOptions { - format: ImageFormat::Jpeg, + format: ImageFormat::Png, resize_options: Some(ImageResizeOptions::sized(100f32, 100f32)), ..Default::default() }; - let buffer = GenericImageProcessor::generate_from_path(&png_path, options) + let buffer = GenericImageProcessor::generate_from_path(&jpg_path, options) .expect("Failed to generate image buffer"); let dimensions = image::load_from_memory(&buffer) @@ -180,26 +191,45 @@ mod tests { assert_eq!(dimensions.1, 100); } + //JPG -> webp #[test] - fn test_generate_jpg_to_png() { + fn test_generate_jpg_to_webp_fail() { let jpg_path = get_test_jpg_path(); + let options = ImageProcessorOptions { + format: ImageFormat::Webp, + ..Default::default() + }; + + let result = GenericImageProcessor::generate_from_path(&jpg_path, options); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "An unknown error occurred: Incorrect image processor for requested format." + ); + } + + // PNG -> other + // PNG -> PNG + #[test] + fn test_generate_png_to_png() { + let png_path = get_test_png_path(); let options = ImageProcessorOptions { format: ImageFormat::Png, ..Default::default() }; - let buffer = GenericImageProcessor::generate_from_path(&jpg_path, options) + let buffer = GenericImageProcessor::generate_from_path(&png_path, options) .expect("Failed to generate image buffer"); assert!(!buffer.is_empty()); - // should be a valid PNG + // should *still* be a valid PNG assert!( image::load_from_memory_with_format(&buffer, image::ImageFormat::Png).is_ok() ); } #[test] - fn test_generate_jpg_to_png_with_rescale() { - let jpg_path = get_test_jpg_path(); + fn test_generate_png_to_png_with_rescale() { + let png_path = get_test_png_path(); let options = ImageProcessorOptions { format: ImageFormat::Png, resize_options: Some(ImageResizeOptions::scaled(0.5, 0.5)), @@ -207,9 +237,9 @@ mod tests { }; let current_dimensions = - image::image_dimensions(&jpg_path).expect("Failed to get dimensions"); + image::image_dimensions(&png_path).expect("Failed to get dimensions"); - let buffer = GenericImageProcessor::generate_from_path(&jpg_path, options) + let buffer = GenericImageProcessor::generate_from_path(&png_path, options) .expect("Failed to generate image buffer"); let new_dimensions = image::load_from_memory(&buffer) @@ -221,15 +251,15 @@ mod tests { } #[test] - fn test_generate_jpg_to_png_with_resize() { - let jpg_path = get_test_jpg_path(); + fn test_generate_png_to_png_with_resize() { + let png_path = get_test_png_path(); let options = ImageProcessorOptions { format: ImageFormat::Png, resize_options: Some(ImageResizeOptions::sized(100f32, 100f32)), ..Default::default() }; - let buffer = GenericImageProcessor::generate_from_path(&jpg_path, options) + let buffer = GenericImageProcessor::generate_from_path(&png_path, options) .expect("Failed to generate image buffer"); let dimensions = image::load_from_memory(&buffer) @@ -240,28 +270,30 @@ mod tests { assert_eq!(dimensions.1, 100); } + //PNG -> JPG #[test] - fn test_generate_png_to_png() { + fn test_generate_png_to_jpg() { let png_path = get_test_png_path(); let options = ImageProcessorOptions { - format: ImageFormat::Png, + format: ImageFormat::Jpeg, ..Default::default() }; let buffer = GenericImageProcessor::generate_from_path(&png_path, options) .expect("Failed to generate image buffer"); assert!(!buffer.is_empty()); - // should *still* be a valid PNG + // should be a valid JPEG assert!( - image::load_from_memory_with_format(&buffer, image::ImageFormat::Png).is_ok() + image::load_from_memory_with_format(&buffer, image::ImageFormat::Jpeg) + .is_ok() ); } #[test] - fn test_generate_png_to_png_with_rescale() { + fn test_generate_png_to_jpg_with_rescale() { let png_path = get_test_png_path(); let options = ImageProcessorOptions { - format: ImageFormat::Png, + format: ImageFormat::Jpeg, resize_options: Some(ImageResizeOptions::scaled(0.5, 0.5)), ..Default::default() }; @@ -281,10 +313,10 @@ mod tests { } #[test] - fn test_generate_png_to_png_with_resize() { + fn test_generate_png_to_jpg_with_resize() { let png_path = get_test_png_path(); let options = ImageProcessorOptions { - format: ImageFormat::Png, + format: ImageFormat::Jpeg, resize_options: Some(ImageResizeOptions::sized(100f32, 100f32)), ..Default::default() }; @@ -299,20 +331,4 @@ mod tests { assert_eq!(dimensions.0, 100); assert_eq!(dimensions.1, 100); } - - #[test] - fn test_generate_jpg_to_webp_fail() { - let jpg_path = get_test_jpg_path(); - let options = ImageProcessorOptions { - format: ImageFormat::Webp, - ..Default::default() - }; - - let result = GenericImageProcessor::generate_from_path(&jpg_path, options); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err().to_string(), - "An unknown error occurred: Incorrect image processor for requested format." - ); - } } diff --git a/core/src/filesystem/image/mod.rs b/core/src/filesystem/image/mod.rs index a0193a728..746abedd4 100644 --- a/core/src/filesystem/image/mod.rs +++ b/core/src/filesystem/image/mod.rs @@ -44,14 +44,15 @@ mod tests { .to_string() } + pub fn get_test_avif_path() -> String { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("integration-tests/data/example.avif") + .to_string_lossy() + .to_string() + } + // TODO(339): Avif + Jxl support // pub fn get_test_jxl_path() -> String { // PathBuf::from(env!("CARGO_MANIFEST_DIR")) // .join("integration-tests/data/example.jxl") - // } - - // pub fn get_test_avif_path() -> String { - // PathBuf::from(env!("CARGO_MANIFEST_DIR")) - // .join("integration-tests/data/example.avif") - // } } diff --git a/core/src/filesystem/image/process.rs b/core/src/filesystem/image/process.rs index bfde28391..0640df07a 100644 --- a/core/src/filesystem/image/process.rs +++ b/core/src/filesystem/image/process.rs @@ -58,6 +58,7 @@ pub enum ImageFormat { Jpeg, // JpegXl, Png, + // Avif, } impl ImageFormat { @@ -78,6 +79,7 @@ impl From for image::ImageFormat { fn from(val: ImageFormat) -> Self { match val { ImageFormat::Webp => image::ImageFormat::WebP, + // ImageFormat::Avif => image::ImageFormat::Avif, ImageFormat::Jpeg => image::ImageFormat::Jpeg, // See https://github.com/image-rs/image/issues/1765. Image removed the // unsupported enum variant, which makes this awkward to support... @@ -207,6 +209,7 @@ mod tests { #[test] fn test_image_format_extension() { assert_eq!(ImageFormat::Webp.extension(), "webp"); + // assert_eq!(ImageFormat::Avif.extension(), "avif"); assert_eq!(ImageFormat::Jpeg.extension(), "jpeg"); // assert_eq!(ImageFormat::JpegXl.extension(), "jxl"); assert_eq!(ImageFormat::Png.extension(), "png"); diff --git a/core/src/filesystem/media/pdf.rs b/core/src/filesystem/media/pdf.rs index 7405c8704..ea96cef06 100644 --- a/core/src/filesystem/media/pdf.rs +++ b/core/src/filesystem/media/pdf.rs @@ -6,7 +6,7 @@ use std::{ }; use pdf::file::FileOptions; -use pdfium_render::{prelude::Pdfium, render_config::PdfRenderConfig}; +use pdfium_render::prelude::{PdfRenderConfig, Pdfium}; use crate::{ config::StumpConfig, diff --git a/core/src/filesystem/scanner/library_scan_job.rs b/core/src/filesystem/scanner/library_scan_job.rs index a57c50bf4..88b16f4bd 100644 --- a/core/src/filesystem/scanner/library_scan_job.rs +++ b/core/src/filesystem/scanner/library_scan_job.rs @@ -409,10 +409,15 @@ impl JobExt for LibraryScanJob { format!("Scanning series at {}", path_buf.display()).as_str(), )); + // If the library is collection-priority, any child directories are 'ignored' and their + // files are part of / folded into the top-most folder (series). + // If the library is not collection-priority, each subdirectory is its own series. + // Therefore, we only scan one level deep when walking a series whose library is not + // collection-priority to avoid scanning duplicates which are part of other series let max_depth = self .options .as_ref() - .and_then(|o| o.is_collection_based().then_some(1)); + .and_then(|o| (!o.is_collection_based()).then_some(1)); let ignore_rules = generate_rule_set(&[ path_buf.clone(), diff --git a/core/src/filesystem/scanner/series_scan_job.rs b/core/src/filesystem/scanner/series_scan_job.rs index 3af9b3529..24da69a12 100644 --- a/core/src/filesystem/scanner/series_scan_job.rs +++ b/core/src/filesystem/scanner/series_scan_job.rs @@ -5,7 +5,9 @@ use specta::Type; use crate::{ db::{ - entity::{CoreJobOutput, LibraryOptions}, + entity::{ + macros::library_path_with_options_select, CoreJobOutput, LibraryOptions, + }, FileStatus, }, filesystem::image::{ThumbnailGenerationJob, ThumbnailGenerationJobParams}, @@ -13,7 +15,7 @@ use crate::{ error::JobError, Executor, JobExt, JobOutputExt, JobProgress, JobTaskOutput, WorkerCtx, WorkerSendExt, WorkingState, WrappedJob, }, - prisma::{library, library_options, media, series, PrismaClient}, + prisma::{library, media, series, PrismaClient}, utils::chain_optional_iter, CoreEvent, }; @@ -94,23 +96,28 @@ impl JobExt for SeriesScanJob { ctx: &WorkerCtx, ) -> Result, JobError> { let mut output = Self::Output::default(); - let library_options = ctx + let library = ctx .db - .library_options() - .find_first(vec![library_options::library::is(vec![ - library::series::some(vec![ - series::id::equals(self.id.clone()), - series::path::equals(self.path.clone()), - ]), + .library() + .find_first(vec![library::series::some(vec![ + series::id::equals(self.id.clone()), + series::path::equals(self.path.clone()), ])]) + .select(library_path_with_options_select::select()) .exec() .await? - .map(LibraryOptions::from) .ok_or(JobError::InitFailed( - "Associated library options not found".to_string(), + "Associated library not found".to_string(), ))?; + let library_options = LibraryOptions::from(library.library_options); let ignore_rules = generate_rule_set(&[PathBuf::from(self.path.clone())]); - let max_depth = library_options.is_collection_based().then_some(1); + + // If the library is collection-priority, any child directories are 'ignored' and their + // files are part of / folded into the top-most folder (series). + // If the library is not collection-priority, each subdirectory is its own series. + // Therefore, we only scan one level deep when walking a series whose library is not + // collection-priority to avoid scanning duplicates which are part of other series + let max_depth = (!library_options.is_collection_based()).then_some(1); self.options = Some(library_options); diff --git a/core/src/filesystem/scanner/walk.rs b/core/src/filesystem/scanner/walk.rs index 15d007db9..a32ea2552 100644 --- a/core/src/filesystem/scanner/walk.rs +++ b/core/src/filesystem/scanner/walk.rs @@ -235,13 +235,19 @@ pub async fn walk_series(path: &Path, ctx: WalkerCtx) -> CoreResult, /// The SMTP host to use pub host: String, /// The SMTP port to use @@ -62,7 +64,7 @@ impl EmailerClient { /// Create a new [EmailerClient] instance with the given configuration and template directory. /// /// # Example - /// ```rust + /// ```no_run /// use email::{EmailerClient, EmailerClientConfig}; /// use std::path::PathBuf; /// @@ -70,7 +72,7 @@ impl EmailerClient { /// sender_email: "aaron@stumpapp.dev".to_string(), /// sender_display_name: "Aaron's Stump Instance".to_string(), /// username: "aaron@stumpapp.dev".to_string(), - /// password: "decrypted_password".to_string(), + /// password: Some("decrypted_password".to_string()), /// host: "smtp.stumpapp.dev".to_string(), /// port: 587, /// tls_enabled: true, @@ -91,7 +93,7 @@ impl EmailerClient { /// Internally, this will just call [EmailerClient::send_attachments] with a single attachment. /// /// # Example - /// ```rust + /// ```no_run /// use email::{AttachmentPayload, EmailerClient, EmailerClientConfig}; /// use std::path::PathBuf; /// use lettre::message::header::ContentType; @@ -101,7 +103,7 @@ impl EmailerClient { /// sender_email: "aaron@stumpapp.dev".to_string(), /// sender_display_name: "Aaron's Stump Instance".to_string(), /// username: "aaron@stumpapp.dev".to_string(), - /// password: "decrypted_password".to_string(), + /// password: Some("decrypted_password".to_string()), /// host: "smtp.stumpapp.dev".to_string(), /// port: 587, /// tls_enabled: true, @@ -137,7 +139,7 @@ impl EmailerClient { /// The attachments are sent as a multipart email, with the first attachment being the email body. /// /// # Example - /// ```rust + /// ```no_run /// use email::{AttachmentPayload, EmailerClient, EmailerClientConfig}; /// use std::path::PathBuf; /// use lettre::message::header::ContentType; @@ -147,7 +149,7 @@ impl EmailerClient { /// sender_email: "aaron@stumpapp.dev".to_string(), /// sender_display_name: "Aaron's Stump Instance".to_string(), /// username: "aaron@stumpapp.dev".to_string(), - /// password: "decrypted_password".to_string(), + /// password: Some("decrypted_password".to_string()), /// host: "smtp.stumpapp.dev".to_string(), /// port: 587, /// tls_enabled: true, @@ -218,8 +220,13 @@ impl EmailerClient { .subject(subject) .multipart(multipart_builder)?; - let creds = - Credentials::new(self.config.username.clone(), self.config.password.clone()); + let password = self + .config + .password + .as_deref() + .ok_or(EmailError::NoPassword)? + .to_string(); + let creds = Credentials::new(self.config.username.clone(), password); // Note this issue: https://github.com/lettre/lettre/issues/359 let transport = if self.config.tls_enabled { diff --git a/crates/email/src/error.rs b/crates/email/src/error.rs index 6c92865d4..41a38b181 100644 --- a/crates/email/src/error.rs +++ b/crates/email/src/error.rs @@ -10,6 +10,8 @@ pub enum EmailError { InvalidEmail(String), #[error("Failed to build email: {0}")] EmailBuildFailed(#[from] lettre::error::Error), + #[error("The emailer config is missing a password")] + NoPassword, #[error("Failed to send email: {0}")] SendFailed(#[from] smtp::Error), #[error("Failed to register template: {0}")] diff --git a/crates/email/src/template.rs b/crates/email/src/template.rs index a9d0c77e2..e5eed1979 100644 --- a/crates/email/src/template.rs +++ b/crates/email/src/template.rs @@ -21,7 +21,7 @@ impl AsRef for EmailTemplate { /// Render a template to a string using the given data and templates directory. /// /// # Example -/// ```rust +/// ```no_run /// use email::{render_template, EmailTemplate}; /// use serde_json::json; /// use std::path::PathBuf; diff --git a/crates/stump-config-gen/Cargo.toml b/crates/stump-config-gen/Cargo.toml new file mode 100644 index 000000000..10d08da3a --- /dev/null +++ b/crates/stump-config-gen/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "stump-config-gen" +version = "0.0.6" +description = "A procedural macro for generating the StumpConfig struct." +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.86" +quote = "1.0.36" +syn = "2.0.75" + +[dev-dependencies] +serde = { workspace = true } +thiserror = { workspace = true } +itertools = { workspace = true } +toml = { workspace = true } +temp-env = "0.3.6" diff --git a/crates/stump-config-gen/src/config_vars.rs b/crates/stump-config-gen/src/config_vars.rs new file mode 100644 index 000000000..32581bc3a --- /dev/null +++ b/crates/stump-config-gen/src/config_vars.rs @@ -0,0 +1,146 @@ +use proc_macro2::{Span, TokenStream}; +use syn::{DataStruct, Expr, Field, Fields, Ident}; + +use crate::type_utils; + +pub struct StumpConfigVariable { + pub span: Span, + pub variable_name: Ident, + pub variable_type: TokenStream, + pub is_optional: bool, + pub is_vec: bool, + pub attributes: StumpConfigVariableAttributes, +} + +pub struct StumpConfigVariableAttributes { + pub default_value: Option, + pub debug_value: Option, + pub required_by_new: bool, + pub env_key: Option, + pub validator: Option, +} + +impl StumpConfigVariable { + pub fn is_parse_type(&self) -> bool { + let parse_types = [ + "u8", "u16", "u32", "u64", "i8", "i16", "i32", "i64", "f16", "f32", "f64", + "usize", "bool", + ]; + let type_str = if self.is_vec { + self.variable_type + .to_string() + .trim_start_matches("Vec<") + .trim_end_matches('>') + .to_string() + } else { + self.variable_type.to_string() + }; + + parse_types.contains(&type_str.as_str()) + } + + pub fn error(&self, err: T) -> TokenStream { + syn::Error::new(self.span, err).into_compile_error() + } +} + +/// Parses a struct, extracting a vec representing each of its fields +/// so that they can be reprsented in the macro's output code. +pub fn get_config_variables(data_struct: &DataStruct) -> Vec { + let mut output_vec = Vec::new(); + + if let Fields::Named(fields_named) = &data_struct.fields { + for field in fields_named.named.iter() { + // Get the name of the variable as a String + let variable_name = field.ident.as_ref().unwrap().clone(); + + // Parse the field's type info + let (variable_type, is_optional, is_vec) = + type_utils::parse_field_type(field); + // Parse attributes + let attributes = parse_config_var_attributes(field); + + // Then we add a new variable + output_vec.push(StumpConfigVariable { + span: variable_name.span(), + variable_name, + variable_type, + is_optional, + is_vec, + attributes, + }) + } + } + + output_vec +} + +/// Parses the attributes on a config variable, this is where +/// any of the #[attribute] values are unwrapped and stored +fn parse_config_var_attributes(field: &Field) -> StumpConfigVariableAttributes { + let mut default_value = None; + let mut debug_value = None; + let mut required_by_new = false; + let mut env_key = None; + let mut validator = None; + let field_ident = field.ident.as_ref().unwrap(); + + for attr in &field.attrs { + // #[default_value(Expr)] + if attr.path().is_ident("default_value") { + let default_value_expr: Expr = attr.parse_args().unwrap_or_else(|e| { + panic!( + "Failed to parse default_value expression for {}: {}", + field_ident, e + ) + }); + default_value = Some(default_value_expr); + } + + // #[required_by_new] + if attr.path().is_ident("required_by_new") { + required_by_new = true; + } + + // #[env_key(Expr)] + if attr.path().is_ident("env_key") { + let env_key_expr: Expr = attr.parse_args().unwrap_or_else(|e| { + panic!( + "Failed to parse env_key expression for {}: {}", + field_ident, e + ) + }); + env_key = Some(env_key_expr); + } + + // #[debug_value(Expr)] + if attr.path().is_ident("debug_value") { + let debug_value_expr: Expr = attr.parse_args().unwrap_or_else(|e| { + panic!( + "Failed to parse debug_value expression for {}: {}", + field_ident, e + ) + }); + debug_value = Some(debug_value_expr); + } + + // #[validator(fn)] + if attr.path().is_ident("validator") { + let validator_iden: Ident = attr.parse_args().unwrap_or_else(|e| { + panic!( + "Failed to parse validator identity for {}: {}", + field_ident, e + ) + }); + validator = Some(validator_iden); + } + } + + StumpConfigVariableAttributes { + default_value, + debug_value, + required_by_new, + env_key, + validator, + } +} diff --git a/crates/stump-config-gen/src/gen_config_impls.rs b/crates/stump-config-gen/src/gen_config_impls.rs new file mode 100644 index 000000000..5749e2a97 --- /dev/null +++ b/crates/stump-config-gen/src/gen_config_impls.rs @@ -0,0 +1,192 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::Ident; + +use crate::{config_vars::StumpConfigVariable, InputAttributes}; + +pub fn gen_stump_config_impls( + struct_ident: &Ident, + input_attrs: &InputAttributes, + config_vars: &[StumpConfigVariable], +) -> TokenStream { + let new_impl = gen_new_impl(config_vars); + let debug_impl = gen_debug_impl(config_vars); + + let partial_struct_name = format_ident!("Partial{}", struct_ident); + let with_file_impl = gen_with_file_impl(&partial_struct_name, input_attrs); + let with_env_impl = gen_with_env_impl(&partial_struct_name, config_vars); + + quote! { + impl #struct_ident { + #new_impl + #debug_impl + + #with_file_impl + + #with_env_impl + } + } +} + +fn gen_new_impl(config_vars: &[StumpConfigVariable]) -> TokenStream { + let mut setters: Vec = Vec::new(); + let mut args: Vec = Vec::new(); + + for var in config_vars { + let name = &var.variable_name; + + if var.attributes.required_by_new { + let var_type = &var.variable_type; + + args.push(quote! {#name: #var_type}); + setters.push(quote! {#name: #name}) + } else { + if var.attributes.default_value.is_none() { + setters.push( + var.error(format!("{} needs a default value", var.variable_name)), + ); + continue; + } + + let default_expr = var.attributes.default_value.as_ref().unwrap(); + setters.push(quote! {#name: #default_expr}) + } + } + + quote! { + pub fn new(#(#args),*) -> Self { + Self { + #(#setters),* + } + } + } +} + +fn gen_debug_impl(config_vars: &[StumpConfigVariable]) -> TokenStream { + let mut setters: Vec = Vec::new(); + + for var in config_vars { + let name = &var.variable_name; + + let setter = match (&var.attributes.debug_value, &var.attributes.default_value) { + (Some(debug), _) => quote! {#name: #debug}, + (None, Some(default)) => quote! {#name: #default}, + (None, None) => { + var.error("Must set a default_value or debug_value for each variable") + }, + }; + setters.push(setter); + } + + quote! { + pub fn debug() -> Self { + Self { + #(#setters),* + } + } + } +} + +fn gen_with_file_impl( + partial_struct_name: &Ident, + input_attrs: &InputAttributes, +) -> TokenStream { + let config_file_loc = &input_attrs.config_file_location; + quote! { + #[doc="Looks for the config directory, loading its contents and replacing stored configuration"] + #[doc="variables with those contents. If the config file doesn't exist, the stored variables"] + #[doc="remain unchanged and the function returns `Ok`."] + pub fn with_config_file(mut self) -> crate::CoreResult { + let config_toml = #config_file_loc; + + if !config_toml.exists() { + return Ok(self); + } + + let toml_content_str = std::fs::read_to_string(config_toml)?; + let toml_configs = toml::from_str::<#partial_struct_name>(&toml_content_str) + .map_err(|e| { + eprintln!("Failed to parse Stump config file: {}", e); + crate::CoreError::InitializationError(e.to_string()) + })?; + + toml_configs.apply_to_config(&mut self); + Ok(self) + } + } +} + +fn gen_with_env_impl( + partial_struct_name: &Ident, + config_vars: &[StumpConfigVariable], +) -> TokenStream { + let env_var_extractors = config_vars.iter().map(gen_env_var_extractors); + + quote! { + #[doc="Loads configuration variables from the environment, replacing stored"] + #[doc="values with the environment values."] + pub fn with_environment(mut self) -> crate::CoreResult { + let mut env_configs = #partial_struct_name::empty(); + + #(#env_var_extractors)* + + env_configs.apply_to_config(&mut self); + Ok(self) + } + } +} + +fn gen_env_var_extractors(var: &StumpConfigVariable) -> TokenStream { + if let Some(var_env_key) = &var.attributes.env_key { + let env_key_str = var_env_key.to_token_stream().to_string(); + let var_name = &var.variable_name; + let var_type = &var.variable_type; + + let is_parse_type = var.is_parse_type(); + // Handle types that need to be parsed from strings + if is_parse_type && !var.is_vec { + return quote! { + if let Ok(#var_name) = env::var(#var_env_key) { + let parsed_val = #var_name.parse::<#var_type>().map_err(|e| { + eprintln!("Failed to parse provided {}: {}", #env_key_str, e); + crate::CoreError::InitializationError(e.to_string()) + })?; + env_configs.#var_name = Some(parsed_val); + } + }; + } + + // Handle types that need to be parsed from String AND into Vec. + if is_parse_type && var.is_vec { + return var.error( + "The config generator macro doesn't implement Vec parsable types.", + ); + } + + // Handle types that don't need to be parsed, but are Vec + if !is_parse_type && var.is_vec { + return quote! { + if let Ok(#var_name) = env::var(#var_env_key) { + if !#var_name.is_empty() { + env_configs.#var_name = Some( + #var_name + .split(',') + .map(|val| val.trim().to_string()) + .collect_vec(), + ) + } + } + }; + } + + // Handle non-Vec non-parse types + return quote! { + if let Ok(#var_name) = env::var(#var_env_key) { + env_configs.#var_name = Some(#var_name); + } + }; + } + + // If there isn't an env key we can return an empty set of tokens + quote! {} +} diff --git a/crates/stump-config-gen/src/gen_partial_config.rs b/crates/stump-config-gen/src/gen_partial_config.rs new file mode 100644 index 000000000..8b1f58396 --- /dev/null +++ b/crates/stump-config-gen/src/gen_partial_config.rs @@ -0,0 +1,106 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::Ident; + +use crate::config_vars::StumpConfigVariable; + +pub fn gen_partial_stump_config( + struct_ident: &Ident, + config_vars: &[StumpConfigVariable], +) -> proc_macro2::TokenStream { + let mut struct_defs = Vec::new(); + let mut empty_setters = Vec::new(); + + for var in config_vars { + let name = &var.variable_name; + let type_name = &var.variable_type; + + struct_defs.push(quote! {pub #name: Option<#type_name>}); + empty_setters.push(quote! {#name: None}); + } + + let empty_fn_impl = gen_empty_impl(&empty_setters); + let apply_fn_impl = gen_apply_impl(struct_ident, config_vars); + let partial_struct_name = format_ident!("Partial{}", struct_ident); + + quote! { + #[derive(serde::Deserialize, Debug, Clone, PartialEq)] + struct #partial_struct_name { + #(#struct_defs),* + } + + impl #partial_struct_name { + #empty_fn_impl + #apply_fn_impl + } + } +} + +fn gen_empty_impl(empty_setters: &[TokenStream]) -> TokenStream { + quote! { + pub fn empty() -> Self { + Self { + #(#empty_setters),* + } + } + } +} + +fn gen_apply_impl( + struct_ident: &Ident, + config_vars: &[StumpConfigVariable], +) -> TokenStream { + // Generate the setters for each config var + let apply_setters = config_vars.iter().map(config_var_to_setter); + + // Output the final function implementation + quote! { + pub fn apply_to_config(self, config: &mut #struct_ident) { + #(#apply_setters)* + } + } +} + +fn config_var_to_setter(var: &StumpConfigVariable) -> TokenStream { + let var_name = &var.variable_name; + + let setter = match (var.is_vec, var.is_optional) { + (true, true) => { + var.error("The config macro doesn't support Option> config variables.") + }, + (true, false) => { + let orig_var_name = format_ident!("orig_{}", var_name); + + quote! { + let #orig_var_name = config.#var_name.clone(); + config + .#var_name + .extend(#var_name.into_iter().filter(|x| !#orig_var_name.contains(x))); + } + }, + (false, true) => quote! { + config.#var_name = Some(#var_name); + }, + (false, false) => quote! { + config.#var_name = #var_name; + }, + }; + + // This portion wraps the variable-specific setter in a check for Some() in the partial config + // and, if there is a validator set, only applies the variable if it passes, and then returns. + if let Some(validator) = &var.attributes.validator { + quote! { + if let Some(#var_name) = self.#var_name { + if #validator(&#var_name) { + #setter + } + } + } + } else { + quote! { + if let Some(#var_name) = self.#var_name { + #setter + } + } + } +} diff --git a/crates/stump-config-gen/src/lib.rs b/crates/stump-config-gen/src/lib.rs new file mode 100644 index 000000000..81bf6016a --- /dev/null +++ b/crates/stump-config-gen/src/lib.rs @@ -0,0 +1,120 @@ +mod config_vars; +mod gen_config_impls; +mod gen_partial_config; +mod type_utils; + +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput, Expr}; + +use gen_config_impls::gen_stump_config_impls; +use gen_partial_config::gen_partial_stump_config; + +/// A procedural macro for generating the internals of the configuration struct +/// used in Stump. +/// +/// This macro generates four main functions for the struct that it is applied to: +/// `new`, `debug`, `with_config_file`, and `with_environment`. Each member of the +/// struct should be annotated to indicate how each should be determined. The +/// following section explains each available annotation. +/// +/// ### `#[default_value(Expr)]` +/// +/// This attribute is applied to a field of the input struct to define a default value +/// for the variable. Whichever expression is provided will be used in the `new(...)` +/// function to set the value of the output. This value must be set unless you use the +/// `required_by_new` attribute (described below). +/// +/// ### `#[debug_value(Expr)]` +/// +/// This attribute is applied to a field of the input struct to indicate what its value +/// should be when constructing the `debug()` output. If no `debug_value` is set, the +/// `default_value` is used instead. Each field *must* have at least one of `debug_value` +/// or `default_value` set. +/// +/// ### `#[required_by_new]` +/// +/// This attribute indicates that a field of the input will be a required parameter of the +/// `new` function constructed by the macro. Using `required_by_new` will override any +/// `default_value` set, using the `new` function parameter's value instead. +/// +/// ### `#[env_key(Expr)]` +/// +/// This attribute indicates the environment variable from which the field it is applied to +/// is derived using the `with_environment` function. Inputs can be a `str` value or `const` +/// (or, indeed, any expression). Variables without this annotation will not be included in +/// the `with_environment` function at all. +/// +/// ### `#[validator(Fn)]` +/// +/// This attribute optionally allows a validation function to be defined which will run before +/// any value of type `T` is written to the struct when running `with_environment` or +/// `with_config_file`. The function must have the signature `Fn(T) -> bool`, with a `true` +/// result causing the variable to be written, and a `false` result causing it not to be written. +/// +/// ### `#[config_file_location(Expr)]` +/// +/// This attribute should be applied directly to the input struct (i.e., below the `derive` line) +/// and provides an expression that can be used to determine the config file path that should be +/// opened when the `with_config_file` function runs. Any expression works here, including a +/// reference to a private function of the input struct. +/// +#[proc_macro_derive( + StumpConfigGenerator, + attributes( + default_value, + debug_value, + required_by_new, + env_key, + validator, + config_file_location + ) +)] +pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let input_attrs = parse_input_attrs(&ast); + + // Make sure it's a struct and unwrap it + if let Data::Struct(data_struct) = &ast.data { + let struct_ident = &ast.ident; + let config_vars = config_vars::get_config_variables(data_struct); + + let generated_partial_config = + gen_partial_stump_config(struct_ident, &config_vars); + + let generated_config_impls = + gen_stump_config_impls(struct_ident, &input_attrs, &config_vars); + + let tokens = quote! { + #generated_partial_config + + #generated_config_impls + }; + + tokens.into() + } else { + panic!("Can only generate a config from a struct"); + } +} + +struct InputAttributes { + pub config_file_location: Expr, +} + +fn parse_input_attrs(ast: &DeriveInput) -> InputAttributes { + let mut maybe_config_file_location = None; + + for attr in &ast.attrs { + if attr.path().is_ident("config_file_location") { + let config_file_expr: Expr = attr + .parse_args() + .expect("Failed to parse config_file_location expression"); + maybe_config_file_location = Some(config_file_expr); + } + } + + let config_file_location = + maybe_config_file_location.expect("config_file_location must be defined."); + InputAttributes { + config_file_location, + } +} diff --git a/crates/stump-config-gen/src/type_utils.rs b/crates/stump-config-gen/src/type_utils.rs new file mode 100644 index 000000000..506f1b7ce --- /dev/null +++ b/crates/stump-config-gen/src/type_utils.rs @@ -0,0 +1,99 @@ +use proc_macro2::TokenStream; +use syn::{Field, Type, TypePath}; + +pub fn parse_field_type(field: &Field) -> (TokenStream, bool, bool) { + match &field.ty { + Type::Path(type_path) => { + let type_name = type_path.path.segments.last().unwrap().ident.to_string(); + let is_optional = type_name == "Option"; + + // If it's an Option get the inner type name + let (type_tokens, is_vec) = if is_optional { + parse_option_inner_type(type_path) + } + // If it's a Vec we want both Vec and T. + else if type_name == "Vec" { + let vector_type = get_vector_type(type_path); + (vector_type, true) + } else { + let non_vector_type = type_name.parse::().unwrap(); + (non_vector_type, false) + }; + + (type_tokens, is_optional, is_vec) + }, + _ => panic!("Unsupported type"), + } +} + +pub fn parse_option_inner_type(type_path: &TypePath) -> (TokenStream, bool) { + // Unravel the Option + match &type_path.path.segments.last().unwrap().arguments { + // Match on angle bracketed arguments only + syn::PathArguments::AngleBracketed(args) => { + // If we have a type T between the angle brackets + if let Some(syn::GenericArgument::Type(Type::Path(inner_type_path))) = + args.args.first() + { + // Get type T as a String + let inner_path_name = inner_type_path + .path + .segments + .last() + .unwrap() + .ident + .to_string(); + + // If T is a Vec then we need to do more processing + if inner_path_name == "Vec" { + let vector_type = get_vector_type(inner_type_path); + (vector_type, true) + } + // Otherwise we can just parse and return it + else { + let non_vector_type = inner_path_name.parse::().unwrap(); + (non_vector_type, false) + } + } + // This shouldn't happen, but would occur if there were empty angle brackets + else { + panic!("Failed to unravel Option, are angle brackets empty?"); + } + }, + // This shouldn't happen either, but would occur if Option had no angle backets + _ => panic!("Failed to unravel Option, is there no type for Option?"), + } +} + +pub fn get_vector_type(type_path: &TypePath) -> TokenStream { + // Unravel the Vec + match &type_path.path.segments.last().unwrap().arguments { + // Match on angle bracketed arguments only + syn::PathArguments::AngleBracketed(args) => { + // Make sure we have a T between the angle brackets + if let Some(syn::GenericArgument::Type(Type::Path(inner_type_path))) = + args.args.first() + { + // Get the T as a String + let inner_path_name = inner_type_path + .path + .segments + .last() + .unwrap() + .ident + .to_string(); + + // Output the full type name, by inserting the T name into Vec + format!("Vec<{}>", inner_path_name) + .parse::() + .unwrap() + } + // This shouldn't happen, but would occur if the angle brackets were empty + else { + panic!("Failed to unravel Vec, are angle brackets empty?") + } + }, + // This shouldn't happen either, but would occur if Vec had no type T + _ => panic!("Failed to unravel Vec, is there no type for Vec?"), + } +} diff --git a/crates/stump-config-gen/tests/basic_tests.rs b/crates/stump-config-gen/tests/basic_tests.rs new file mode 100644 index 000000000..c33b01c6b --- /dev/null +++ b/crates/stump-config-gen/tests/basic_tests.rs @@ -0,0 +1,133 @@ +// Because the macro expects these to exist at the crate root +use thiserror::Error; + +type CoreResult = Result; + +#[derive(Error, Debug)] +enum CoreError { + #[error("Failed to initialize: {0}")] + InitializationError(String), + #[error("Failed to read file: {0}")] + IoError(#[from] std::io::Error), +} + +use std::{env, path::PathBuf}; + +use itertools::Itertools; +use serde::Deserialize; + +use stump_config_gen::StumpConfigGenerator; + +#[derive(StumpConfigGenerator, Deserialize)] +#[config_file_location(get_mock_config_file())] +struct EmptyConfig {} + +mod env_keys { + pub const PORT_ENV_KEY: &str = "PORT_ENV_KEY"; + pub const LIST_OF_NAMES_KEY: &str = "LIST_OF_NAMES_KEY"; + pub const MAYBE_BOO_KEY: &str = "MAYBE_BOO_KEY"; +} +use env_keys::*; + +#[derive(StumpConfigGenerator, Deserialize, PartialEq, Debug)] +#[config_file_location(get_mock_config_file())] +struct BasicConfig { + #[default_value(3000)] + #[env_key(PORT_ENV_KEY)] + pub port: u32, + + #[default_value(vec![])] + #[debug_value(vec!["Alice".to_string(), "Bob".to_string()])] + #[env_key(LIST_OF_NAMES_KEY)] + pub list_of_names: Vec, + + #[default_value(None)] + #[env_key(MAYBE_BOO_KEY)] + pub maybe_boo: Option, +} + +#[test] +fn test_create_new() { + let config = BasicConfig::new(); + + assert_eq!( + config, + BasicConfig { + port: 3000, + list_of_names: vec![], + maybe_boo: None, + } + ); +} + +#[test] +fn test_apply_partial_to_debug() { + let mut config = BasicConfig::debug(); + + let partial_config = PartialBasicConfig { + port: Some(3333), + list_of_names: Some(vec!["Carmen".to_string()]), + maybe_boo: Some("Boo".to_string()), + }; + + partial_config.apply_to_config(&mut config); + + assert_eq!( + config, + BasicConfig { + port: 3333, + list_of_names: vec![ + "Alice".to_string(), + "Bob".to_string(), + "Carmen".to_string(), + ], + maybe_boo: Some("Boo".to_string()), + } + ); +} + +#[test] +fn test_getting_config_from_environment() { + temp_env::with_vars( + [ + (PORT_ENV_KEY, Some("1337")), + (LIST_OF_NAMES_KEY, Some("Alice,Bob")), + (MAYBE_BOO_KEY, Some("Boo")), + ], + || { + let config = BasicConfig::new() + .with_environment() + .expect("Failed to fetch config from environment"); + + assert_eq!( + config, + BasicConfig { + port: 1337, + list_of_names: vec!["Alice".to_string(), "Bob".to_string(),], + maybe_boo: Some("Boo".to_string()), + } + ); + }, + ) +} + +#[test] +fn test_getting_config_from_toml() { + let config = BasicConfig::new() + .with_config_file() + .expect("Failed to fetch config from file"); + + assert_eq!( + config, + BasicConfig { + port: 2222, + list_of_names: vec!["Bob".to_string(), "Carmen".to_string(),], + maybe_boo: Some("Boo".to_string()), + } + ); +} + +#[allow(unused)] +fn get_mock_config_file() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/basic-config.toml") +} diff --git a/crates/stump-config-gen/tests/data/basic-config.toml b/crates/stump-config-gen/tests/data/basic-config.toml new file mode 100644 index 000000000..1bffc279f --- /dev/null +++ b/crates/stump-config-gen/tests/data/basic-config.toml @@ -0,0 +1,3 @@ +port = 2222 +list_of_names = ["Bob", "Carmen"] +maybe_boo = "Boo" diff --git a/docker/Dockerfile b/docker/Dockerfile index 11f97d303..49f230079 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,19 +2,20 @@ # Frontend Build Stage # ------------------------------------------------------------------------------ -FROM node:20.0.0-alpine3.16 as frontend +FROM node:20.0.0-alpine3.16 AS frontend ARG TARGETARCH WORKDIR /app COPY . . -# https://github.com/nodejs/docker-node/issues/1335 -RUN yarn config set network-timeout 300000 && \ - yarn install --frozen-lockfile && \ - yarn web build && \ - mv ./apps/web/dist/ ./build && \ - if [ ! -d "./build" ] || [ ! "$(ls -A ./build)" ]; then exit 1; fi +RUN --mount=type=cache,target=/usr/local/yarn/.cache,id=${TARGETARCH} \ + # https://github.com/nodejs/docker-node/issues/1335 + yarn config set network-timeout 300000 && \ + YARN_CACHE_FOLDER=/usr/local/yarn/.cache yarn install --frozen-lockfile && \ + yarn web build && \ + mv ./apps/web/dist/ ./build && \ + if [ ! -d "./build" ] || [ ! "$(ls -A ./build)" ]; then exit 1; fi # ------------------------------------------------------------------------------ # Cargo Build Stage @@ -23,27 +24,31 @@ RUN yarn config set network-timeout 300000 && \ FROM rust:1.79.0-slim-buster AS builder ARG GIT_REV -ENV GIT_REV=${GIT_REV} - -ARG TAGS -ENV TAGS=${TAGS} +ARG TARGETARCH +ARG RUN_PRISMA_GENERATE +ENV GIT_REV=${GIT_REV} +ENV RUN_PRISMA_GENERATE=${RUN_PRISMA_GENERATE} + +RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \ + apt-get update && apt-get install -y \ + build-essential \ + cmake \ + wget \ + git \ + libssl-dev \ + pkg-config \ + libsqlite3-dev; + +# Cargo build for stump WORKDIR /app -RUN apt-get update && apt-get install -y \ - build-essential \ - cmake \ - git \ - libssl-dev \ - pkg-config \ - libsqlite3-dev; - COPY . . -RUN ./scripts/release/utils.sh -w; \ - set -ex; \ - ./scripts/release/utils.sh -p; \ - cargo build --package stump_server --bin stump_server --release; \ + +RUN --mount=type=cache,target=/usr/local/cargo/registry,id=${TARGETARCH} \ + --mount=type=cache,target=/app/target,id=${TARGETARCH} \ + RUN_PRISMA_GENERATE=${RUN_PRISMA_GENERATE} ./docker/build_server.sh && \ cp ./target/release/stump_server ./stump_server # ------------------------------------------------------------------------------ @@ -53,18 +58,19 @@ RUN ./scripts/release/utils.sh -w; \ FROM debian:buster-slim AS pdfium ARG TARGETARCH -RUN apt-get update && apt-get install -y curl tar; \ - # Download and extract PDFium - set -ex; \ - mkdir -p pdfium; \ - if [ "$TARGETARCH" = "amd64" ]; then \ - # NOTE: This was previously -x86, need to test more on amd64-compatible systems to ensure I have the right one - curl -sLo pdfium.tgz https://github.com/bblanchon/pdfium-binaries/releases/download/chromium/6406/pdfium-linux-x64.tgz; \ - elif [ "$TARGETARCH" = "arm64" ]; then \ - curl -sLo pdfium.tgz https://github.com/bblanchon/pdfium-binaries/releases/download/chromium/6406/pdfium-linux-arm64.tgz; \ - fi; \ - tar -xzvf pdfium.tgz -C ./pdfium; \ - rm pdfium.tgz +RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \ + apt-get update && apt-get install -y curl tar; \ + # Download and extract PDFium + set -ex; \ + mkdir -p pdfium; \ + if [ "$TARGETARCH" = "amd64" ]; then \ + # NOTE: This was previously -x86, need to test more on amd64-compatible systems to ensure I have the right one + curl -sLo pdfium.tgz https://github.com/bblanchon/pdfium-binaries/releases/download/chromium/6406/pdfium-linux-x64.tgz; \ + elif [ "$TARGETARCH" = "arm64" ]; then \ + curl -sLo pdfium.tgz https://github.com/bblanchon/pdfium-binaries/releases/download/chromium/6406/pdfium-linux-arm64.tgz; \ + fi; \ + tar -xzvf pdfium.tgz -C ./pdfium; \ + rm pdfium.tgz # ------------------------------------------------------------------------------ # Final Stage @@ -80,8 +86,11 @@ COPY --from=pdfium /pdfium /opt/pdfium COPY --from=frontend /app/build /app/client COPY docker/entrypoint.sh /entrypoint.sh + RUN chmod +x /entrypoint.sh; \ ln -s /opt/pdfium/lib/libpdfium.so /lib/libpdfium.so; \ + echo "/usr/local/lib" >> /etc/ld.so.conf.d/mylibs.conf \ + && ldconfig; \ if [ ! -d "/app/client" ] || [ ! "$(ls -A /app/client)" ]; then exit 1; fi # Default Stump environment variables @@ -95,4 +104,4 @@ ENV STUMP_CONFIG_DIR=/config \ WORKDIR /app -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/build.ps1 b/docker/build.ps1 index 898b957a0..03996236c 100644 --- a/docker/build.ps1 +++ b/docker/build.ps1 @@ -2,6 +2,7 @@ $FORMAT = if ($args[0]) { $args[0] } else { "auto" } $PLATFORMS = if ($args[1]) { $args[1] } else { "linux/amd64" } $TAG = if ($args[2]) { $args[2] } else { "nightly" } +$RUN_PRISMA_GENERATE = if ($args[3]) { $args[3] } else { "false" } # Get the current Git revision $GIT_REV = git rev-parse --short HEAD @@ -13,4 +14,5 @@ docker buildx build ` --progress=$FORMAT ` --platform=$PLATFORMS ` -t aaronleopold/stump:$TAG ` - --build-arg GIT_REV=$GIT_REV . + --build-arg GIT_REV=$GIT_REV ` + --build-arg RUN_PRISMA_GENERATE=$RUN_PRISMA_GENERATE . diff --git a/docker/build.sh b/docker/build.sh index 371aef626..e0abea75a 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -1,7 +1,32 @@ #!/bin/bash -FORMAT=${1:-auto} -PLATFORMS=${2:-linux/amd64} -TAG=${3:-nightly} +_ENGINE=${ENGINE:-docker} +_FORMAT=${FORMAT:-auto} +_PLATFORMS=${PLATFORMS:-linux/amd64} +_TAGS=${TAGS:-"aaronleopold/stump:nightly"} +_RUN_PRISMA_GENERATE=${RUN_PRISMA_GENERATE:=false} +_GIT_REV=${GIT_REV:-$(git rev-parse --short HEAD)} +_PUSH=${PUSH:-false} -docker buildx build -f ./docker/Dockerfile --load --progress=$FORMAT --platform=$PLATFORMS -t aaronleopold/stump:$TAG --build-arg GIT_REV=$(git rev-parse --short HEAD) . +FORMATTED_TAGS="" +for tag in ${_TAGS//,/}; do + FORMATTED_TAGS="$FORMATTED_TAGS --tag $tag" +done + +PUSH_OR_LOAD_ARG="--load" +# Not supported? https://github.com/containers/buildah/issues/4671 +# if [ "$_PUSH" = true ]; then +# PUSH_OR_LOAD_ARG="--output=type=registry" +# fi + +echo "Building with tag arguments: $FORMATTED_TAGS" + +set -ex; \ +${_ENGINE} buildx build \ + -f ./docker/Dockerfile \ + ${PUSH_OR_LOAD_ARG} \ + --progress=$_FORMAT \ + --platform=$_PLATFORMS \ + ${FORMATTED_TAGS} \ + --build-arg GIT_REV=$_GIT_REV \ + --build-arg RUN_PRISMA_GENERATE=$_RUN_PRISMA_GENERATE . diff --git a/docker/build_server.sh b/docker/build_server.sh new file mode 100755 index 000000000..c5a3f26d8 --- /dev/null +++ b/docker/build_server.sh @@ -0,0 +1,12 @@ +#!/bin/bash + + +if [ "$RUN_PRISMA_GENERATE" = "true" ]; then + set -ex; \ + cargo prisma generate --schema ./core/prisma/schema.prisma +fi + +set -ex; \ + ./scripts/release/utils.sh -w; \ + ./scripts/release/utils.sh -p; \ + cargo build --package stump_server --bin stump_server --release \ No newline at end of file diff --git a/docs/components/DownloadLinks.tsx b/docs/components/DownloadLinks.tsx index 27ae6c762..4f8afd092 100644 --- a/docs/components/DownloadLinks.tsx +++ b/docs/components/DownloadLinks.tsx @@ -1,4 +1,11 @@ -import { SiAndroid, SiApple, SiDocker, SiLinux, SiWindows10 } from '@icons-pack/react-simple-icons' +import { + SiAndroid, + SiApple, + SiDocker, + SiIos, + SiLinux, + SiWindows10, +} from '@icons-pack/react-simple-icons' import clsx from 'clsx' import { motion } from 'framer-motion' import React from 'react' @@ -29,6 +36,7 @@ export default function DownloadLinks() { target="_blank" rel="noreferrer" > + {/* @ts-expect-error: Its fine */} @@ -39,20 +47,17 @@ export default function DownloadLinks() { const links = [ { - disabled: true, - href: '#', + href: 'https://github.com/stumpapp/stump/releases/latest', icon: SiLinux, title: 'Linux', }, { - disabled: true, - href: '#', + href: 'https://github.com/stumpapp/stump/releases/latest', icon: SiApple, title: 'macOS', }, { - disabled: true, - href: '#', + href: 'https://github.com/stumpapp/stump/releases/latest', icon: SiWindows10, title: 'Windows', }, @@ -67,4 +72,10 @@ const links = [ icon: SiAndroid, title: 'Android', }, + { + disabled: true, + href: '#', + icon: SiIos, + title: 'iOS', + }, ] diff --git a/docs/components/Footer.tsx b/docs/components/Footer.tsx index a0ea3f4b3..7c458b68e 100644 --- a/docs/components/Footer.tsx +++ b/docs/components/Footer.tsx @@ -70,6 +70,7 @@ export default function Footer() { className="text-gray-750 hover:text-gray-650 dark:text-gray-300 dark:hover:text-gray-100" > {item.name} + {/* @ts-expect-error: Its fine */}