mirror of
https://github.com/immich-app/immich.git
synced 2026-02-07 18:30:17 +03:00
Compare commits
18 Commits
feat/graph
...
feat/rotat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a4f48204d | ||
|
|
9cd0871178 | ||
|
|
dbbefde98d | ||
|
|
5407a28533 | ||
|
|
f5edc87e4d | ||
|
|
bf16b61d43 | ||
|
|
8c882b54cd | ||
|
|
2d7c333c8c | ||
|
|
7c821dd205 | ||
|
|
703361da1a | ||
|
|
fa5aeaf539 | ||
|
|
5f3a42a132 | ||
|
|
9d85272c2b | ||
|
|
d2575d8f00 | ||
|
|
f0a4c945bd | ||
|
|
a3766b879e | ||
|
|
1a190c33a0 | ||
|
|
17a63e37b2 |
4
.github/workflows/cli.yml
vendored
4
.github/workflows/cli.yml
vendored
@@ -56,10 +56,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
uses: docker/setup-qemu-action@v3.4.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@v3.9.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
|
||||
133
.github/workflows/docker.yml
vendored
133
.github/workflows/docker.yml
vendored
@@ -87,7 +87,6 @@ jobs:
|
||||
TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||
|
||||
|
||||
build_and_push_ml:
|
||||
name: Build and Push ML
|
||||
needs: pre-job
|
||||
@@ -122,10 +121,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
uses: docker/setup-qemu-action@v3.4.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@v3.9.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
# Only push to Docker Hub when making a release
|
||||
@@ -195,33 +194,38 @@ jobs:
|
||||
|
||||
build_and_push_server:
|
||||
name: Build and Push Server
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: pre-job
|
||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||
env:
|
||||
image: immich-server
|
||||
context: .
|
||||
file: server/Dockerfile
|
||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
||||
DOCKER_REPO: altran1502/immich-server
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platforms: linux/amd64,linux/arm64
|
||||
device: cpu
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
# Only push to Docker Hub when making a release
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: docker/login-action@v3
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -235,16 +239,81 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push image
|
||||
id: build
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
# Skip pushing when PR from a fork
|
||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
outputs: type=image,"name=${{ env.GHCR_REPO }},${{ env.DOCKER_REPO }}",push-by-digest=true,name-canonical=true,push=true
|
||||
build-args: |
|
||||
DEVICE=cpu
|
||||
BUILD_ID=${{ github.run_id }}
|
||||
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p ${{ runner.temp }}/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge_server:
|
||||
name: Merge & Push Server
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }}
|
||||
env:
|
||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
||||
DOCKER_REPO: altran1502/immich-server
|
||||
needs:
|
||||
- build_and_push_server
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Generate docker image tags
|
||||
id: metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
# Disable latest tag
|
||||
latest=false
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository_owner }}/${{env.image}}
|
||||
name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }}
|
||||
name=${{ env.GHCR_REPO }}
|
||||
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
# Tag with branch name
|
||||
type=ref,event=branch,suffix=${{ matrix.suffix }}
|
||||
@@ -254,38 +323,16 @@ jobs:
|
||||
type=ref,event=tag,suffix=${{ matrix.suffix }}
|
||||
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
|
||||
|
||||
- name: Determine build cache output
|
||||
id: cache-target
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
# Essentially just ignore the cache output (PR can't write to registry cache)
|
||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
platforms: ${{ matrix.platforms }}
|
||||
# Skip pushing when PR from a fork
|
||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}}
|
||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
build-args: |
|
||||
DEVICE=${{ matrix.device }}
|
||||
BUILD_ID=${{ github.run_id }}
|
||||
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *) \
|
||||
$(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *)
|
||||
|
||||
success-check-server:
|
||||
name: Docker Build & Push Server Success
|
||||
needs: [build_and_push_server, retag_server]
|
||||
needs: [merge_server, retag_server]
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
|
||||
17
.github/workflows/preview-comment.yaml
vendored
Normal file
17
.github/workflows/preview-comment.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Preview comment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
comment-status:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.label.name == 'preview' }}
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: mshick/add-pr-comment@v2
|
||||
with:
|
||||
message-id: "preview-status"
|
||||
message: "Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.cloud/"
|
||||
252
cli/package-lock.json
generated
252
cli/package-lock.json
generated
@@ -881,9 +881,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz",
|
||||
"integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==",
|
||||
"version": "9.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
|
||||
"integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1498,21 +1498,21 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
|
||||
"integrity": "sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==",
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz",
|
||||
"integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.20.0",
|
||||
"@typescript-eslint/type-utils": "8.20.0",
|
||||
"@typescript-eslint/utils": "8.20.0",
|
||||
"@typescript-eslint/visitor-keys": "8.20.0",
|
||||
"@typescript-eslint/scope-manager": "8.23.0",
|
||||
"@typescript-eslint/type-utils": "8.23.0",
|
||||
"@typescript-eslint/utils": "8.23.0",
|
||||
"@typescript-eslint/visitor-keys": "8.23.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.0.0"
|
||||
"ts-api-utils": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1528,16 +1528,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz",
|
||||
"integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==",
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz",
|
||||
"integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.20.0",
|
||||
"@typescript-eslint/types": "8.20.0",
|
||||
"@typescript-eslint/typescript-estree": "8.20.0",
|
||||
"@typescript-eslint/visitor-keys": "8.20.0",
|
||||
"@typescript-eslint/scope-manager": "8.23.0",
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/typescript-estree": "8.23.0",
|
||||
"@typescript-eslint/visitor-keys": "8.23.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1553,14 +1553,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz",
|
||||
"integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==",
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz",
|
||||
"integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.20.0",
|
||||
"@typescript-eslint/visitor-keys": "8.20.0"
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/visitor-keys": "8.23.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1571,16 +1571,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz",
|
||||
"integrity": "sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==",
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz",
|
||||
"integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.20.0",
|
||||
"@typescript-eslint/utils": "8.20.0",
|
||||
"@typescript-eslint/typescript-estree": "8.23.0",
|
||||
"@typescript-eslint/utils": "8.23.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.0.0"
|
||||
"ts-api-utils": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1595,9 +1595,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz",
|
||||
"integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==",
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz",
|
||||
"integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1609,20 +1609,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz",
|
||||
"integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==",
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz",
|
||||
"integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.20.0",
|
||||
"@typescript-eslint/visitor-keys": "8.20.0",
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/visitor-keys": "8.23.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^2.0.0"
|
||||
"ts-api-utils": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1636,16 +1636,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz",
|
||||
"integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==",
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz",
|
||||
"integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.20.0",
|
||||
"@typescript-eslint/types": "8.20.0",
|
||||
"@typescript-eslint/typescript-estree": "8.20.0"
|
||||
"@typescript-eslint/scope-manager": "8.23.0",
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"@typescript-eslint/typescript-estree": "8.23.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1660,13 +1660,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz",
|
||||
"integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==",
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz",
|
||||
"integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.20.0",
|
||||
"@typescript-eslint/types": "8.23.0",
|
||||
"eslint-visitor-keys": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1691,9 +1691,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.3.tgz",
|
||||
"integrity": "sha512-uVbJ/xhImdNtzPnLyxCZJMTeTIYdgcC2nWtBBBpR1H6z0w8m7D+9/zrDIx2nNxgMg9r+X8+RY2qVpUDeW2b3nw==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.5.tgz",
|
||||
"integrity": "sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1714,8 +1714,8 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "3.0.3",
|
||||
"vitest": "3.0.3"
|
||||
"@vitest/browser": "3.0.5",
|
||||
"vitest": "3.0.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
@@ -1724,14 +1724,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.3.tgz",
|
||||
"integrity": "sha512-SbRCHU4qr91xguu+dH3RUdI5dC86zm8aZWydbp961aIR7G8OYNN6ZiayFuf9WAngRbFOfdrLHCGgXTj3GtoMRQ==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz",
|
||||
"integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.0.3",
|
||||
"@vitest/utils": "3.0.3",
|
||||
"@vitest/spy": "3.0.5",
|
||||
"@vitest/utils": "3.0.5",
|
||||
"chai": "^5.1.2",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
@@ -1740,13 +1740,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.3.tgz",
|
||||
"integrity": "sha512-XT2XBc4AN9UdaxJAeIlcSZ0ILi/GzmG5G8XSly4gaiqIvPV3HMTSIDZWJVX6QRJ0PX1m+W8Cy0K9ByXNb/bPIA==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz",
|
||||
"integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.0.3",
|
||||
"@vitest/spy": "3.0.5",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.17"
|
||||
},
|
||||
@@ -1767,9 +1767,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.3.tgz",
|
||||
"integrity": "sha512-gCrM9F7STYdsDoNjGgYXKPq4SkSxwwIU5nkaQvdUxiQ0EcNlez+PdKOVIsUJvh9P9IeIFmjn4IIREWblOBpP2Q==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz",
|
||||
"integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1780,38 +1780,38 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.3.tgz",
|
||||
"integrity": "sha512-Rgi2kOAk5ZxWZlwPguRJFOBmWs6uvvyAAR9k3MvjRvYrG7xYvKChZcmnnpJCS98311CBDMqsW9MzzRFsj2gX3g==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz",
|
||||
"integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.0.3",
|
||||
"pathe": "^2.0.1"
|
||||
"@vitest/utils": "3.0.5",
|
||||
"pathe": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.3.tgz",
|
||||
"integrity": "sha512-kNRcHlI4txBGztuJfPEJ68VezlPAXLRT1u5UCx219TU3kOG2DplNxhWLwDf2h6emwmTPogzLnGVwP6epDaJN6Q==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz",
|
||||
"integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.0.3",
|
||||
"@vitest/pretty-format": "3.0.5",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.1"
|
||||
"pathe": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.3.tgz",
|
||||
"integrity": "sha512-7/dgux8ZBbF7lEIKNnEqQlyRaER9nkAL9eTmdKJkDO3hS8p59ATGwKOCUDHcBLKr7h/oi/6hP+7djQk8049T2A==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz",
|
||||
"integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1822,13 +1822,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.3.tgz",
|
||||
"integrity": "sha512-f+s8CvyzPtMFY1eZKkIHGhPsQgYo5qCm6O8KZoim9qm1/jT64qBgGpO5tHscNH6BzRHM+edLNOP+3vO8+8pE/A==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz",
|
||||
"integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.0.3",
|
||||
"@vitest/pretty-format": "3.0.5",
|
||||
"loupe": "^3.1.2",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
@@ -2334,9 +2334,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz",
|
||||
"integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==",
|
||||
"version": "9.19.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
|
||||
"integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2345,7 +2345,7 @@
|
||||
"@eslint/config-array": "^0.19.0",
|
||||
"@eslint/core": "^0.10.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "9.18.0",
|
||||
"@eslint/js": "9.19.0",
|
||||
"@eslint/plugin-kit": "^0.2.5",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
@@ -2407,9 +2407,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.2.tgz",
|
||||
"integrity": "sha512-1yI3/hf35wmlq66C8yOyrujQnel+v5l1Vop5Cl2I6ylyNTT1JbuUUnV3/41PzwTzcyDp/oF0jWE3HXvcH5AQOQ==",
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz",
|
||||
"integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2685,9 +2685,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
|
||||
"integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz",
|
||||
"integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
@@ -3180,9 +3180,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
|
||||
"integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
|
||||
"integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -3285,9 +3285,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mock-fs": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.4.1.tgz",
|
||||
"integrity": "sha512-sz/Q8K1gXXXHR+qr0GZg2ysxCRr323kuN10O7CtQjraJsFDJ4SJ+0I5MzALz7aRp9lHk8Cc/YdsT95h9Ka1aFw==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz",
|
||||
"integrity": "sha512-d/P1M/RacgM3dB0sJ8rjeRNXxtapkPCUnMGmIN0ixJ16F/E4GUZCvWcSGfWGz8eaXYvn1s9baUwNjI4LOPEjiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4166,9 +4166,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz",
|
||||
"integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4288,15 +4288,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.0.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz",
|
||||
"integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz",
|
||||
"integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.24.2",
|
||||
"postcss": "^8.4.49",
|
||||
"rollup": "^4.23.0"
|
||||
"postcss": "^8.5.1",
|
||||
"rollup": "^4.30.1"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -4360,16 +4360,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.3.tgz",
|
||||
"integrity": "sha512-0sQcwhwAEw/UJGojbhOrnq3HtiZ3tC7BzpAa0lx3QaTX0S3YX70iGcik25UBdB96pmdwjyY2uyKNYruxCDmiEg==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz",
|
||||
"integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
"debug": "^4.4.0",
|
||||
"es-module-lexer": "^1.6.0",
|
||||
"pathe": "^2.0.1",
|
||||
"pathe": "^2.0.2",
|
||||
"vite": "^5.0.0 || ^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4403,31 +4403,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.3.tgz",
|
||||
"integrity": "sha512-dWdwTFUW9rcnL0LyF2F+IfvNQWB0w9DERySCk8VMG75F8k25C7LsZoh6XfCjPvcR8Nb+Lqi9JKr6vnzH7HSrpQ==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz",
|
||||
"integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "3.0.3",
|
||||
"@vitest/mocker": "3.0.3",
|
||||
"@vitest/pretty-format": "^3.0.3",
|
||||
"@vitest/runner": "3.0.3",
|
||||
"@vitest/snapshot": "3.0.3",
|
||||
"@vitest/spy": "3.0.3",
|
||||
"@vitest/utils": "3.0.3",
|
||||
"@vitest/expect": "3.0.5",
|
||||
"@vitest/mocker": "3.0.5",
|
||||
"@vitest/pretty-format": "^3.0.5",
|
||||
"@vitest/runner": "3.0.5",
|
||||
"@vitest/snapshot": "3.0.5",
|
||||
"@vitest/spy": "3.0.5",
|
||||
"@vitest/utils": "3.0.5",
|
||||
"chai": "^5.1.2",
|
||||
"debug": "^4.4.0",
|
||||
"expect-type": "^1.1.0",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.1",
|
||||
"pathe": "^2.0.2",
|
||||
"std-env": "^3.8.0",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^0.3.2",
|
||||
"tinypool": "^1.0.2",
|
||||
"tinyrainbow": "^2.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vite-node": "3.0.3",
|
||||
"vite-node": "3.0.5",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4441,9 +4441,10 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@vitest/browser": "3.0.3",
|
||||
"@vitest/ui": "3.0.3",
|
||||
"@vitest/browser": "3.0.5",
|
||||
"@vitest/ui": "3.0.5",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
@@ -4451,6 +4452,9 @@
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/debug": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
|
||||
@@ -77,9 +77,7 @@ docker start immich_postgres # Start Postgres server
|
||||
sleep 10 # Wait for Postgres server to start up
|
||||
docker exec -it immich_postgres bash # Enter the Docker shell and run the following command
|
||||
# Check the database user if you deviated from the default. If your backup ends in `.gz`, replace `cat` with `gunzip`
|
||||
cat < "/dump.sql" \
|
||||
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
|
||||
| psql --dbname=postgres --username=<DB_USERNAME> # Restore Backup
|
||||
cat < "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname=postgres --username=<DB_USERNAME>
|
||||
exit # Exit the Docker shell
|
||||
docker compose up -d # Start remainder of Immich apps
|
||||
```
|
||||
|
||||
790
e2e/package-lock.json
generated
790
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -298,6 +298,7 @@ describe('/libraries', () => {
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
|
||||
|
||||
@@ -602,6 +602,8 @@
|
||||
"enable": "Enable",
|
||||
"enabled": "Enabled",
|
||||
"end_date": "End date",
|
||||
"rotate_left": "Rotate left",
|
||||
"rotate_right": "Rotate right",
|
||||
"error": "Error",
|
||||
"error_loading_image": "Error loading image",
|
||||
"error_title": "Error - Something went wrong",
|
||||
@@ -644,6 +646,7 @@
|
||||
"quota_higher_than_disk_size": "You set a quota higher than the disk size",
|
||||
"repair_unable_to_check_items": "Unable to check {count, select, one {item} other {items}}",
|
||||
"unable_to_add_album_users": "Unable to add users to album",
|
||||
"unable_to_rotate_image": "Unable to rotate image",
|
||||
"unable_to_add_assets_to_shared_link": "Unable to add assets to shared link",
|
||||
"unable_to_add_comment": "Unable to add comment",
|
||||
"unable_to_add_exclusion_pattern": "Unable to add exclusion pattern",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:adb581d8ed80edd03efd4dcad66db115b9ce8de8522b01720b9f3e6146f0884c AS builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:14b4620f59a90f163dfa6bd252b68743f9a41d494a9fde935f9d7669d98094bb AS builder-cpu
|
||||
|
||||
FROM builder-cpu AS builder-openvino
|
||||
|
||||
@@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:6ed5bff4d7d377e2a27d9285553b8c21cfccc4f00881de1b24c9bc8d90016e82 AS prod-cpu
|
||||
FROM python:3.11-slim-bookworm@sha256:42420f737ba91d509fc60d5ed65ed0492678a90c561e1fa08786ae8ba8b52eda AS prod-cpu
|
||||
|
||||
FROM prod-cpu AS prod-openvino
|
||||
|
||||
|
||||
314
machine-learning/poetry.lock
generated
314
machine-learning/poetry.lock
generated
@@ -758,23 +758,23 @@ test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.6"
|
||||
version = "0.115.8"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"},
|
||||
{file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"},
|
||||
{file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"},
|
||||
{file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
|
||||
starlette = ">=0.40.0,<0.42.0"
|
||||
starlette = ">=0.40.0,<0.46.0"
|
||||
typing-extensions = ">=4.8.0"
|
||||
|
||||
[package.extras]
|
||||
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"]
|
||||
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
@@ -1331,13 +1331,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "0.27.1"
|
||||
version = "0.28.1"
|
||||
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "huggingface_hub-0.27.1-py3-none-any.whl", hash = "sha256:1c5155ca7d60b60c2e2fc38cbb3ffb7f7c3adf48f824015b219af9061771daec"},
|
||||
{file = "huggingface_hub-0.27.1.tar.gz", hash = "sha256:c004463ca870283909d715d20f066ebd6968c2207dae9393fdffb3c1d4d8f98b"},
|
||||
{file = "huggingface_hub-0.28.1-py3-none-any.whl", hash = "sha256:aa6b9a3ffdae939b72c464dbb0d7f99f56e649b55c3d52406f49e0a5a620c0a7"},
|
||||
{file = "huggingface_hub-0.28.1.tar.gz", hash = "sha256:893471090c98e3b6efbdfdacafe4052b20b84d59866fb6f54c33d9af18c303ae"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1350,13 +1350,13 @@ tqdm = ">=4.42.1"
|
||||
typing-extensions = ">=3.7.4.3"
|
||||
|
||||
[package.extras]
|
||||
all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
|
||||
all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
|
||||
cli = ["InquirerPy (==0.3.4)"]
|
||||
dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.5.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
|
||||
dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "libcst (==1.4.0)", "mypy (==1.5.1)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
|
||||
fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"]
|
||||
hf-transfer = ["hf-transfer (>=0.1.4)"]
|
||||
inference = ["aiohttp"]
|
||||
quality = ["libcst (==1.4.0)", "mypy (==1.5.1)", "ruff (>=0.5.0)"]
|
||||
quality = ["libcst (==1.4.0)", "mypy (==1.5.1)", "ruff (>=0.9.0)"]
|
||||
tensorflow = ["graphviz", "pydot", "tensorflow"]
|
||||
tensorflow-testing = ["keras (<3.0)", "tensorflow"]
|
||||
testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio (>=4.0.0)", "jedi", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"]
|
||||
@@ -1625,13 +1625,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "locust"
|
||||
version = "2.32.6"
|
||||
version = "2.32.9"
|
||||
description = "Developer-friendly load testing framework"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "locust-2.32.6-py3-none-any.whl", hash = "sha256:d5c0e4f73134415d250087034431cf3ea42ca695d3dee7f10812287cacb6c4ef"},
|
||||
{file = "locust-2.32.6.tar.gz", hash = "sha256:6600cc308398e724764aacc56ccddf6cfcd0127c4c92dedd5c4979dd37ef5b15"},
|
||||
{file = "locust-2.32.9-py3-none-any.whl", hash = "sha256:d9447c26d2bbaec5a0ace7cadefa1a31820ed392234257b309965a43d5e8d26f"},
|
||||
{file = "locust-2.32.9.tar.gz", hash = "sha256:4c297afa5cdc3de15dfa79279576e5f33c1d69dd70006b51d079dcbd212201cc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1649,8 +1649,8 @@ psutil = ">=5.9.1"
|
||||
pywin32 = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
pyzmq = ">=25.0.0"
|
||||
requests = [
|
||||
{version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""},
|
||||
{version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""},
|
||||
{version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""},
|
||||
]
|
||||
setuptools = ">=70.0.0"
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
@@ -1893,49 +1893,43 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.14.1"
|
||||
version = "1.15.0"
|
||||
description = "Optional static typing for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"},
|
||||
{file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"},
|
||||
{file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"},
|
||||
{file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"},
|
||||
{file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"},
|
||||
{file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"},
|
||||
{file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"},
|
||||
{file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"},
|
||||
{file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"},
|
||||
{file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"},
|
||||
{file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"},
|
||||
{file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"},
|
||||
{file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"},
|
||||
{file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"},
|
||||
{file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"},
|
||||
{file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"},
|
||||
{file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"},
|
||||
{file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"},
|
||||
{file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"},
|
||||
{file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"},
|
||||
{file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"},
|
||||
{file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"},
|
||||
{file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"},
|
||||
{file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"},
|
||||
{file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"},
|
||||
{file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"},
|
||||
{file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"},
|
||||
{file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"},
|
||||
{file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"},
|
||||
{file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"},
|
||||
{file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"},
|
||||
{file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"},
|
||||
{file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"},
|
||||
{file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"},
|
||||
{file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2181,94 +2175,98 @@ files = [
|
||||
|
||||
[package.dependencies]
|
||||
numpy = [
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||
{version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
|
||||
{version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""},
|
||||
{version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""},
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.10.14"
|
||||
version = "3.10.15"
|
||||
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "orjson-3.10.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:849ea7845a55f09965826e816cdc7689d6cf74fe9223d79d758c714af955bcb6"},
|
||||
{file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5947b139dfa33f72eecc63f17e45230a97e741942955a6c9e650069305eb73d"},
|
||||
{file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cde6d76910d3179dae70f164466692f4ea36da124d6fb1a61399ca589e81d69a"},
|
||||
{file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6dfbaeb7afa77ca608a50e2770a0461177b63a99520d4928e27591b142c74b1"},
|
||||
{file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa45e489ef80f28ff0e5ba0a72812b8cfc7c1ef8b46a694723807d1b07c89ebb"},
|
||||
{file = "orjson-3.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5007abfdbb1d866e2aa8990bd1c465f0f6da71d19e695fc278282be12cffa5"},
|
||||
{file = "orjson-3.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1b49e2af011c84c3f2d541bb5cd1e3c7c2df672223e7e3ea608f09cf295e5f8a"},
|
||||
{file = "orjson-3.10.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:164ac155109226b3a2606ee6dda899ccfbe6e7e18b5bdc3fbc00f79cc074157d"},
|
||||
{file = "orjson-3.10.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6b1225024cf0ef5d15934b5ffe9baf860fe8bc68a796513f5ea4f5056de30bca"},
|
||||
{file = "orjson-3.10.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d6546e8073dc382e60fcae4a001a5a1bc46da5eab4a4878acc2d12072d6166d5"},
|
||||
{file = "orjson-3.10.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f1d2942605c894162252d6259b0121bf1cb493071a1ea8cb35d79cb3e6ac5bc"},
|
||||
{file = "orjson-3.10.14-cp310-cp310-win32.whl", hash = "sha256:397083806abd51cf2b3bbbf6c347575374d160331a2d33c5823e22249ad3118b"},
|
||||
{file = "orjson-3.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:fa18f949d3183a8d468367056be989666ac2bef3a72eece0bade9cdb733b3c28"},
|
||||
{file = "orjson-3.10.14-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f506fd666dd1ecd15a832bebc66c4df45c1902fd47526292836c339f7ba665a9"},
|
||||
{file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efe5fd254cfb0eeee13b8ef7ecb20f5d5a56ddda8a587f3852ab2cedfefdb5f6"},
|
||||
{file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ddc8c866d7467f5ee2991397d2ea94bcf60d0048bdd8ca555740b56f9042725"},
|
||||
{file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af8e42ae4363773658b8d578d56dedffb4f05ceeb4d1d4dd3fb504950b45526"},
|
||||
{file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84dd83110503bc10e94322bf3ffab8bc49150176b49b4984dc1cce4c0a993bf9"},
|
||||
{file = "orjson-3.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36f5bfc0399cd4811bf10ec7a759c7ab0cd18080956af8ee138097d5b5296a95"},
|
||||
{file = "orjson-3.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868943660fb2a1e6b6b965b74430c16a79320b665b28dd4511d15ad5038d37d5"},
|
||||
{file = "orjson-3.10.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33449c67195969b1a677533dee9d76e006001213a24501333624623e13c7cc8e"},
|
||||
{file = "orjson-3.10.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e4c9f60f9fb0b5be66e416dcd8c9d94c3eabff3801d875bdb1f8ffc12cf86905"},
|
||||
{file = "orjson-3.10.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0de4d6315cfdbd9ec803b945c23b3a68207fd47cbe43626036d97e8e9561a436"},
|
||||
{file = "orjson-3.10.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83adda3db595cb1a7e2237029b3249c85afbe5c747d26b41b802e7482cb3933e"},
|
||||
{file = "orjson-3.10.14-cp311-cp311-win32.whl", hash = "sha256:998019ef74a4997a9d741b1473533cdb8faa31373afc9849b35129b4b8ec048d"},
|
||||
{file = "orjson-3.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:9d034abdd36f0f0f2240f91492684e5043d46f290525d1117712d5b8137784eb"},
|
||||
{file = "orjson-3.10.14-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2ad4b7e367efba6dc3f119c9a0fcd41908b7ec0399a696f3cdea7ec477441b09"},
|
||||
{file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f496286fc85e93ce0f71cc84fc1c42de2decf1bf494094e188e27a53694777a7"},
|
||||
{file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7f189bbfcded40e41a6969c1068ba305850ba016665be71a217918931416fbf"},
|
||||
{file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cc8204f0b75606869c707da331058ddf085de29558b516fc43c73ee5ee2aadb"},
|
||||
{file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deaa2899dff7f03ab667e2ec25842d233e2a6a9e333efa484dfe666403f3501c"},
|
||||
{file = "orjson-3.10.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1c3ea52642c9714dc6e56de8a451a066f6d2707d273e07fe8a9cc1ba073813d"},
|
||||
{file = "orjson-3.10.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d3f9ed72e7458ded9a1fb1b4d4ed4c4fdbaf82030ce3f9274b4dc1bff7ace2b"},
|
||||
{file = "orjson-3.10.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:07520685d408a2aba514c17ccc16199ff2934f9f9e28501e676c557f454a37fe"},
|
||||
{file = "orjson-3.10.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:76344269b550ea01488d19a2a369ab572c1ac4449a72e9f6ac0d70eb1cbfb953"},
|
||||
{file = "orjson-3.10.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e2979d0f2959990620f7e62da6cd954e4620ee815539bc57a8ae46e2dacf90e3"},
|
||||
{file = "orjson-3.10.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03f61ca3674555adcb1aa717b9fc87ae936aa7a63f6aba90a474a88701278780"},
|
||||
{file = "orjson-3.10.14-cp312-cp312-win32.whl", hash = "sha256:d5075c54edf1d6ad81d4c6523ce54a748ba1208b542e54b97d8a882ecd810fd1"},
|
||||
{file = "orjson-3.10.14-cp312-cp312-win_amd64.whl", hash = "sha256:175cafd322e458603e8ce73510a068d16b6e6f389c13f69bf16de0e843d7d406"},
|
||||
{file = "orjson-3.10.14-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:0905ca08a10f7e0e0c97d11359609300eb1437490a7f32bbaa349de757e2e0c7"},
|
||||
{file = "orjson-3.10.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92d13292249f9f2a3e418cbc307a9fbbef043c65f4bd8ba1eb620bc2aaba3d15"},
|
||||
{file = "orjson-3.10.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90937664e776ad316d64251e2fa2ad69265e4443067668e4727074fe39676414"},
|
||||
{file = "orjson-3.10.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9ed3d26c4cb4f6babaf791aa46a029265850e80ec2a566581f5c2ee1a14df4f1"},
|
||||
{file = "orjson-3.10.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:56ee546c2bbe9599aba78169f99d1dc33301853e897dbaf642d654248280dc6e"},
|
||||
{file = "orjson-3.10.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:901e826cb2f1bdc1fcef3ef59adf0c451e8f7c0b5deb26c1a933fb66fb505eae"},
|
||||
{file = "orjson-3.10.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26336c0d4b2d44636e1e1e6ed1002f03c6aae4a8a9329561c8883f135e9ff010"},
|
||||
{file = "orjson-3.10.14-cp313-cp313-win32.whl", hash = "sha256:e2bc525e335a8545c4e48f84dd0328bc46158c9aaeb8a1c2276546e94540ea3d"},
|
||||
{file = "orjson-3.10.14-cp313-cp313-win_amd64.whl", hash = "sha256:eca04dfd792cedad53dc9a917da1a522486255360cb4e77619343a20d9f35364"},
|
||||
{file = "orjson-3.10.14-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a0fba3b8a587a54c18585f077dcab6dd251c170d85cfa4d063d5746cd595a0f"},
|
||||
{file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:175abf3d20e737fec47261d278f95031736a49d7832a09ab684026528c4d96db"},
|
||||
{file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29ca1a93e035d570e8b791b6c0feddd403c6a5388bfe870bf2aa6bba1b9d9b8e"},
|
||||
{file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f77202c80e8ab5a1d1e9faf642343bee5aaf332061e1ada4e9147dbd9eb00c46"},
|
||||
{file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e2ec73b7099b6a29b40a62e08a23b936423bd35529f8f55c42e27acccde7954"},
|
||||
{file = "orjson-3.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2d1679df9f9cd9504f8dff24555c1eaabba8aad7f5914f28dab99e3c2552c9d"},
|
||||
{file = "orjson-3.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:691ab9a13834310a263664313e4f747ceb93662d14a8bdf20eb97d27ed488f16"},
|
||||
{file = "orjson-3.10.14-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b11ed82054fce82fb74cea33247d825d05ad6a4015ecfc02af5fbce442fbf361"},
|
||||
{file = "orjson-3.10.14-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:e70a1d62b8288677d48f3bea66c21586a5f999c64ecd3878edb7393e8d1b548d"},
|
||||
{file = "orjson-3.10.14-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:16642f10c1ca5611251bd835de9914a4b03095e28a34c8ba6a5500b5074338bd"},
|
||||
{file = "orjson-3.10.14-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3871bad546aa66c155e3f36f99c459780c2a392d502a64e23fb96d9abf338511"},
|
||||
{file = "orjson-3.10.14-cp38-cp38-win32.whl", hash = "sha256:0293a88815e9bb5c90af4045f81ed364d982f955d12052d989d844d6c4e50945"},
|
||||
{file = "orjson-3.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:6169d3868b190d6b21adc8e61f64e3db30f50559dfbdef34a1cd6c738d409dfc"},
|
||||
{file = "orjson-3.10.14-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:06d4ec218b1ec1467d8d64da4e123b4794c781b536203c309ca0f52819a16c03"},
|
||||
{file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962c2ec0dcaf22b76dee9831fdf0c4a33d4bf9a257a2bc5d4adc00d5c8ad9034"},
|
||||
{file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:21d3be4132f71ef1360385770474f29ea1538a242eef72ac4934fe142800e37f"},
|
||||
{file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c28ed60597c149a9e3f5ad6dd9cebaee6fb2f0e3f2d159a4a2b9b862d4748860"},
|
||||
{file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e947f70167fe18469f2023644e91ab3d24f9aed69a5e1c78e2c81b9cea553fb"},
|
||||
{file = "orjson-3.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64410696c97a35af2432dea7bdc4ce32416458159430ef1b4beb79fd30093ad6"},
|
||||
{file = "orjson-3.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8050a5d81c022561ee29cd2739de5b4445f3c72f39423fde80a63299c1892c52"},
|
||||
{file = "orjson-3.10.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b49a28e30d3eca86db3fe6f9b7f4152fcacbb4a467953cd1b42b94b479b77956"},
|
||||
{file = "orjson-3.10.14-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ca041ad20291a65d853a9523744eebc3f5a4b2f7634e99f8fe88320695ddf766"},
|
||||
{file = "orjson-3.10.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d313a2998b74bb26e9e371851a173a9b9474764916f1fc7971095699b3c6e964"},
|
||||
{file = "orjson-3.10.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7796692136a67b3e301ef9052bde6fe8e7bd5200da766811a3a608ffa62aaff0"},
|
||||
{file = "orjson-3.10.14-cp39-cp39-win32.whl", hash = "sha256:eee4bc767f348fba485ed9dc576ca58b0a9eac237f0e160f7a59bce628ed06b3"},
|
||||
{file = "orjson-3.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:96a1c0ee30fb113b3ae3c748fd75ca74a157ff4c58476c47db4d61518962a011"},
|
||||
{file = "orjson-3.10.14.tar.gz", hash = "sha256:cf31f6f071a6b8e7aa1ead1fa27b935b48d00fbfa6a28ce856cfff2d5dd68eed"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab"},
|
||||
{file = "orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e"},
|
||||
{file = "orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665"},
|
||||
{file = "orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890"},
|
||||
{file = "orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e8afd6200e12771467a1a44e5ad780614b86abb4b11862ec54861a82d677746"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9a18c500f19273e9e104cca8c1f0b40a6470bcccfc33afcc088045d0bf5ea6"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb00b7bfbdf5d34a13180e4805d76b4567025da19a197645ca746fc2fb536586"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33aedc3d903378e257047fee506f11e0833146ca3e57a1a1fb0ddb789876c1e1"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0099ae6aed5eb1fc84c9eb72b95505a3df4267e6962eb93cdd5af03be71c98"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c864a80a2d467d7786274fce0e4f93ef2a7ca4ff31f7fc5634225aaa4e9e98c"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c25774c9e88a3e0013d7d1a6c8056926b607a61edd423b50eb5c88fd7f2823ae"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e78c211d0074e783d824ce7bb85bf459f93a233eb67a5b5003498232ddfb0e8a"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:43e17289ffdbbac8f39243916c893d2ae41a2ea1a9cbb060a56a4d75286351ae"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:781d54657063f361e89714293c095f506c533582ee40a426cb6489c48a637b81"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6875210307d36c94873f553786a808af2788e362bd0cf4c8e66d976791e7b528"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-win32.whl", hash = "sha256:305b38b2b8f8083cc3d618927d7f424349afce5975b316d33075ef0f73576b60"},
|
||||
{file = "orjson-3.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:5dd9ef1639878cc3efffed349543cbf9372bdbd79f478615a1c633fe4e4180d1"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ffe19f3e8d68111e8644d4f4e267a069ca427926855582ff01fc012496d19969"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d433bf32a363823863a96561a555227c18a522a8217a6f9400f00ddc70139ae2"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da03392674f59a95d03fa5fb9fe3a160b0511ad84b7a3914699ea5a1b3a38da2"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a63bb41559b05360ded9132032239e47983a39b151af1201f07ec9370715c82"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3766ac4702f8f795ff3fa067968e806b4344af257011858cc3d6d8721588b53f"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1c73dcc8fadbd7c55802d9aa093b36878d34a3b3222c41052ce6b0fc65f8e8"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b299383825eafe642cbab34be762ccff9fd3408d72726a6b2a4506d410a71ab3"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:abc7abecdbf67a173ef1316036ebbf54ce400ef2300b4e26a7b843bd446c2480"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3614ea508d522a621384c1d6639016a5a2e4f027f3e4a1c93a51867615d28829"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:295c70f9dc154307777ba30fe29ff15c1bcc9dfc5c48632f37d20a607e9ba85a"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:63309e3ff924c62404923c80b9e2048c1f74ba4b615e7584584389ada50ed428"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-win32.whl", hash = "sha256:a2f708c62d026fb5340788ba94a55c23df4e1869fec74be455e0b2f5363b8507"},
|
||||
{file = "orjson-3.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:efcf6c735c3d22ef60c4aa27a5238f1a477df85e9b15f2142f9d669beb2d13fd"},
|
||||
{file = "orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2498,13 +2496,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.5"
|
||||
version = "2.10.6"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53"},
|
||||
{file = "pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff"},
|
||||
{file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"},
|
||||
{file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2712,13 +2710,13 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "0.25.2"
|
||||
version = "0.25.3"
|
||||
description = "Pytest support for asyncio"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"},
|
||||
{file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"},
|
||||
{file = "pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"},
|
||||
{file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3049,29 +3047,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.9.2"
|
||||
version = "0.9.6"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347"},
|
||||
{file = "ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00"},
|
||||
{file = "ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a"},
|
||||
{file = "ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145"},
|
||||
{file = "ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5"},
|
||||
{file = "ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6"},
|
||||
{file = "ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0"},
|
||||
{file = "ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba"},
|
||||
{file = "ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504"},
|
||||
{file = "ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83"},
|
||||
{file = "ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc"},
|
||||
{file = "ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b"},
|
||||
{file = "ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e"},
|
||||
{file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666"},
|
||||
{file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5"},
|
||||
{file = "ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5"},
|
||||
{file = "ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217"},
|
||||
{file = "ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6"},
|
||||
{file = "ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897"},
|
||||
{file = "ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08"},
|
||||
{file = "ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656"},
|
||||
{file = "ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d"},
|
||||
{file = "ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa"},
|
||||
{file = "ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a"},
|
||||
{file = "ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -541,7 +541,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -685,7 +685,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -715,7 +715,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -748,7 +748,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -791,7 +791,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -831,7 +831,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
CURRENT_PROJECT_VERSION = 194;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.126.0</string>
|
||||
<string>1.126.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -93,7 +93,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>193</string>
|
||||
<string>194</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -59,9 +59,10 @@ class MapBottomSheet extends HookConsumerWidget {
|
||||
child: DraggableScrollableSheet(
|
||||
controller: sheetController,
|
||||
minChildSize: sheetMinExtent,
|
||||
maxChildSize: 0.5,
|
||||
maxChildSize: 0.8,
|
||||
initialChildSize: sheetMinExtent,
|
||||
snap: true,
|
||||
snapSizes: [sheetMinExtent, 0.5, 0.8],
|
||||
shouldCloseOnMinExtent: false,
|
||||
builder: (ctx, scrollController) => MapAssetGrid(
|
||||
controller: scrollController,
|
||||
@@ -78,18 +79,23 @@ class MapBottomSheet extends HookConsumerWidget {
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: bottomSheetOffset,
|
||||
builder: (ctx, value, child) => Positioned(
|
||||
right: 0,
|
||||
bottom: context.height * (value + 0.02),
|
||||
child: child!,
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: onZoomToLocation,
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
child: const Icon(Icons.my_location),
|
||||
),
|
||||
builder: (context, value, child) {
|
||||
return Positioned(
|
||||
right: 0,
|
||||
bottom: context.height * (value + 0.02),
|
||||
child: AnimatedOpacity(
|
||||
opacity: value < 0.8 ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: ElevatedButton(
|
||||
onPressed: onZoomToLocation,
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
child: const Icon(Icons.my_location),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
105
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
105
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
@@ -20,6 +20,7 @@ class AssetBulkUpdateDto {
|
||||
this.isFavorite,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.orientation,
|
||||
this.rating,
|
||||
});
|
||||
|
||||
@@ -67,6 +68,8 @@ class AssetBulkUpdateDto {
|
||||
///
|
||||
num? longitude;
|
||||
|
||||
AssetBulkUpdateDtoOrientationEnum? orientation;
|
||||
|
||||
/// Minimum value: -1
|
||||
/// Maximum value: 5
|
||||
///
|
||||
@@ -86,6 +89,7 @@ class AssetBulkUpdateDto {
|
||||
other.isFavorite == isFavorite &&
|
||||
other.latitude == latitude &&
|
||||
other.longitude == longitude &&
|
||||
other.orientation == orientation &&
|
||||
other.rating == rating;
|
||||
|
||||
@override
|
||||
@@ -98,10 +102,11 @@ class AssetBulkUpdateDto {
|
||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||
(latitude == null ? 0 : latitude!.hashCode) +
|
||||
(longitude == null ? 0 : longitude!.hashCode) +
|
||||
(orientation == null ? 0 : orientation!.hashCode) +
|
||||
(rating == null ? 0 : rating!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating]';
|
||||
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, orientation=$orientation, rating=$rating]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -136,6 +141,11 @@ class AssetBulkUpdateDto {
|
||||
} else {
|
||||
// json[r'longitude'] = null;
|
||||
}
|
||||
if (this.orientation != null) {
|
||||
json[r'orientation'] = this.orientation;
|
||||
} else {
|
||||
// json[r'orientation'] = null;
|
||||
}
|
||||
if (this.rating != null) {
|
||||
json[r'rating'] = this.rating;
|
||||
} else {
|
||||
@@ -162,6 +172,7 @@ class AssetBulkUpdateDto {
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
orientation: AssetBulkUpdateDtoOrientationEnum.fromJson(json[r'orientation']),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
);
|
||||
}
|
||||
@@ -214,3 +225,95 @@ class AssetBulkUpdateDto {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
class AssetBulkUpdateDtoOrientationEnum {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const AssetBulkUpdateDtoOrientationEnum._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final int value;
|
||||
|
||||
@override
|
||||
String toString() => value.toString();
|
||||
|
||||
int toJson() => value;
|
||||
|
||||
static const number1 = AssetBulkUpdateDtoOrientationEnum._(1);
|
||||
static const number2 = AssetBulkUpdateDtoOrientationEnum._(2);
|
||||
static const number3 = AssetBulkUpdateDtoOrientationEnum._(3);
|
||||
static const number4 = AssetBulkUpdateDtoOrientationEnum._(4);
|
||||
static const number5 = AssetBulkUpdateDtoOrientationEnum._(5);
|
||||
static const number6 = AssetBulkUpdateDtoOrientationEnum._(6);
|
||||
static const number7 = AssetBulkUpdateDtoOrientationEnum._(7);
|
||||
static const number8 = AssetBulkUpdateDtoOrientationEnum._(8);
|
||||
|
||||
/// List of all possible values in this [enum][AssetBulkUpdateDtoOrientationEnum].
|
||||
static const values = <AssetBulkUpdateDtoOrientationEnum>[
|
||||
number1,
|
||||
number2,
|
||||
number3,
|
||||
number4,
|
||||
number5,
|
||||
number6,
|
||||
number7,
|
||||
number8,
|
||||
];
|
||||
|
||||
static AssetBulkUpdateDtoOrientationEnum? fromJson(dynamic value) => AssetBulkUpdateDtoOrientationEnumTypeTransformer().decode(value);
|
||||
|
||||
static List<AssetBulkUpdateDtoOrientationEnum> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetBulkUpdateDtoOrientationEnum>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetBulkUpdateDtoOrientationEnum.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [AssetBulkUpdateDtoOrientationEnum] to int,
|
||||
/// and [decode] dynamic data back to [AssetBulkUpdateDtoOrientationEnum].
|
||||
class AssetBulkUpdateDtoOrientationEnumTypeTransformer {
|
||||
factory AssetBulkUpdateDtoOrientationEnumTypeTransformer() => _instance ??= const AssetBulkUpdateDtoOrientationEnumTypeTransformer._();
|
||||
|
||||
const AssetBulkUpdateDtoOrientationEnumTypeTransformer._();
|
||||
|
||||
int encode(AssetBulkUpdateDtoOrientationEnum data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a AssetBulkUpdateDtoOrientationEnum.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
AssetBulkUpdateDtoOrientationEnum? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case 1: return AssetBulkUpdateDtoOrientationEnum.number1;
|
||||
case 2: return AssetBulkUpdateDtoOrientationEnum.number2;
|
||||
case 3: return AssetBulkUpdateDtoOrientationEnum.number3;
|
||||
case 4: return AssetBulkUpdateDtoOrientationEnum.number4;
|
||||
case 5: return AssetBulkUpdateDtoOrientationEnum.number5;
|
||||
case 6: return AssetBulkUpdateDtoOrientationEnum.number6;
|
||||
case 7: return AssetBulkUpdateDtoOrientationEnum.number7;
|
||||
case 8: return AssetBulkUpdateDtoOrientationEnum.number8;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [AssetBulkUpdateDtoOrientationEnumTypeTransformer] instance.
|
||||
static AssetBulkUpdateDtoOrientationEnumTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
105
mobile/openapi/lib/model/update_asset_dto.dart
generated
105
mobile/openapi/lib/model/update_asset_dto.dart
generated
@@ -20,6 +20,7 @@ class UpdateAssetDto {
|
||||
this.latitude,
|
||||
this.livePhotoVideoId,
|
||||
this.longitude,
|
||||
this.orientation,
|
||||
this.rating,
|
||||
});
|
||||
|
||||
@@ -73,6 +74,8 @@ class UpdateAssetDto {
|
||||
///
|
||||
num? longitude;
|
||||
|
||||
UpdateAssetDtoOrientationEnum? orientation;
|
||||
|
||||
/// Minimum value: -1
|
||||
/// Maximum value: 5
|
||||
///
|
||||
@@ -92,6 +95,7 @@ class UpdateAssetDto {
|
||||
other.latitude == latitude &&
|
||||
other.livePhotoVideoId == livePhotoVideoId &&
|
||||
other.longitude == longitude &&
|
||||
other.orientation == orientation &&
|
||||
other.rating == rating;
|
||||
|
||||
@override
|
||||
@@ -104,10 +108,11 @@ class UpdateAssetDto {
|
||||
(latitude == null ? 0 : latitude!.hashCode) +
|
||||
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
||||
(longitude == null ? 0 : longitude!.hashCode) +
|
||||
(orientation == null ? 0 : orientation!.hashCode) +
|
||||
(rating == null ? 0 : rating!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, longitude=$longitude, rating=$rating]';
|
||||
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, longitude=$longitude, orientation=$orientation, rating=$rating]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -146,6 +151,11 @@ class UpdateAssetDto {
|
||||
} else {
|
||||
// json[r'longitude'] = null;
|
||||
}
|
||||
if (this.orientation != null) {
|
||||
json[r'orientation'] = this.orientation;
|
||||
} else {
|
||||
// json[r'orientation'] = null;
|
||||
}
|
||||
if (this.rating != null) {
|
||||
json[r'rating'] = this.rating;
|
||||
} else {
|
||||
@@ -170,6 +180,7 @@ class UpdateAssetDto {
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
orientation: UpdateAssetDtoOrientationEnum.fromJson(json[r'orientation']),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
);
|
||||
}
|
||||
@@ -221,3 +232,95 @@ class UpdateAssetDto {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
class UpdateAssetDtoOrientationEnum {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const UpdateAssetDtoOrientationEnum._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final int value;
|
||||
|
||||
@override
|
||||
String toString() => value.toString();
|
||||
|
||||
int toJson() => value;
|
||||
|
||||
static const number1 = UpdateAssetDtoOrientationEnum._(1);
|
||||
static const number2 = UpdateAssetDtoOrientationEnum._(2);
|
||||
static const number3 = UpdateAssetDtoOrientationEnum._(3);
|
||||
static const number4 = UpdateAssetDtoOrientationEnum._(4);
|
||||
static const number5 = UpdateAssetDtoOrientationEnum._(5);
|
||||
static const number6 = UpdateAssetDtoOrientationEnum._(6);
|
||||
static const number7 = UpdateAssetDtoOrientationEnum._(7);
|
||||
static const number8 = UpdateAssetDtoOrientationEnum._(8);
|
||||
|
||||
/// List of all possible values in this [enum][UpdateAssetDtoOrientationEnum].
|
||||
static const values = <UpdateAssetDtoOrientationEnum>[
|
||||
number1,
|
||||
number2,
|
||||
number3,
|
||||
number4,
|
||||
number5,
|
||||
number6,
|
||||
number7,
|
||||
number8,
|
||||
];
|
||||
|
||||
static UpdateAssetDtoOrientationEnum? fromJson(dynamic value) => UpdateAssetDtoOrientationEnumTypeTransformer().decode(value);
|
||||
|
||||
static List<UpdateAssetDtoOrientationEnum> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UpdateAssetDtoOrientationEnum>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UpdateAssetDtoOrientationEnum.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [UpdateAssetDtoOrientationEnum] to int,
|
||||
/// and [decode] dynamic data back to [UpdateAssetDtoOrientationEnum].
|
||||
class UpdateAssetDtoOrientationEnumTypeTransformer {
|
||||
factory UpdateAssetDtoOrientationEnumTypeTransformer() => _instance ??= const UpdateAssetDtoOrientationEnumTypeTransformer._();
|
||||
|
||||
const UpdateAssetDtoOrientationEnumTypeTransformer._();
|
||||
|
||||
int encode(UpdateAssetDtoOrientationEnum data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a UpdateAssetDtoOrientationEnum.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
UpdateAssetDtoOrientationEnum? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case 1: return UpdateAssetDtoOrientationEnum.number1;
|
||||
case 2: return UpdateAssetDtoOrientationEnum.number2;
|
||||
case 3: return UpdateAssetDtoOrientationEnum.number3;
|
||||
case 4: return UpdateAssetDtoOrientationEnum.number4;
|
||||
case 5: return UpdateAssetDtoOrientationEnum.number5;
|
||||
case 6: return UpdateAssetDtoOrientationEnum.number6;
|
||||
case 7: return UpdateAssetDtoOrientationEnum.number7;
|
||||
case 8: return UpdateAssetDtoOrientationEnum.number8;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [UpdateAssetDtoOrientationEnumTypeTransformer] instance.
|
||||
static UpdateAssetDtoOrientationEnumTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7963,6 +7963,21 @@
|
||||
"longitude": {
|
||||
"type": "number"
|
||||
},
|
||||
"orientation": {
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8
|
||||
],
|
||||
"maximum": 8,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
@@ -12880,6 +12895,21 @@
|
||||
"longitude": {
|
||||
"type": "number"
|
||||
},
|
||||
"orientation": {
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8
|
||||
],
|
||||
"maximum": 8,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
|
||||
@@ -391,6 +391,7 @@ export type AssetBulkUpdateDto = {
|
||||
isFavorite?: boolean;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
orientation?: Orientation;
|
||||
rating?: number;
|
||||
};
|
||||
export type AssetBulkUploadCheckItem = {
|
||||
@@ -439,6 +440,7 @@ export type UpdateAssetDto = {
|
||||
latitude?: number;
|
||||
livePhotoVideoId?: string | null;
|
||||
longitude?: number;
|
||||
orientation?: Orientation;
|
||||
rating?: number;
|
||||
};
|
||||
export type AssetMediaReplaceDto = {
|
||||
@@ -3481,6 +3483,16 @@ export enum AssetMediaStatus {
|
||||
Replaced = "replaced",
|
||||
Duplicate = "duplicate"
|
||||
}
|
||||
export enum Orientation {
|
||||
$1 = 1,
|
||||
$2 = 2,
|
||||
$3 = 3,
|
||||
$4 = 4,
|
||||
$5 = 5,
|
||||
$6 = 6,
|
||||
$7 = 7,
|
||||
$8 = 8
|
||||
}
|
||||
export enum Action {
|
||||
Accept = "accept",
|
||||
Reject = "reject"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:20250204@sha256:8b203f19f4d5cf4619b60ee5f50d6d4b5ea3745747f5e5170d1b7404ebeb0792 AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:20250211@sha256:6ae577a6518e1ccca973db16955f4d79b01cac3ae122759ccb1c17bf6c330ba9 AS dev
|
||||
|
||||
RUN apt-get install --no-install-recommends -yqq tini
|
||||
WORKDIR /usr/src/app
|
||||
@@ -42,7 +42,7 @@ RUN npm run build
|
||||
|
||||
|
||||
# prod build
|
||||
FROM ghcr.io/immich-app/base-server-prod:20250204@sha256:2af3da713d5ab3ccca23b216b747557ea6016117e72deac101e35069ccaf9b5e
|
||||
FROM ghcr.io/immich-app/base-server-prod:20250211@sha256:879cfe3d2afd4b7bdb211f694f99b6c0679a8bbd3e96964ad6c878f8471d63ea
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
1554
server/package-lock.json
generated
1554
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,20 +35,17 @@
|
||||
"email:dev": "email dev -p 3050 --dir src/emails"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/server": "^4.11.3",
|
||||
"@nestjs/apollo": "^13.0.2",
|
||||
"@nestjs/bullmq": "^11.0.1",
|
||||
"@nestjs/common": "^11.0.4",
|
||||
"@nestjs/core": "^11.0.4",
|
||||
"@nestjs/event-emitter": "^3.0.0",
|
||||
"@nestjs/graphql": "^13.0.2",
|
||||
"@nestjs/platform-express": "^11.0.4",
|
||||
"@nestjs/platform-socket.io": "^11.0.4",
|
||||
"@nestjs/schedule": "^5.0.0",
|
||||
"@nestjs/swagger": "^11.0.2",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@nestjs/websockets": "^11.0.4",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.55.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.56.0",
|
||||
"@opentelemetry/context-async-hooks": "^1.24.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.57.0",
|
||||
"@opentelemetry/sdk-node": "^0.57.0",
|
||||
@@ -66,7 +63,6 @@
|
||||
"fast-glob": "^3.3.2",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"geo-tz": "^8.0.0",
|
||||
"graphql": "^16.10.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"i18n-iso-countries": "^7.6.0",
|
||||
"ioredis": "^5.3.2",
|
||||
|
||||
@@ -1,38 +1,35 @@
|
||||
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { Inject, Module, OnModuleDestroy, OnModuleInit, ValidationPipe } from '@nestjs/common';
|
||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE, ModuleRef } from '@nestjs/core';
|
||||
import { GraphQLModule } from '@nestjs/graphql';
|
||||
import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { ClsModule } from 'nestjs-cls';
|
||||
import { KyselyModule } from 'nestjs-kysely';
|
||||
import { OpenTelemetryModule } from 'nestjs-otel';
|
||||
import { join } from 'node:path';
|
||||
import postgres from 'postgres';
|
||||
import { commands } from 'src/commands';
|
||||
import { IWorker } from 'src/constants';
|
||||
import { controllers } from 'src/controllers';
|
||||
import { entities } from 'src/entities';
|
||||
import { ImmichWorker } from 'src/enum';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository } from 'src/interfaces/job.interface';
|
||||
import { AuthGuard } from 'src/middleware/auth.guard';
|
||||
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
|
||||
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
||||
import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter';
|
||||
import { LoggingInterceptor } from 'src/middleware/logging.interceptor';
|
||||
import { providers, repositories } from 'src/repositories';
|
||||
import { repositories } from 'src/repositories';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||
import { resolvers } from 'src/resolvers';
|
||||
import { services } from 'src/services';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
import { DatabaseService } from 'src/services/database.service';
|
||||
|
||||
const common = [...services, ...providers, ...repositories];
|
||||
const common = [...repositories, ...services];
|
||||
|
||||
const middleware = [
|
||||
FileUploadInterceptor,
|
||||
@@ -82,21 +79,30 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||
constructor(
|
||||
@Inject(IWorker) private worker: ImmichWorker,
|
||||
logger: LoggingRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
private eventRepository: EventRepository,
|
||||
private jobRepository: JobRepository,
|
||||
private telemetryRepository: TelemetryRepository,
|
||||
private authService: AuthService,
|
||||
) {
|
||||
logger.setAppName(this.worker);
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.telemetryRepository.setup({ repositories: [...providers.map(({ useClass }) => useClass), ...repositories] });
|
||||
this.telemetryRepository.setup({ repositories });
|
||||
|
||||
this.jobRepository.setup({ services });
|
||||
if (this.worker === ImmichWorker.MICROSERVICES) {
|
||||
this.jobRepository.startWorkers();
|
||||
}
|
||||
|
||||
this.eventRepository.setAuthFn(async (client) =>
|
||||
this.authService.authenticate({
|
||||
headers: client.request.headers,
|
||||
queryParams: {},
|
||||
metadata: { adminRoute: false, sharedLinkRoute: false, uri: '/api/socket.io' },
|
||||
}),
|
||||
);
|
||||
|
||||
this.eventRepository.setup({ services });
|
||||
await this.eventRepository.emit('app.bootstrap');
|
||||
}
|
||||
@@ -108,28 +114,9 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
...imports,
|
||||
ScheduleModule.forRoot(),
|
||||
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||
driver: ApolloDriver,
|
||||
playground: true,
|
||||
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
|
||||
sortSchema: true,
|
||||
debug: true,
|
||||
buildSchemaOptions: {
|
||||
numberScalarMode: 'integer',
|
||||
},
|
||||
}),
|
||||
],
|
||||
imports: [...imports, ScheduleModule.forRoot()],
|
||||
controllers: [...controllers],
|
||||
providers: [
|
||||
//
|
||||
...common,
|
||||
...middleware,
|
||||
...resolvers,
|
||||
{ provide: IWorker, useValue: ImmichWorker.API },
|
||||
],
|
||||
providers: [...common, ...middleware, { provide: IWorker, useValue: ImmichWorker.API }],
|
||||
})
|
||||
export class ApiModule extends BaseModule {}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Reflector } from '@nestjs/core';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { KyselyModule } from 'nestjs-kysely';
|
||||
import { OpenTelemetryModule } from 'nestjs-otel';
|
||||
@@ -13,7 +14,7 @@ import postgres from 'postgres';
|
||||
import { format } from 'sql-formatter';
|
||||
import { GENERATE_SQL_KEY, GenerateSqlQueries } from 'src/decorators';
|
||||
import { entities } from 'src/entities';
|
||||
import { providers, repositories } from 'src/repositories';
|
||||
import { repositories } from 'src/repositories';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
@@ -45,8 +46,7 @@ export class SqlLogger implements Logger {
|
||||
|
||||
const reflector = new Reflector();
|
||||
|
||||
type Repository = (typeof providers)[0]['useClass'];
|
||||
type Provider = { provide: any; useClass: Repository };
|
||||
type Repository = ClassConstructor<any>;
|
||||
type SqlGeneratorOptions = { targetDir: string };
|
||||
|
||||
class SqlGenerator {
|
||||
@@ -59,15 +59,11 @@ class SqlGenerator {
|
||||
async run() {
|
||||
try {
|
||||
await this.setup();
|
||||
const targets = [
|
||||
...providers,
|
||||
...repositories.map((repository) => ({ provide: repository, useClass: repository as any })),
|
||||
];
|
||||
for (const repository of targets) {
|
||||
if (repository.provide === LoggingRepository) {
|
||||
for (const Repository of repositories) {
|
||||
if (Repository === LoggingRepository) {
|
||||
continue;
|
||||
}
|
||||
await this.process(repository);
|
||||
await this.process(Repository);
|
||||
}
|
||||
await this.write();
|
||||
this.stats();
|
||||
@@ -105,19 +101,19 @@ class SqlGenerator {
|
||||
TypeOrmModule.forFeature(entities),
|
||||
OpenTelemetryModule.forRoot(otel),
|
||||
],
|
||||
providers: [...providers, ...repositories, AuthService, SchedulerRegistry],
|
||||
providers: [...repositories, AuthService, SchedulerRegistry],
|
||||
}).compile();
|
||||
|
||||
this.app = await moduleFixture.createNestApplication().init();
|
||||
}
|
||||
|
||||
async process({ provide: token, useClass: Repository }: Provider) {
|
||||
async process(Repository: Repository) {
|
||||
if (!this.app) {
|
||||
throw new Error('Not initialized');
|
||||
}
|
||||
|
||||
const data: string[] = [`-- NOTE: This file is auto generated by ./sql-generator`];
|
||||
const instance = this.app.get<Repository>(token);
|
||||
const instance = this.app.get<Repository>(Repository);
|
||||
|
||||
// normal repositories
|
||||
data.push(...(await this.runTargets(instance, `${Repository.name}`)));
|
||||
|
||||
@@ -5,14 +5,14 @@ import {
|
||||
CQMode,
|
||||
ImageFormat,
|
||||
LogLevel,
|
||||
QueueName,
|
||||
ToneMapping,
|
||||
TranscodeHWAccel,
|
||||
TranscodePolicy,
|
||||
VideoCodec,
|
||||
VideoContainer,
|
||||
} from 'src/enum';
|
||||
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
||||
import { ImageOptions } from 'src/types';
|
||||
import { ConcurrentQueueName, ImageOptions } from 'src/types';
|
||||
|
||||
export interface SystemConfig {
|
||||
backup: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Duration } from 'luxon';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { SemVer } from 'semver';
|
||||
import { ExifOrientation } from 'src/enum';
|
||||
import { DatabaseExtension, ExifOrientation } from 'src/enum';
|
||||
|
||||
export const POSTGRES_VERSION_RANGE = '>=14.0.0';
|
||||
export const VECTORS_VERSION_RANGE = '>=0.2 <0.4';
|
||||
@@ -16,6 +16,16 @@ export const LIFECYCLE_EXTENSION = 'x-immich-lifecycle';
|
||||
export const DEPRECATED_IN_PREFIX = 'This property was deprecated in ';
|
||||
export const ADDED_IN_PREFIX = 'This property was added in ';
|
||||
|
||||
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
|
||||
export const JOBS_LIBRARY_PAGINATION_SIZE = 10_000;
|
||||
|
||||
export const EXTENSION_NAMES: Record<DatabaseExtension, string> = {
|
||||
cube: 'cube',
|
||||
earthdistance: 'earthdistance',
|
||||
vector: 'pgvector',
|
||||
vectors: 'pgvecto.rs',
|
||||
} as const;
|
||||
|
||||
export const SALT_ROUNDS = 10;
|
||||
|
||||
export const IWorker = 'IWorker';
|
||||
@@ -33,7 +43,7 @@ export const citiesFile = 'cities500.txt';
|
||||
export const MOBILE_REDIRECT = 'app.immich:///oauth-callback';
|
||||
export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
||||
|
||||
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico', '/graphql'];
|
||||
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
|
||||
|
||||
export const FACE_THUMBNAIL_SIZE = 250;
|
||||
|
||||
|
||||
@@ -35,9 +35,10 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ImmichHeader, RouteKey } from 'src/enum';
|
||||
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
|
||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||
import { FileUploadInterceptor, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor';
|
||||
import { FileUploadInterceptor, getFiles } from 'src/middleware/file-upload.interceptor';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||
import { UploadFiles } from 'src/types';
|
||||
import { sendFile } from 'src/utils/file';
|
||||
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Req,
|
||||
Res,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
@@ -39,26 +38,13 @@ export class UserController {
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
async searchUsers(@Req() req: Request): Promise<UserResponseDto[]> {
|
||||
const response = await fetch(`http://localhost:2283/graphql`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
operationName: null,
|
||||
query: '{ users { id name email } }',
|
||||
}),
|
||||
headers: {
|
||||
...req.headers,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = await response.json();
|
||||
return data.users;
|
||||
searchUsers(@Auth() auth: AuthDto): Promise<UserResponseDto[]> {
|
||||
return this.service.search(auth);
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
@Authenticated()
|
||||
getMyUser(@Auth() auth: AuthDto): UserAdminResponseDto {
|
||||
getMyUser(@Auth() auth: AuthDto): Promise<UserAdminResponseDto> {
|
||||
return this.service.getMe(auth);
|
||||
}
|
||||
|
||||
@@ -70,7 +56,7 @@ export class UserController {
|
||||
|
||||
@Get('me/preferences')
|
||||
@Authenticated()
|
||||
getMyPreferences(@Auth() auth: AuthDto): UserPreferencesResponseDto {
|
||||
getMyPreferences(@Auth() auth: AuthDto): Promise<UserPreferencesResponseDto> {
|
||||
return this.service.getMyPreferences(auth);
|
||||
}
|
||||
|
||||
@@ -85,7 +71,7 @@ export class UserController {
|
||||
|
||||
@Get('me/license')
|
||||
@Authenticated()
|
||||
getUserLicense(@Auth() auth: AuthDto): LicenseResponseDto {
|
||||
getUserLicense(@Auth() auth: AuthDto): Promise<LicenseResponseDto> {
|
||||
return this.service.getLicense(auth);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ vitest.mock('src/constants', () => ({
|
||||
APP_MEDIA_LOCATION: '/photos',
|
||||
ADDED_IN_PREFIX: 'This property was added in ',
|
||||
DEPRECATED_IN_PREFIX: 'This property was deprecated in ',
|
||||
IWorker: 'IWorker',
|
||||
}));
|
||||
|
||||
describe('StorageCore', () => {
|
||||
|
||||
@@ -4,13 +4,13 @@ import { APP_MEDIA_LOCATION } from 'src/constants';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { getAssetFiles } from 'src/utils/asset.util';
|
||||
import { getConfig } from 'src/utils/config';
|
||||
@@ -33,23 +33,23 @@ let instance: StorageCore | null;
|
||||
|
||||
export class StorageCore {
|
||||
private constructor(
|
||||
private assetRepository: IAssetRepository,
|
||||
private assetRepository: AssetRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
private cryptoRepository: CryptoRepository,
|
||||
private moveRepository: IMoveRepository,
|
||||
private personRepository: IPersonRepository,
|
||||
private storageRepository: IStorageRepository,
|
||||
private moveRepository: MoveRepository,
|
||||
private personRepository: PersonRepository,
|
||||
private storageRepository: StorageRepository,
|
||||
private systemMetadataRepository: SystemMetadataRepository,
|
||||
private logger: LoggingRepository,
|
||||
) {}
|
||||
|
||||
static create(
|
||||
assetRepository: IAssetRepository,
|
||||
assetRepository: AssetRepository,
|
||||
configRepository: ConfigRepository,
|
||||
cryptoRepository: CryptoRepository,
|
||||
moveRepository: IMoveRepository,
|
||||
personRepository: IPersonRepository,
|
||||
storageRepository: IStorageRepository,
|
||||
moveRepository: MoveRepository,
|
||||
personRepository: PersonRepository,
|
||||
storageRepository: StorageRepository,
|
||||
systemMetadataRepository: SystemMetadataRepository,
|
||||
logger: LoggingRepository,
|
||||
) {
|
||||
|
||||
@@ -1,3 +1,53 @@
|
||||
import { Permission } from 'src/enum';
|
||||
|
||||
export type AuthUser = {
|
||||
id: string;
|
||||
isAdmin: boolean;
|
||||
name: string;
|
||||
email: string;
|
||||
quotaUsageInBytes: number;
|
||||
quotaSizeInBytes: number | null;
|
||||
};
|
||||
|
||||
export type AuthApiKey = {
|
||||
id: string;
|
||||
permissions: Permission[];
|
||||
};
|
||||
|
||||
export type AuthSharedLink = {
|
||||
id: string;
|
||||
expiresAt: Date | null;
|
||||
userId: string;
|
||||
showExif: boolean;
|
||||
allowUpload: boolean;
|
||||
allowDownload: boolean;
|
||||
password: string | null;
|
||||
};
|
||||
|
||||
export type AuthSession = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const columns = {
|
||||
authUser: [
|
||||
'users.id',
|
||||
'users.name',
|
||||
'users.email',
|
||||
'users.isAdmin',
|
||||
'users.quotaUsageInBytes',
|
||||
'users.quotaSizeInBytes',
|
||||
],
|
||||
authApiKey: ['api_keys.id', 'api_keys.permissions'],
|
||||
authSession: ['sessions.id', 'sessions.updatedAt'],
|
||||
authSharedLink: [
|
||||
'shared_links.id',
|
||||
'shared_links.userId',
|
||||
'shared_links.expiresAt',
|
||||
'shared_links.showExif',
|
||||
'shared_links.allowUpload',
|
||||
'shared_links.allowDownload',
|
||||
'shared_links.password',
|
||||
],
|
||||
userDto: ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'],
|
||||
apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'],
|
||||
} as const;
|
||||
|
||||
32
server/src/db.d.ts
vendored
32
server/src/db.d.ts
vendored
@@ -3,23 +3,19 @@
|
||||
* Please do not edit it manually.
|
||||
*/
|
||||
|
||||
import type { ColumnType } from "kysely";
|
||||
import type { ColumnType } from 'kysely';
|
||||
import { Permission } from 'src/enum';
|
||||
|
||||
export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[]
|
||||
? U[]
|
||||
: ArrayTypeImpl<T>;
|
||||
export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[] ? U[] : ArrayTypeImpl<T>;
|
||||
|
||||
export type ArrayTypeImpl<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
? ColumnType<S[], I[], U[]>
|
||||
: T[];
|
||||
export type ArrayTypeImpl<T> = T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S[], I[], U[]> : T[];
|
||||
|
||||
export type AssetsStatusEnum = "active" | "deleted" | "trashed";
|
||||
export type AssetsStatusEnum = 'active' | 'deleted' | 'trashed';
|
||||
|
||||
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
? ColumnType<S, I | undefined, U>
|
||||
: ColumnType<T, T | undefined, T>;
|
||||
export type Generated<T> =
|
||||
T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S, I | undefined, U> : ColumnType<T, T | undefined, T>;
|
||||
|
||||
export type Int8 = ColumnType<string, bigint | number | string, bigint | number | string>;
|
||||
export type Int8 = ColumnType<number>;
|
||||
|
||||
export type Json = JsonValue;
|
||||
|
||||
@@ -33,7 +29,7 @@ export type JsonPrimitive = boolean | number | string | null;
|
||||
|
||||
export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
|
||||
|
||||
export type Sourcetype = "exif" | "machine-learning";
|
||||
export type Sourcetype = 'exif' | 'machine-learning';
|
||||
|
||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||
|
||||
@@ -81,7 +77,7 @@ export interface ApiKeys {
|
||||
id: Generated<string>;
|
||||
key: string;
|
||||
name: string;
|
||||
permissions: string[];
|
||||
permissions: Permission[];
|
||||
updatedAt: Generated<Timestamp>;
|
||||
userId: string;
|
||||
}
|
||||
@@ -126,8 +122,8 @@ export interface Assets {
|
||||
duplicateId: string | null;
|
||||
duration: string | null;
|
||||
encodedVideoPath: Generated<string | null>;
|
||||
fileCreatedAt: Timestamp;
|
||||
fileModifiedAt: Timestamp;
|
||||
fileCreatedAt: Timestamp | null;
|
||||
fileModifiedAt: Timestamp | null;
|
||||
id: Generated<string>;
|
||||
isArchived: Generated<boolean>;
|
||||
isExternal: Generated<boolean>;
|
||||
@@ -136,7 +132,7 @@ export interface Assets {
|
||||
isVisible: Generated<boolean>;
|
||||
libraryId: string | null;
|
||||
livePhotoVideoId: string | null;
|
||||
localDateTime: Timestamp;
|
||||
localDateTime: Timestamp | null;
|
||||
originalFileName: string;
|
||||
originalPath: string;
|
||||
ownerId: string;
|
||||
@@ -444,6 +440,6 @@ export interface DB {
|
||||
typeorm_metadata: TypeormMetadata;
|
||||
user_metadata: UserMetadata;
|
||||
users: Users;
|
||||
"vectors.pg_vector_index_stat": VectorsPgVectorIndexStat;
|
||||
'vectors.pg_vector_index_stat': VectorsPgVectorIndexStat;
|
||||
version_history: VersionHistory;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ import { SetMetadata, applyDecorators } from '@nestjs/common';
|
||||
import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||
import _ from 'lodash';
|
||||
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
||||
import { ImmichWorker, MetadataKey } from 'src/enum';
|
||||
import { EmitEvent } from 'src/interfaces/event.interface';
|
||||
import { JobName, QueueName } from 'src/interfaces/job.interface';
|
||||
import { ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum';
|
||||
import { EmitEvent } from 'src/repositories/event.repository';
|
||||
import { setUnion } from 'src/utils/set';
|
||||
|
||||
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AssetType } from 'src/enum';
|
||||
import { AssetStats } from 'src/interfaces/asset.interface';
|
||||
import { AssetType, ExifOrientation } from 'src/enum';
|
||||
import { AssetStats } from 'src/repositories/asset.repository';
|
||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class DeviceIdDto {
|
||||
@@ -54,6 +54,12 @@ export class UpdateAssetBase {
|
||||
@Max(5)
|
||||
@Min(-1)
|
||||
rating?: number;
|
||||
|
||||
@Optional()
|
||||
@Min(1)
|
||||
@Max(8)
|
||||
@ApiProperty({ type: 'integer' })
|
||||
orientation?: ExifOrientation;
|
||||
}
|
||||
|
||||
export class AssetBulkUpdateDto extends UpdateAssetBase {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
import { SessionEntity } from 'src/entities/session.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser } from 'src/database';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { ImmichCookie } from 'src/enum';
|
||||
import { AuthApiKey } from 'src/types';
|
||||
import { toEmail } from 'src/validation';
|
||||
|
||||
export type CookieResponse = {
|
||||
@@ -14,11 +12,11 @@ export type CookieResponse = {
|
||||
};
|
||||
|
||||
export class AuthDto {
|
||||
user!: UserEntity;
|
||||
user!: AuthUser;
|
||||
|
||||
apiKey?: AuthApiKey;
|
||||
sharedLink?: SharedLinkEntity;
|
||||
session?: SessionEntity;
|
||||
sharedLink?: AuthSharedLink;
|
||||
session?: AuthSession;
|
||||
}
|
||||
|
||||
export class LoginCredentialDto {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty } from 'class-validator';
|
||||
import { ManualJobName } from 'src/enum';
|
||||
import { JobCommand, QueueName } from 'src/interfaces/job.interface';
|
||||
import { JobCommand, ManualJobName, QueueName } from 'src/enum';
|
||||
import { ValidateBoolean } from 'src/validation';
|
||||
|
||||
export class JobIdParamDto {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty } from 'class-validator';
|
||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { PartnerDirection } from 'src/interfaces/partner.interface';
|
||||
import { PartnerDirection } from 'src/repositories/partner.repository';
|
||||
|
||||
export class UpdatePartnerDto {
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -25,13 +25,14 @@ import {
|
||||
Colorspace,
|
||||
ImageFormat,
|
||||
LogLevel,
|
||||
QueueName,
|
||||
ToneMapping,
|
||||
TranscodeHWAccel,
|
||||
TranscodePolicy,
|
||||
VideoCodec,
|
||||
VideoContainer,
|
||||
} from 'src/enum';
|
||||
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
||||
import { ConcurrentQueueName } from 'src/types';
|
||||
import { IsCronExpression, ValidateBoolean } from 'src/validation';
|
||||
|
||||
const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { AssetOrder } from 'src/enum';
|
||||
import { TimeBucketSize } from 'src/interfaces/asset.interface';
|
||||
import { TimeBucketSize } from 'src/repositories/asset.repository';
|
||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class TimeBucketDto {
|
||||
|
||||
@@ -47,7 +47,7 @@ export const mapUser = (entity: UserEntity): UserResponseDto => {
|
||||
email: entity.email,
|
||||
name: entity.name,
|
||||
profileImagePath: entity.profileImagePath,
|
||||
avatarColor: getPreferences(entity).avatar.color,
|
||||
avatarColor: getPreferences(entity.email, entity.metadata || []).avatar.color,
|
||||
profileChangedAt: entity.profileChangedAt,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,8 +13,8 @@ import { StackEntity } from 'src/entities/stack.entity';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
|
||||
import { TimeBucketSize } from 'src/interfaces/asset.interface';
|
||||
import { AssetSearchBuilderOptions } from 'src/interfaces/search.interface';
|
||||
import { TimeBucketSize } from 'src/repositories/asset.repository';
|
||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
||||
import { anyUuid, asUuid } from 'src/utils/database';
|
||||
import {
|
||||
Column,
|
||||
@@ -100,13 +100,13 @@ export class AssetEntity {
|
||||
deletedAt!: Date | null;
|
||||
|
||||
@Index('idx_asset_file_created_at')
|
||||
@Column({ type: 'timestamptz' })
|
||||
@Column({ type: 'timestamptz', nullable: true, default: null })
|
||||
fileCreatedAt!: Date;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
@Column({ type: 'timestamptz', nullable: true, default: null })
|
||||
localDateTime!: Date;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
@Column({ type: 'timestamptz', nullable: true, default: null })
|
||||
fileModifiedAt!: Date;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
@@ -180,6 +180,12 @@ export class AssetEntity {
|
||||
duplicateId!: string | null;
|
||||
}
|
||||
|
||||
export type AssetEntityPlaceholder = AssetEntity & {
|
||||
fileCreatedAt: Date | null;
|
||||
fileModifiedAt: Date | null;
|
||||
localDateTime: Date | null;
|
||||
};
|
||||
|
||||
export function withExif<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||
return qb.leftJoin('exif', 'assets.id', 'exif.assetId').select((eb) => eb.fn.toJson(eb.table('exif')).as('exifInfo'));
|
||||
}
|
||||
@@ -419,5 +425,8 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
|
||||
)
|
||||
.$if(!!options.withExif, withExifInner)
|
||||
.$if(!!(options.withFaces || options.withPeople || options.personIds), (qb) => qb.select(withFacesAndPeople))
|
||||
.$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null));
|
||||
.$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null))
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,18 @@ import { DeepPartial } from 'src/types';
|
||||
import { HumanReadableSize } from 'src/utils/bytes';
|
||||
import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
|
||||
export type UserMetadataItem<T extends keyof UserMetadata = UserMetadataKey> = {
|
||||
key: T;
|
||||
value: UserMetadata[T];
|
||||
};
|
||||
|
||||
@Entity('user_metadata')
|
||||
export class UserMetadataEntity<T extends keyof UserMetadata = UserMetadataKey> {
|
||||
export class UserMetadataEntity<T extends keyof UserMetadata = UserMetadataKey> implements UserMetadataItem<T> {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
userId!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, (user) => user.metadata, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
||||
user!: UserEntity;
|
||||
user?: UserEntity;
|
||||
|
||||
@PrimaryColumn({ type: 'varchar' })
|
||||
key!: T;
|
||||
|
||||
@@ -384,3 +384,156 @@ export enum ExifOrientation {
|
||||
MirrorHorizontalRotate90CW = 7,
|
||||
Rotate270CW = 8,
|
||||
}
|
||||
|
||||
export enum DatabaseExtension {
|
||||
CUBE = 'cube',
|
||||
EARTH_DISTANCE = 'earthdistance',
|
||||
VECTOR = 'vector',
|
||||
VECTORS = 'vectors',
|
||||
}
|
||||
|
||||
export enum BootstrapEventPriority {
|
||||
// Database service should be initialized before anything else, most other services need database access
|
||||
DatabaseService = -200,
|
||||
// Initialise config after other bootstrap services, stop other services from using config on bootstrap
|
||||
SystemConfig = 100,
|
||||
}
|
||||
|
||||
export enum QueueName {
|
||||
THUMBNAIL_GENERATION = 'thumbnailGeneration',
|
||||
METADATA_EXTRACTION = 'metadataExtraction',
|
||||
VIDEO_CONVERSION = 'videoConversion',
|
||||
FACE_DETECTION = 'faceDetection',
|
||||
FACIAL_RECOGNITION = 'facialRecognition',
|
||||
SMART_SEARCH = 'smartSearch',
|
||||
DUPLICATE_DETECTION = 'duplicateDetection',
|
||||
BACKGROUND_TASK = 'backgroundTask',
|
||||
STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration',
|
||||
MIGRATION = 'migration',
|
||||
SEARCH = 'search',
|
||||
SIDECAR = 'sidecar',
|
||||
LIBRARY = 'library',
|
||||
NOTIFICATION = 'notifications',
|
||||
BACKUP_DATABASE = 'backupDatabase',
|
||||
}
|
||||
|
||||
export enum JobName {
|
||||
//backups
|
||||
BACKUP_DATABASE = 'database-backup',
|
||||
|
||||
// conversion
|
||||
QUEUE_VIDEO_CONVERSION = 'queue-video-conversion',
|
||||
VIDEO_CONVERSION = 'video-conversion',
|
||||
|
||||
// thumbnails
|
||||
QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails',
|
||||
GENERATE_THUMBNAILS = 'generate-thumbnails',
|
||||
GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail',
|
||||
|
||||
// metadata
|
||||
QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction',
|
||||
METADATA_EXTRACTION = 'metadata-extraction',
|
||||
LINK_LIVE_PHOTOS = 'link-live-photos',
|
||||
|
||||
// user
|
||||
USER_DELETION = 'user-deletion',
|
||||
USER_DELETE_CHECK = 'user-delete-check',
|
||||
USER_SYNC_USAGE = 'user-sync-usage',
|
||||
|
||||
// asset
|
||||
ASSET_DELETION = 'asset-deletion',
|
||||
ASSET_DELETION_CHECK = 'asset-deletion-check',
|
||||
|
||||
// storage template
|
||||
STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration',
|
||||
STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single',
|
||||
|
||||
// tags
|
||||
TAG_CLEANUP = 'tag-cleanup',
|
||||
|
||||
// migration
|
||||
QUEUE_MIGRATION = 'queue-migration',
|
||||
MIGRATE_ASSET = 'migrate-asset',
|
||||
MIGRATE_PERSON = 'migrate-person',
|
||||
|
||||
// facial recognition
|
||||
PERSON_CLEANUP = 'person-cleanup',
|
||||
QUEUE_FACE_DETECTION = 'queue-face-detection',
|
||||
FACE_DETECTION = 'face-detection',
|
||||
QUEUE_FACIAL_RECOGNITION = 'queue-facial-recognition',
|
||||
FACIAL_RECOGNITION = 'facial-recognition',
|
||||
|
||||
// library management
|
||||
LIBRARY_QUEUE_SYNC_FILES = 'library-queue-sync-files',
|
||||
LIBRARY_QUEUE_SYNC_ASSETS = 'library-queue-sync-assets',
|
||||
LIBRARY_SYNC_FILE = 'library-sync-file',
|
||||
LIBRARY_SYNC_ASSET = 'library-sync-asset',
|
||||
LIBRARY_DELETE = 'library-delete',
|
||||
LIBRARY_QUEUE_SYNC_ALL = 'library-queue-sync-all',
|
||||
LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup',
|
||||
|
||||
// cleanup
|
||||
DELETE_FILES = 'delete-files',
|
||||
CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs',
|
||||
CLEAN_OLD_SESSION_TOKENS = 'clean-old-session-tokens',
|
||||
|
||||
// smart search
|
||||
QUEUE_SMART_SEARCH = 'queue-smart-search',
|
||||
SMART_SEARCH = 'smart-search',
|
||||
|
||||
QUEUE_TRASH_EMPTY = 'queue-trash-empty',
|
||||
|
||||
// duplicate detection
|
||||
QUEUE_DUPLICATE_DETECTION = 'queue-duplicate-detection',
|
||||
DUPLICATE_DETECTION = 'duplicate-detection',
|
||||
|
||||
// XMP sidecars
|
||||
QUEUE_SIDECAR = 'queue-sidecar',
|
||||
SIDECAR_DISCOVERY = 'sidecar-discovery',
|
||||
SIDECAR_SYNC = 'sidecar-sync',
|
||||
SIDECAR_WRITE = 'sidecar-write',
|
||||
|
||||
// Notification
|
||||
NOTIFY_SIGNUP = 'notify-signup',
|
||||
NOTIFY_ALBUM_INVITE = 'notify-album-invite',
|
||||
NOTIFY_ALBUM_UPDATE = 'notify-album-update',
|
||||
SEND_EMAIL = 'notification-send-email',
|
||||
|
||||
// Version check
|
||||
VERSION_CHECK = 'version-check',
|
||||
}
|
||||
|
||||
export enum JobCommand {
|
||||
START = 'start',
|
||||
PAUSE = 'pause',
|
||||
RESUME = 'resume',
|
||||
EMPTY = 'empty',
|
||||
CLEAR_FAILED = 'clear-failed',
|
||||
}
|
||||
|
||||
export enum JobStatus {
|
||||
SUCCESS = 'success',
|
||||
FAILED = 'failed',
|
||||
SKIPPED = 'skipped',
|
||||
}
|
||||
|
||||
export enum QueueCleanType {
|
||||
FAILED = 'failed',
|
||||
}
|
||||
|
||||
export enum VectorIndex {
|
||||
CLIP = 'clip_index',
|
||||
FACE = 'face_index',
|
||||
}
|
||||
|
||||
export enum DatabaseLock {
|
||||
GeodataImport = 100,
|
||||
Migrations = 200,
|
||||
SystemFileMounts = 300,
|
||||
StorageTemplateMigration = 420,
|
||||
VersionHistory = 500,
|
||||
CLIPDimSize = 512,
|
||||
Library = 1337,
|
||||
GetSystemConfig = 69,
|
||||
BackupDatabase = 42,
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Insertable, Updateable } from 'kysely';
|
||||
import { Albums } from 'src/db';
|
||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { IBulkAsset } from 'src/utils/asset.util';
|
||||
|
||||
export const IAlbumRepository = 'IAlbumRepository';
|
||||
|
||||
export interface AlbumAssetCount {
|
||||
albumId: string;
|
||||
assetCount: number;
|
||||
startDate: Date | null;
|
||||
endDate: Date | null;
|
||||
}
|
||||
|
||||
export interface AlbumInfoOptions {
|
||||
withAssets: boolean;
|
||||
}
|
||||
|
||||
export interface IAlbumRepository extends IBulkAsset {
|
||||
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | undefined>;
|
||||
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
||||
removeAsset(assetId: string): Promise<void>;
|
||||
getMetadataForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
||||
getOwned(ownerId: string): Promise<AlbumEntity[]>;
|
||||
getShared(ownerId: string): Promise<AlbumEntity[]>;
|
||||
getNotShared(ownerId: string): Promise<AlbumEntity[]>;
|
||||
restoreAll(userId: string): Promise<void>;
|
||||
softDeleteAll(userId: string): Promise<void>;
|
||||
deleteAll(userId: string): Promise<void>;
|
||||
create(album: Insertable<Albums>, assetIds: string[], albumUsers: AlbumUserCreateDto[]): Promise<AlbumEntity>;
|
||||
update(id: string, album: Updateable<Albums>): Promise<AlbumEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
updateThumbnails(): Promise<number | undefined>;
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
import { Insertable, Updateable } from 'kysely';
|
||||
import { AssetFiles, AssetJobStatus, Assets, Exif } from 'src/db';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
|
||||
import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
|
||||
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
||||
|
||||
export type AssetStats = Record<AssetType, number>;
|
||||
|
||||
export interface AssetStatsOptions {
|
||||
isFavorite?: boolean;
|
||||
isArchived?: boolean;
|
||||
isTrashed?: boolean;
|
||||
}
|
||||
|
||||
export interface LivePhotoSearchOptions {
|
||||
ownerId: string;
|
||||
libraryId?: string | null;
|
||||
livePhotoCID: string;
|
||||
otherAssetId: string;
|
||||
type: AssetType;
|
||||
}
|
||||
|
||||
export enum WithoutProperty {
|
||||
THUMBNAIL = 'thumbnail',
|
||||
ENCODED_VIDEO = 'encoded-video',
|
||||
EXIF = 'exif',
|
||||
SMART_SEARCH = 'smart-search',
|
||||
DUPLICATE = 'duplicate',
|
||||
FACES = 'faces',
|
||||
SIDECAR = 'sidecar',
|
||||
}
|
||||
|
||||
export enum WithProperty {
|
||||
SIDECAR = 'sidecar',
|
||||
}
|
||||
|
||||
export enum TimeBucketSize {
|
||||
DAY = 'DAY',
|
||||
MONTH = 'MONTH',
|
||||
}
|
||||
|
||||
export interface AssetBuilderOptions {
|
||||
isArchived?: boolean;
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
isDuplicate?: boolean;
|
||||
albumId?: string;
|
||||
tagId?: string;
|
||||
personId?: string;
|
||||
userIds?: string[];
|
||||
withStacked?: boolean;
|
||||
exifInfo?: boolean;
|
||||
status?: AssetStatus;
|
||||
assetType?: AssetType;
|
||||
}
|
||||
|
||||
export interface TimeBucketOptions extends AssetBuilderOptions {
|
||||
size: TimeBucketSize;
|
||||
order?: AssetOrder;
|
||||
}
|
||||
|
||||
export interface TimeBucketItem {
|
||||
timeBucket: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface MonthDay {
|
||||
day: number;
|
||||
month: number;
|
||||
}
|
||||
|
||||
export interface AssetExploreFieldOptions {
|
||||
maxFields: number;
|
||||
minAssetsPerField: number;
|
||||
}
|
||||
|
||||
export interface AssetFullSyncOptions {
|
||||
ownerId: string;
|
||||
lastId?: string;
|
||||
updatedUntil: Date;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface AssetDeltaSyncOptions {
|
||||
userIds: string[];
|
||||
updatedAfter: Date;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface AssetUpdateDuplicateOptions {
|
||||
targetDuplicateId: string | null;
|
||||
assetIds: string[];
|
||||
duplicateIds: string[];
|
||||
}
|
||||
|
||||
export interface UpsertFileOptions {
|
||||
assetId: string;
|
||||
type: AssetFileType;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface AssetGetByChecksumOptions {
|
||||
ownerId: string;
|
||||
checksum: Buffer;
|
||||
libraryId?: string;
|
||||
}
|
||||
|
||||
export type AssetPathEntity = Pick<AssetEntity, 'id' | 'originalPath' | 'isOffline'>;
|
||||
|
||||
export interface GetByIdsRelations {
|
||||
exifInfo?: boolean;
|
||||
faces?: { person?: boolean };
|
||||
files?: boolean;
|
||||
library?: boolean;
|
||||
owner?: boolean;
|
||||
smartSearch?: boolean;
|
||||
stack?: { assets?: boolean };
|
||||
tags?: boolean;
|
||||
}
|
||||
|
||||
export interface DuplicateGroup {
|
||||
duplicateId: string;
|
||||
assets: AssetEntity[];
|
||||
}
|
||||
|
||||
export interface DayOfYearAssets {
|
||||
yearsAgo: number;
|
||||
assets: AssetEntity[];
|
||||
}
|
||||
|
||||
export const IAssetRepository = 'IAssetRepository';
|
||||
|
||||
export interface IAssetRepository {
|
||||
create(asset: Insertable<Assets>): Promise<AssetEntity>;
|
||||
getByIds(ids: string[], relations?: GetByIdsRelations): Promise<AssetEntity[]>;
|
||||
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]>;
|
||||
getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise<DayOfYearAssets[]>;
|
||||
getByChecksum(options: AssetGetByChecksumOptions): Promise<AssetEntity | undefined>;
|
||||
getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]>;
|
||||
getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
|
||||
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
||||
getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<string[]>;
|
||||
getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||
getById(id: string, relations?: GetByIdsRelations): Promise<AssetEntity | undefined>;
|
||||
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
|
||||
getRandom(userIds: string[], count: number): Promise<AssetEntity[]>;
|
||||
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | undefined>;
|
||||
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | undefined>;
|
||||
deleteAll(ownerId: string): Promise<void>;
|
||||
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
|
||||
getLivePhotoCount(motionId: string): Promise<number>;
|
||||
updateAll(ids: string[], options: Updateable<Assets>): Promise<void>;
|
||||
updateDuplicates(options: AssetUpdateDuplicateOptions): Promise<void>;
|
||||
update(asset: Updateable<Assets> & { id: string }): Promise<AssetEntity>;
|
||||
remove(asset: AssetEntity): Promise<void>;
|
||||
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | undefined>;
|
||||
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
|
||||
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
|
||||
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
|
||||
upsertExif(exif: Insertable<Exif>): Promise<void>;
|
||||
upsertJobStatus(...jobStatus: Insertable<AssetJobStatus>[]): Promise<void>;
|
||||
getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
|
||||
getDuplicates(userId: string): Promise<DuplicateGroup[]>;
|
||||
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]>;
|
||||
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]>;
|
||||
upsertFile(options: Insertable<AssetFiles>): Promise<void>;
|
||||
upsertFiles(options: Insertable<AssetFiles>[]): Promise<void>;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export const ICryptoRepository = 'ICryptoRepository';
|
||||
|
||||
export interface ICryptoRepository {
|
||||
randomBytes(size: number): Buffer;
|
||||
randomUUID(): string;
|
||||
hashFile(filePath: string | Buffer): Promise<Buffer>;
|
||||
hashSha256(data: string): string;
|
||||
verifySha256(data: string, encrypted: string, publicKey: string): boolean;
|
||||
hashSha1(data: string | Buffer): Buffer;
|
||||
hashBcrypt(data: string | Buffer, saltOrRounds: string | number): Promise<string>;
|
||||
compareBcrypt(data: string | Buffer, encrypted: string): boolean;
|
||||
newPassword(bytes: number): string;
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
export enum DatabaseExtension {
|
||||
CUBE = 'cube',
|
||||
EARTH_DISTANCE = 'earthdistance',
|
||||
VECTOR = 'vector',
|
||||
VECTORS = 'vectors',
|
||||
}
|
||||
|
||||
export type VectorExtension = DatabaseExtension.VECTOR | DatabaseExtension.VECTORS;
|
||||
|
||||
export type DatabaseConnectionURL = {
|
||||
connectionType: 'url';
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type DatabaseConnectionParts = {
|
||||
connectionType: 'parts';
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
database: string;
|
||||
};
|
||||
|
||||
export type DatabaseConnectionParams = DatabaseConnectionURL | DatabaseConnectionParts;
|
||||
|
||||
export enum VectorIndex {
|
||||
CLIP = 'clip_index',
|
||||
FACE = 'face_index',
|
||||
}
|
||||
|
||||
export enum DatabaseLock {
|
||||
GeodataImport = 100,
|
||||
Migrations = 200,
|
||||
SystemFileMounts = 300,
|
||||
StorageTemplateMigration = 420,
|
||||
VersionHistory = 500,
|
||||
CLIPDimSize = 512,
|
||||
Library = 1337,
|
||||
GetSystemConfig = 69,
|
||||
BackupDatabase = 42,
|
||||
}
|
||||
|
||||
export const EXTENSION_NAMES: Record<DatabaseExtension, string> = {
|
||||
cube: 'cube',
|
||||
earthdistance: 'earthdistance',
|
||||
vector: 'pgvector',
|
||||
vectors: 'pgvecto.rs',
|
||||
} as const;
|
||||
|
||||
export interface ExtensionVersion {
|
||||
availableVersion: string | null;
|
||||
installedVersion: string | null;
|
||||
}
|
||||
|
||||
export interface VectorUpdateResult {
|
||||
restartRequired: boolean;
|
||||
}
|
||||
|
||||
export const IDatabaseRepository = 'IDatabaseRepository';
|
||||
|
||||
export interface IDatabaseRepository {
|
||||
init(): void;
|
||||
reconnect(): Promise<boolean>;
|
||||
shutdown(): Promise<void>;
|
||||
getExtensionVersion(extension: DatabaseExtension): Promise<ExtensionVersion>;
|
||||
getExtensionVersionRange(extension: VectorExtension): string;
|
||||
getPostgresVersion(): Promise<string>;
|
||||
getPostgresVersionRange(): string;
|
||||
createExtension(extension: DatabaseExtension): Promise<void>;
|
||||
updateVectorExtension(extension: VectorExtension, version?: string): Promise<VectorUpdateResult>;
|
||||
reindex(index: VectorIndex): Promise<void>;
|
||||
shouldReindex(name: VectorIndex): Promise<boolean>;
|
||||
runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise<void>;
|
||||
withLock<R>(lock: DatabaseLock, callback: () => Promise<R>): Promise<R>;
|
||||
tryLock(lock: DatabaseLock): Promise<boolean>;
|
||||
isBusy(lock: DatabaseLock): boolean;
|
||||
wait(lock: DatabaseLock): Promise<void>;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { JobItem, QueueName } from 'src/interfaces/job.interface';
|
||||
|
||||
export const IEventRepository = 'IEventRepository';
|
||||
|
||||
type EventMap = {
|
||||
// app events
|
||||
'app.bootstrap': [];
|
||||
'app.shutdown': [];
|
||||
|
||||
'config.init': [{ newConfig: SystemConfig }];
|
||||
// config events
|
||||
'config.update': [
|
||||
{
|
||||
newConfig: SystemConfig;
|
||||
oldConfig: SystemConfig;
|
||||
},
|
||||
];
|
||||
'config.validate': [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
|
||||
|
||||
// album events
|
||||
'album.update': [{ id: string; recipientIds: string[] }];
|
||||
'album.invite': [{ id: string; userId: string }];
|
||||
|
||||
// asset events
|
||||
'asset.tag': [{ assetId: string }];
|
||||
'asset.untag': [{ assetId: string }];
|
||||
'asset.hide': [{ assetId: string; userId: string }];
|
||||
'asset.show': [{ assetId: string; userId: string }];
|
||||
'asset.trash': [{ assetId: string; userId: string }];
|
||||
'asset.delete': [{ assetId: string; userId: string }];
|
||||
|
||||
// asset bulk events
|
||||
'assets.trash': [{ assetIds: string[]; userId: string }];
|
||||
'assets.delete': [{ assetIds: string[]; userId: string }];
|
||||
'assets.restore': [{ assetIds: string[]; userId: string }];
|
||||
|
||||
'job.start': [QueueName, JobItem];
|
||||
|
||||
// session events
|
||||
'session.delete': [{ sessionId: string }];
|
||||
|
||||
// stack events
|
||||
'stack.create': [{ stackId: string; userId: string }];
|
||||
'stack.update': [{ stackId: string; userId: string }];
|
||||
'stack.delete': [{ stackId: string; userId: string }];
|
||||
|
||||
// stack bulk events
|
||||
'stacks.delete': [{ stackIds: string[]; userId: string }];
|
||||
|
||||
// user events
|
||||
'user.signup': [{ notify: boolean; id: string; tempPassword?: string }];
|
||||
|
||||
// websocket events
|
||||
'websocket.connect': [{ userId: string }];
|
||||
};
|
||||
|
||||
export const serverEvents = ['config.update'] as const;
|
||||
export type ServerEvents = (typeof serverEvents)[number];
|
||||
|
||||
export type EmitEvent = keyof EventMap;
|
||||
export type EmitHandler<T extends EmitEvent> = (...args: ArgsOf<T>) => Promise<void> | void;
|
||||
export type ArgOf<T extends EmitEvent> = EventMap[T][0];
|
||||
export type ArgsOf<T extends EmitEvent> = EventMap[T];
|
||||
|
||||
export interface ClientEventMap {
|
||||
on_upload_success: [AssetResponseDto];
|
||||
on_user_delete: [string];
|
||||
on_asset_delete: [string];
|
||||
on_asset_trash: [string[]];
|
||||
on_asset_update: [AssetResponseDto];
|
||||
on_asset_hidden: [string];
|
||||
on_asset_restore: [string[]];
|
||||
on_asset_stack_update: string[];
|
||||
on_person_thumbnail: [string];
|
||||
on_server_version: [ServerVersionResponseDto];
|
||||
on_config_update: [];
|
||||
on_new_release: [ReleaseNotification];
|
||||
on_session_delete: [string];
|
||||
}
|
||||
|
||||
export type EventItem<T extends EmitEvent> = {
|
||||
event: T;
|
||||
handler: EmitHandler<T>;
|
||||
server: boolean;
|
||||
};
|
||||
|
||||
export enum BootstrapEventPriority {
|
||||
// Database service should be initialized before anything else, most other services need database access
|
||||
DatabaseService = -200,
|
||||
// Initialise config after other bootstrap services, stop other services from using config on bootstrap
|
||||
SystemConfig = 100,
|
||||
}
|
||||
|
||||
export interface IEventRepository {
|
||||
setup(options: { services: ClassConstructor<unknown>[] }): void;
|
||||
emit<T extends keyof EventMap>(event: T, ...args: ArgsOf<T>): Promise<void>;
|
||||
|
||||
/**
|
||||
* Send to connected clients for a specific user
|
||||
*/
|
||||
clientSend<E extends keyof ClientEventMap>(event: E, room: string, ...data: ClientEventMap[E]): void;
|
||||
/**
|
||||
* Send to all connected clients
|
||||
*/
|
||||
clientBroadcast<E extends keyof ClientEventMap>(event: E, ...data: ClientEventMap[E]): void;
|
||||
/**
|
||||
* Send to all connected servers
|
||||
*/
|
||||
serverSend<T extends ServerEvents>(event: T, ...args: ArgsOf<T>): void;
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { EmailImageAttachment } from 'src/repositories/notification.repository';
|
||||
|
||||
export enum QueueName {
|
||||
THUMBNAIL_GENERATION = 'thumbnailGeneration',
|
||||
METADATA_EXTRACTION = 'metadataExtraction',
|
||||
VIDEO_CONVERSION = 'videoConversion',
|
||||
FACE_DETECTION = 'faceDetection',
|
||||
FACIAL_RECOGNITION = 'facialRecognition',
|
||||
SMART_SEARCH = 'smartSearch',
|
||||
DUPLICATE_DETECTION = 'duplicateDetection',
|
||||
BACKGROUND_TASK = 'backgroundTask',
|
||||
STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration',
|
||||
MIGRATION = 'migration',
|
||||
SEARCH = 'search',
|
||||
SIDECAR = 'sidecar',
|
||||
LIBRARY = 'library',
|
||||
NOTIFICATION = 'notifications',
|
||||
BACKUP_DATABASE = 'backupDatabase',
|
||||
}
|
||||
|
||||
export type ConcurrentQueueName = Exclude<
|
||||
QueueName,
|
||||
| QueueName.STORAGE_TEMPLATE_MIGRATION
|
||||
| QueueName.FACIAL_RECOGNITION
|
||||
| QueueName.DUPLICATE_DETECTION
|
||||
| QueueName.BACKUP_DATABASE
|
||||
>;
|
||||
|
||||
export enum JobCommand {
|
||||
START = 'start',
|
||||
PAUSE = 'pause',
|
||||
RESUME = 'resume',
|
||||
EMPTY = 'empty',
|
||||
CLEAR_FAILED = 'clear-failed',
|
||||
}
|
||||
|
||||
export enum JobName {
|
||||
//backups
|
||||
BACKUP_DATABASE = 'database-backup',
|
||||
|
||||
// conversion
|
||||
QUEUE_VIDEO_CONVERSION = 'queue-video-conversion',
|
||||
VIDEO_CONVERSION = 'video-conversion',
|
||||
|
||||
// thumbnails
|
||||
QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails',
|
||||
GENERATE_THUMBNAILS = 'generate-thumbnails',
|
||||
GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail',
|
||||
|
||||
// metadata
|
||||
QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction',
|
||||
METADATA_EXTRACTION = 'metadata-extraction',
|
||||
LINK_LIVE_PHOTOS = 'link-live-photos',
|
||||
|
||||
// user
|
||||
USER_DELETION = 'user-deletion',
|
||||
USER_DELETE_CHECK = 'user-delete-check',
|
||||
USER_SYNC_USAGE = 'user-sync-usage',
|
||||
|
||||
// asset
|
||||
ASSET_DELETION = 'asset-deletion',
|
||||
ASSET_DELETION_CHECK = 'asset-deletion-check',
|
||||
|
||||
// storage template
|
||||
STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration',
|
||||
STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single',
|
||||
|
||||
// tags
|
||||
TAG_CLEANUP = 'tag-cleanup',
|
||||
|
||||
// migration
|
||||
QUEUE_MIGRATION = 'queue-migration',
|
||||
MIGRATE_ASSET = 'migrate-asset',
|
||||
MIGRATE_PERSON = 'migrate-person',
|
||||
|
||||
// facial recognition
|
||||
PERSON_CLEANUP = 'person-cleanup',
|
||||
QUEUE_FACE_DETECTION = 'queue-face-detection',
|
||||
FACE_DETECTION = 'face-detection',
|
||||
QUEUE_FACIAL_RECOGNITION = 'queue-facial-recognition',
|
||||
FACIAL_RECOGNITION = 'facial-recognition',
|
||||
|
||||
// library management
|
||||
LIBRARY_QUEUE_SYNC_FILES = 'library-queue-sync-files',
|
||||
LIBRARY_QUEUE_SYNC_ASSETS = 'library-queue-sync-assets',
|
||||
LIBRARY_SYNC_FILE = 'library-sync-file',
|
||||
LIBRARY_SYNC_ASSET = 'library-sync-asset',
|
||||
LIBRARY_DELETE = 'library-delete',
|
||||
LIBRARY_QUEUE_SYNC_ALL = 'library-queue-sync-all',
|
||||
LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup',
|
||||
|
||||
// cleanup
|
||||
DELETE_FILES = 'delete-files',
|
||||
CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs',
|
||||
CLEAN_OLD_SESSION_TOKENS = 'clean-old-session-tokens',
|
||||
|
||||
// smart search
|
||||
QUEUE_SMART_SEARCH = 'queue-smart-search',
|
||||
SMART_SEARCH = 'smart-search',
|
||||
|
||||
QUEUE_TRASH_EMPTY = 'queue-trash-empty',
|
||||
|
||||
// duplicate detection
|
||||
QUEUE_DUPLICATE_DETECTION = 'queue-duplicate-detection',
|
||||
DUPLICATE_DETECTION = 'duplicate-detection',
|
||||
|
||||
// XMP sidecars
|
||||
QUEUE_SIDECAR = 'queue-sidecar',
|
||||
SIDECAR_DISCOVERY = 'sidecar-discovery',
|
||||
SIDECAR_SYNC = 'sidecar-sync',
|
||||
SIDECAR_WRITE = 'sidecar-write',
|
||||
|
||||
// Notification
|
||||
NOTIFY_SIGNUP = 'notify-signup',
|
||||
NOTIFY_ALBUM_INVITE = 'notify-album-invite',
|
||||
NOTIFY_ALBUM_UPDATE = 'notify-album-update',
|
||||
SEND_EMAIL = 'notification-send-email',
|
||||
|
||||
// Version check
|
||||
VERSION_CHECK = 'version-check',
|
||||
}
|
||||
|
||||
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
|
||||
export const JOBS_LIBRARY_PAGINATION_SIZE = 10_000;
|
||||
|
||||
export interface IBaseJob {
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export interface IDelayedJob extends IBaseJob {
|
||||
/** The minimum time to wait to execute this job, in milliseconds. */
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export interface IEntityJob extends IBaseJob {
|
||||
id: string;
|
||||
source?: 'upload' | 'sidecar-write' | 'copy';
|
||||
notify?: boolean;
|
||||
}
|
||||
|
||||
export interface IAssetDeleteJob extends IEntityJob {
|
||||
deleteOnDisk: boolean;
|
||||
}
|
||||
|
||||
export interface ILibraryFileJob extends IEntityJob {
|
||||
ownerId: string;
|
||||
assetPath: string;
|
||||
}
|
||||
|
||||
export interface ILibraryAssetJob extends IEntityJob {
|
||||
importPaths: string[];
|
||||
exclusionPatterns: string[];
|
||||
}
|
||||
|
||||
export interface IBulkEntityJob extends IBaseJob {
|
||||
ids: string[];
|
||||
}
|
||||
|
||||
export interface IDeleteFilesJob extends IBaseJob {
|
||||
files: Array<string | null | undefined>;
|
||||
}
|
||||
|
||||
export interface ISidecarWriteJob extends IEntityJob {
|
||||
description?: string;
|
||||
dateTimeOriginal?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
rating?: number;
|
||||
tags?: true;
|
||||
}
|
||||
|
||||
export interface IDeferrableJob extends IEntityJob {
|
||||
deferred?: boolean;
|
||||
}
|
||||
|
||||
export interface INightlyJob extends IBaseJob {
|
||||
nightly?: boolean;
|
||||
}
|
||||
|
||||
export interface IEmailJob {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
text: string;
|
||||
imageAttachments?: EmailImageAttachment[];
|
||||
}
|
||||
|
||||
export interface INotifySignupJob extends IEntityJob {
|
||||
tempPassword?: string;
|
||||
}
|
||||
|
||||
export interface INotifyAlbumInviteJob extends IEntityJob {
|
||||
recipientId: string;
|
||||
}
|
||||
|
||||
export interface INotifyAlbumUpdateJob extends IEntityJob, IDelayedJob {
|
||||
recipientIds: string[];
|
||||
}
|
||||
|
||||
export interface JobCounts {
|
||||
active: number;
|
||||
completed: number;
|
||||
failed: number;
|
||||
delayed: number;
|
||||
waiting: number;
|
||||
paused: number;
|
||||
}
|
||||
|
||||
export interface QueueStatus {
|
||||
isActive: boolean;
|
||||
isPaused: boolean;
|
||||
}
|
||||
|
||||
export enum QueueCleanType {
|
||||
FAILED = 'failed',
|
||||
}
|
||||
|
||||
export type JobItem =
|
||||
// Backups
|
||||
| { name: JobName.BACKUP_DATABASE; data?: IBaseJob }
|
||||
|
||||
// Transcoding
|
||||
| { name: JobName.QUEUE_VIDEO_CONVERSION; data: IBaseJob }
|
||||
| { name: JobName.VIDEO_CONVERSION; data: IEntityJob }
|
||||
|
||||
// Thumbnails
|
||||
| { name: JobName.QUEUE_GENERATE_THUMBNAILS; data: IBaseJob }
|
||||
| { name: JobName.GENERATE_THUMBNAILS; data: IEntityJob }
|
||||
|
||||
// User
|
||||
| { name: JobName.USER_DELETE_CHECK; data?: IBaseJob }
|
||||
| { name: JobName.USER_DELETION; data: IEntityJob }
|
||||
| { name: JobName.USER_SYNC_USAGE; data?: IBaseJob }
|
||||
|
||||
// Storage Template
|
||||
| { name: JobName.STORAGE_TEMPLATE_MIGRATION; data?: IBaseJob }
|
||||
| { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE; data: IEntityJob }
|
||||
|
||||
// Migration
|
||||
| { name: JobName.QUEUE_MIGRATION; data?: IBaseJob }
|
||||
| { name: JobName.MIGRATE_ASSET; data: IEntityJob }
|
||||
| { name: JobName.MIGRATE_PERSON; data: IEntityJob }
|
||||
|
||||
// Metadata Extraction
|
||||
| { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
|
||||
| { name: JobName.METADATA_EXTRACTION; data: IEntityJob }
|
||||
| { name: JobName.LINK_LIVE_PHOTOS; data: IEntityJob }
|
||||
// Sidecar Scanning
|
||||
| { name: JobName.QUEUE_SIDECAR; data: IBaseJob }
|
||||
| { name: JobName.SIDECAR_DISCOVERY; data: IEntityJob }
|
||||
| { name: JobName.SIDECAR_SYNC; data: IEntityJob }
|
||||
| { name: JobName.SIDECAR_WRITE; data: ISidecarWriteJob }
|
||||
|
||||
// Facial Recognition
|
||||
| { name: JobName.QUEUE_FACE_DETECTION; data: IBaseJob }
|
||||
| { name: JobName.FACE_DETECTION; data: IEntityJob }
|
||||
| { name: JobName.QUEUE_FACIAL_RECOGNITION; data: INightlyJob }
|
||||
| { name: JobName.FACIAL_RECOGNITION; data: IDeferrableJob }
|
||||
| { name: JobName.GENERATE_PERSON_THUMBNAIL; data: IEntityJob }
|
||||
|
||||
// Smart Search
|
||||
| { name: JobName.QUEUE_SMART_SEARCH; data: IBaseJob }
|
||||
| { name: JobName.SMART_SEARCH; data: IEntityJob }
|
||||
| { name: JobName.QUEUE_TRASH_EMPTY; data?: IBaseJob }
|
||||
|
||||
// Duplicate Detection
|
||||
| { name: JobName.QUEUE_DUPLICATE_DETECTION; data: IBaseJob }
|
||||
| { name: JobName.DUPLICATE_DETECTION; data: IEntityJob }
|
||||
|
||||
// Filesystem
|
||||
| { name: JobName.DELETE_FILES; data: IDeleteFilesJob }
|
||||
|
||||
// Cleanup
|
||||
| { name: JobName.CLEAN_OLD_AUDIT_LOGS; data?: IBaseJob }
|
||||
| { name: JobName.CLEAN_OLD_SESSION_TOKENS; data?: IBaseJob }
|
||||
|
||||
// Tags
|
||||
| { name: JobName.TAG_CLEANUP; data?: IBaseJob }
|
||||
|
||||
// Asset Deletion
|
||||
| { name: JobName.PERSON_CLEANUP; data?: IBaseJob }
|
||||
| { name: JobName.ASSET_DELETION; data: IAssetDeleteJob }
|
||||
| { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob }
|
||||
|
||||
// Library Management
|
||||
| { name: JobName.LIBRARY_SYNC_FILE; data: ILibraryFileJob }
|
||||
| { name: JobName.LIBRARY_QUEUE_SYNC_FILES; data: IEntityJob }
|
||||
| { name: JobName.LIBRARY_QUEUE_SYNC_ASSETS; data: IEntityJob }
|
||||
| { name: JobName.LIBRARY_SYNC_ASSET; data: ILibraryAssetJob }
|
||||
| { name: JobName.LIBRARY_DELETE; data: IEntityJob }
|
||||
| { name: JobName.LIBRARY_QUEUE_SYNC_ALL; data?: IBaseJob }
|
||||
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }
|
||||
|
||||
// Notification
|
||||
| { name: JobName.SEND_EMAIL; data: IEmailJob }
|
||||
| { name: JobName.NOTIFY_ALBUM_INVITE; data: INotifyAlbumInviteJob }
|
||||
| { name: JobName.NOTIFY_ALBUM_UPDATE; data: INotifyAlbumUpdateJob }
|
||||
| { name: JobName.NOTIFY_SIGNUP; data: INotifySignupJob }
|
||||
|
||||
// Version check
|
||||
| { name: JobName.VERSION_CHECK; data: IBaseJob };
|
||||
|
||||
export enum JobStatus {
|
||||
SUCCESS = 'success',
|
||||
FAILED = 'failed',
|
||||
SKIPPED = 'skipped',
|
||||
}
|
||||
export type Jobs = { [K in JobItem['name']]: (JobItem & { name: K })['data'] };
|
||||
export type JobOf<T extends JobName> = Jobs[T];
|
||||
|
||||
export const IJobRepository = 'IJobRepository';
|
||||
|
||||
export interface IJobRepository {
|
||||
setup(options: { services: ClassConstructor<unknown>[] }): void;
|
||||
startWorkers(): void;
|
||||
run(job: JobItem): Promise<JobStatus>;
|
||||
setConcurrency(queueName: QueueName, concurrency: number): void;
|
||||
queue(item: JobItem): Promise<void>;
|
||||
queueAll(items: JobItem[]): Promise<void>;
|
||||
pause(name: QueueName): Promise<void>;
|
||||
resume(name: QueueName): Promise<void>;
|
||||
empty(name: QueueName): Promise<void>;
|
||||
clear(name: QueueName, type: QueueCleanType): Promise<string[]>;
|
||||
getQueueStatus(name: QueueName): Promise<QueueStatus>;
|
||||
getJobCounts(name: QueueName): Promise<JobCounts>;
|
||||
waitForQueueCompletion(...queues: QueueName[]): Promise<void>;
|
||||
removeJob(jobId: string, name: JobName): Promise<IEntityJob | undefined>;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Insertable, Updateable } from 'kysely';
|
||||
import { Libraries } from 'src/db';
|
||||
import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
|
||||
export const ILibraryRepository = 'ILibraryRepository';
|
||||
|
||||
export interface ILibraryRepository {
|
||||
getAll(withDeleted?: boolean): Promise<LibraryEntity[]>;
|
||||
getAllDeleted(): Promise<LibraryEntity[]>;
|
||||
get(id: string, withDeleted?: boolean): Promise<LibraryEntity | undefined>;
|
||||
create(library: Insertable<Libraries>): Promise<LibraryEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
softDelete(id: string): Promise<void>;
|
||||
update(id: string, library: Updateable<Libraries>): Promise<LibraryEntity>;
|
||||
getStatistics(id: string): Promise<LibraryStatsResponseDto | undefined>;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
export const IMachineLearningRepository = 'IMachineLearningRepository';
|
||||
|
||||
export interface BoundingBox {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
}
|
||||
|
||||
export enum ModelTask {
|
||||
FACIAL_RECOGNITION = 'facial-recognition',
|
||||
SEARCH = 'clip',
|
||||
}
|
||||
|
||||
export enum ModelType {
|
||||
DETECTION = 'detection',
|
||||
PIPELINE = 'pipeline',
|
||||
RECOGNITION = 'recognition',
|
||||
TEXTUAL = 'textual',
|
||||
VISUAL = 'visual',
|
||||
}
|
||||
|
||||
export type ModelPayload = { imagePath: string } | { text: string };
|
||||
|
||||
type ModelOptions = { modelName: string };
|
||||
|
||||
export type FaceDetectionOptions = ModelOptions & { minScore: number };
|
||||
|
||||
type VisualResponse = { imageHeight: number; imageWidth: number };
|
||||
export type ClipVisualRequest = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: ModelOptions } };
|
||||
export type ClipVisualResponse = { [ModelTask.SEARCH]: string } & VisualResponse;
|
||||
|
||||
export type ClipTextualRequest = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: ModelOptions } };
|
||||
export type ClipTextualResponse = { [ModelTask.SEARCH]: string };
|
||||
|
||||
export type FacialRecognitionRequest = {
|
||||
[ModelTask.FACIAL_RECOGNITION]: {
|
||||
[ModelType.DETECTION]: ModelOptions & { options: { minScore: number } };
|
||||
[ModelType.RECOGNITION]: ModelOptions;
|
||||
};
|
||||
};
|
||||
|
||||
export interface Face {
|
||||
boundingBox: BoundingBox;
|
||||
embedding: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export type FacialRecognitionResponse = { [ModelTask.FACIAL_RECOGNITION]: Face[] } & VisualResponse;
|
||||
export type DetectedFaces = { faces: Face[] } & VisualResponse;
|
||||
export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest;
|
||||
|
||||
export interface IMachineLearningRepository {
|
||||
encodeImage(urls: string[], imagePath: string, config: ModelOptions): Promise<string>;
|
||||
encodeText(urls: string[], text: string, config: ModelOptions): Promise<string>;
|
||||
detectFaces(urls: string[], imagePath: string, config: FaceDetectionOptions): Promise<DetectedFaces>;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Insertable, Updateable } from 'kysely';
|
||||
import { MoveHistory } from 'src/db';
|
||||
import { MoveEntity } from 'src/entities/move.entity';
|
||||
import { PathType } from 'src/enum';
|
||||
|
||||
export const IMoveRepository = 'IMoveRepository';
|
||||
|
||||
export type MoveCreate = Pick<MoveEntity, 'oldPath' | 'newPath' | 'entityId' | 'pathType'> & Partial<MoveEntity>;
|
||||
|
||||
export interface IMoveRepository {
|
||||
create(entity: Insertable<MoveHistory>): Promise<MoveEntity>;
|
||||
getByEntity(entityId: string, pathType: PathType): Promise<MoveEntity | undefined>;
|
||||
update(id: string, entity: Updateable<MoveHistory>): Promise<MoveEntity>;
|
||||
delete(id: string): Promise<MoveEntity>;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Updateable } from 'kysely';
|
||||
import { Partners } from 'src/db';
|
||||
import { PartnerEntity } from 'src/entities/partner.entity';
|
||||
|
||||
export interface PartnerIds {
|
||||
sharedById: string;
|
||||
sharedWithId: string;
|
||||
}
|
||||
|
||||
export enum PartnerDirection {
|
||||
SharedBy = 'shared-by',
|
||||
SharedWith = 'shared-with',
|
||||
}
|
||||
|
||||
export const IPartnerRepository = 'IPartnerRepository';
|
||||
|
||||
export interface IPartnerRepository {
|
||||
getAll(userId: string): Promise<PartnerEntity[]>;
|
||||
get(partner: PartnerIds): Promise<PartnerEntity | undefined>;
|
||||
create(partner: PartnerIds): Promise<PartnerEntity>;
|
||||
remove(partner: PartnerIds): Promise<void>;
|
||||
update(partner: PartnerIds, entity: Updateable<Partners>): Promise<PartnerEntity>;
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { Insertable, Selectable, Updateable } from 'kysely';
|
||||
import { AssetFaces, FaceSearch, Person } from 'src/db';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { SourceType } from 'src/enum';
|
||||
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
||||
import { FindOptionsRelations } from 'typeorm';
|
||||
|
||||
export const IPersonRepository = 'IPersonRepository';
|
||||
|
||||
export interface PersonSearchOptions {
|
||||
minimumFaceCount: number;
|
||||
withHidden: boolean;
|
||||
closestFaceAssetId?: string;
|
||||
}
|
||||
|
||||
export interface PersonNameSearchOptions {
|
||||
withHidden?: boolean;
|
||||
}
|
||||
|
||||
export interface PersonNameResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AssetFaceId {
|
||||
assetId: string;
|
||||
personId: string;
|
||||
}
|
||||
|
||||
export interface UpdateFacesData {
|
||||
oldPersonId?: string;
|
||||
faceIds?: string[];
|
||||
newPersonId: string;
|
||||
}
|
||||
|
||||
export interface PersonStatistics {
|
||||
assets: number;
|
||||
}
|
||||
|
||||
export interface PeopleStatistics {
|
||||
total: number;
|
||||
hidden: number;
|
||||
}
|
||||
|
||||
export interface DeleteFacesOptions {
|
||||
sourceType: SourceType;
|
||||
}
|
||||
|
||||
export type UnassignFacesOptions = DeleteFacesOptions;
|
||||
|
||||
export type SelectFaceOptions = (keyof Selectable<AssetFaces>)[];
|
||||
|
||||
export interface IPersonRepository {
|
||||
getAll(options?: Partial<PersonEntity>): AsyncIterableIterator<PersonEntity>;
|
||||
getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated<PersonEntity>;
|
||||
getAllWithoutFaces(): Promise<PersonEntity[]>;
|
||||
getById(personId: string): Promise<PersonEntity | null>;
|
||||
getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise<PersonEntity[]>;
|
||||
getDistinctNames(userId: string, options: PersonNameSearchOptions): Promise<PersonNameResponse[]>;
|
||||
|
||||
create(person: Insertable<Person>): Promise<PersonEntity>;
|
||||
createAll(people: Insertable<Person>[]): Promise<string[]>;
|
||||
delete(entities: PersonEntity[]): Promise<void>;
|
||||
deleteFaces(options: DeleteFacesOptions): Promise<void>;
|
||||
refreshFaces(
|
||||
facesToAdd: Insertable<AssetFaces>[],
|
||||
faceIdsToRemove: string[],
|
||||
embeddingsToAdd?: Insertable<FaceSearch>[],
|
||||
): Promise<void>;
|
||||
getAllFaces(options?: Partial<AssetFaceEntity>): AsyncIterableIterator<AssetFaceEntity>;
|
||||
getFaceById(id: string): Promise<AssetFaceEntity>;
|
||||
getFaceByIdWithAssets(
|
||||
id: string,
|
||||
relations?: FindOptionsRelations<AssetFaceEntity>,
|
||||
select?: SelectFaceOptions,
|
||||
): Promise<AssetFaceEntity | undefined>;
|
||||
getFaces(assetId: string): Promise<AssetFaceEntity[]>;
|
||||
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]>;
|
||||
getRandomFace(personId: string): Promise<AssetFaceEntity | undefined>;
|
||||
getStatistics(personId: string): Promise<PersonStatistics>;
|
||||
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
|
||||
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
|
||||
reassignFaces(data: UpdateFacesData): Promise<number>;
|
||||
unassignFaces(options: UnassignFacesOptions): Promise<void>;
|
||||
update(person: Updateable<Person> & { id: string }): Promise<PersonEntity>;
|
||||
updateAll(people: Insertable<Person>[]): Promise<void>;
|
||||
getLatestFaceDate(): Promise<string | undefined>;
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||
import { AssetStatus, AssetType } from 'src/enum';
|
||||
import { Paginated } from 'src/utils/pagination';
|
||||
|
||||
export const ISearchRepository = 'ISearchRepository';
|
||||
|
||||
export interface SearchResult<T> {
|
||||
/** total matches */
|
||||
total: number;
|
||||
/** collection size */
|
||||
count: number;
|
||||
/** current page */
|
||||
page: number;
|
||||
/** items for page */
|
||||
items: T[];
|
||||
/** score */
|
||||
distances: number[];
|
||||
facets: SearchFacet[];
|
||||
}
|
||||
|
||||
export interface SearchFacet {
|
||||
fieldName: string;
|
||||
counts: Array<{
|
||||
count: number;
|
||||
value: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type SearchExploreItemSet<T> = Array<{
|
||||
value: string;
|
||||
data: T;
|
||||
}>;
|
||||
|
||||
export interface SearchExploreItem<T> {
|
||||
fieldName: string;
|
||||
items: SearchExploreItemSet<T>;
|
||||
}
|
||||
|
||||
export interface SearchAssetIDOptions {
|
||||
checksum?: Buffer;
|
||||
deviceAssetId?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface SearchUserIdOptions {
|
||||
deviceId?: string;
|
||||
libraryId?: string | null;
|
||||
userIds?: string[];
|
||||
}
|
||||
|
||||
export type SearchIdOptions = SearchAssetIDOptions & SearchUserIdOptions;
|
||||
|
||||
export interface SearchStatusOptions {
|
||||
isArchived?: boolean;
|
||||
isEncoded?: boolean;
|
||||
isFavorite?: boolean;
|
||||
isMotion?: boolean;
|
||||
isOffline?: boolean;
|
||||
isVisible?: boolean;
|
||||
isNotInAlbum?: boolean;
|
||||
type?: AssetType;
|
||||
status?: AssetStatus;
|
||||
withArchived?: boolean;
|
||||
withDeleted?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchOneToOneRelationOptions {
|
||||
withExif?: boolean;
|
||||
withStacked?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchRelationOptions extends SearchOneToOneRelationOptions {
|
||||
withFaces?: boolean;
|
||||
withPeople?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchDateOptions {
|
||||
createdBefore?: Date;
|
||||
createdAfter?: Date;
|
||||
takenBefore?: Date;
|
||||
takenAfter?: Date;
|
||||
trashedBefore?: Date;
|
||||
trashedAfter?: Date;
|
||||
updatedBefore?: Date;
|
||||
updatedAfter?: Date;
|
||||
}
|
||||
|
||||
export interface SearchPathOptions {
|
||||
encodedVideoPath?: string;
|
||||
originalFileName?: string;
|
||||
originalPath?: string;
|
||||
previewPath?: string;
|
||||
thumbnailPath?: string;
|
||||
}
|
||||
|
||||
export interface SearchExifOptions {
|
||||
city?: string | null;
|
||||
country?: string | null;
|
||||
lensModel?: string | null;
|
||||
make?: string | null;
|
||||
model?: string | null;
|
||||
state?: string | null;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface SearchEmbeddingOptions {
|
||||
embedding: string;
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
export interface SearchPeopleOptions {
|
||||
personIds?: string[];
|
||||
}
|
||||
|
||||
export interface SearchTagOptions {
|
||||
tagIds?: string[];
|
||||
}
|
||||
|
||||
export interface SearchOrderOptions {
|
||||
orderDirection?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface SearchPaginationOptions {
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
type BaseAssetSearchOptions = SearchDateOptions &
|
||||
SearchIdOptions &
|
||||
SearchExifOptions &
|
||||
SearchOrderOptions &
|
||||
SearchPathOptions &
|
||||
SearchStatusOptions &
|
||||
SearchUserIdOptions &
|
||||
SearchPeopleOptions &
|
||||
SearchTagOptions;
|
||||
|
||||
export type AssetSearchOptions = BaseAssetSearchOptions & SearchRelationOptions;
|
||||
|
||||
export type AssetSearchOneToOneRelationOptions = BaseAssetSearchOptions & SearchOneToOneRelationOptions;
|
||||
|
||||
export type AssetSearchBuilderOptions = Omit<AssetSearchOptions, 'orderDirection'>;
|
||||
|
||||
export type SmartSearchOptions = SearchDateOptions &
|
||||
SearchEmbeddingOptions &
|
||||
SearchExifOptions &
|
||||
SearchOneToOneRelationOptions &
|
||||
SearchStatusOptions &
|
||||
SearchUserIdOptions &
|
||||
SearchPeopleOptions &
|
||||
SearchTagOptions;
|
||||
|
||||
export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
||||
hasPerson?: boolean;
|
||||
numResults: number;
|
||||
maxDistance: number;
|
||||
}
|
||||
|
||||
export interface AssetDuplicateSearch {
|
||||
assetId: string;
|
||||
embedding: string;
|
||||
maxDistance: number;
|
||||
type: AssetType;
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
export interface FaceSearchResult {
|
||||
distance: number;
|
||||
id: string;
|
||||
personId: string | null;
|
||||
}
|
||||
|
||||
export interface AssetDuplicateResult {
|
||||
assetId: string;
|
||||
duplicateId: string | null;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
export interface GetStatesOptions {
|
||||
country?: string;
|
||||
}
|
||||
|
||||
export interface GetCitiesOptions extends GetStatesOptions {
|
||||
state?: string;
|
||||
}
|
||||
|
||||
export interface GetCameraModelsOptions {
|
||||
make?: string;
|
||||
}
|
||||
|
||||
export interface GetCameraMakesOptions {
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface ISearchRepository {
|
||||
searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity>;
|
||||
searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated<AssetEntity>;
|
||||
searchDuplicates(options: AssetDuplicateSearch): Promise<AssetDuplicateResult[]>;
|
||||
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
|
||||
searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]>;
|
||||
upsert(assetId: string, embedding: string): Promise<void>;
|
||||
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
|
||||
getAssetsByCity(userIds: string[]): Promise<AssetEntity[]>;
|
||||
deleteAllSearchEmbeddings(): Promise<void>;
|
||||
getDimensionSize(): Promise<number>;
|
||||
setDimensionSize(dimSize: number): Promise<void>;
|
||||
getCountries(userIds: string[]): Promise<Array<string | null>>;
|
||||
getStates(userIds: string[], options: GetStatesOptions): Promise<Array<string | null>>;
|
||||
getCities(userIds: string[], options: GetCitiesOptions): Promise<Array<string | null>>;
|
||||
getCameraMakes(userIds: string[], options: GetCameraMakesOptions): Promise<Array<string | null>>;
|
||||
getCameraModels(userIds: string[], options: GetCameraModelsOptions): Promise<Array<string | null>>;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Insertable, Updateable } from 'kysely';
|
||||
import { SharedLinks } from 'src/db';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
|
||||
export const ISharedLinkRepository = 'ISharedLinkRepository';
|
||||
|
||||
export type SharedLinkSearchOptions = {
|
||||
userId: string;
|
||||
albumId?: string;
|
||||
};
|
||||
|
||||
export interface ISharedLinkRepository {
|
||||
getAll(options: SharedLinkSearchOptions): Promise<SharedLinkEntity[]>;
|
||||
get(userId: string, id: string): Promise<SharedLinkEntity | undefined>;
|
||||
getByKey(key: Buffer): Promise<SharedLinkEntity | undefined>;
|
||||
create(entity: Insertable<SharedLinks> & { assetIds?: string[] }): Promise<SharedLinkEntity>;
|
||||
update(entity: Updateable<SharedLinks> & { id: string; assetIds?: string[] }): Promise<SharedLinkEntity>;
|
||||
remove(entity: SharedLinkEntity): Promise<void>;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Updateable } from 'kysely';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
|
||||
export const IStackRepository = 'IStackRepository';
|
||||
|
||||
export interface StackSearch {
|
||||
ownerId: string;
|
||||
primaryAssetId?: string;
|
||||
}
|
||||
|
||||
export interface IStackRepository {
|
||||
search(query: StackSearch): Promise<StackEntity[]>;
|
||||
create(stack: { ownerId: string; assetIds: string[] }): Promise<StackEntity>;
|
||||
update(id: string, entity: Updateable<StackEntity>): Promise<StackEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
deleteAll(ids: string[]): Promise<void>;
|
||||
getById(id: string): Promise<StackEntity | undefined>;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { WatchOptions } from 'chokidar';
|
||||
import { Stats } from 'node:fs';
|
||||
import { FileReadOptions } from 'node:fs/promises';
|
||||
import { Readable, Writable } from 'node:stream';
|
||||
import { CrawlOptionsDto, WalkOptionsDto } from 'src/dtos/library.dto';
|
||||
|
||||
export interface ImmichReadStream {
|
||||
stream: Readable;
|
||||
type?: string;
|
||||
length?: number;
|
||||
}
|
||||
|
||||
export interface ImmichZipStream extends ImmichReadStream {
|
||||
addFile: (inputPath: string, filename: string) => void;
|
||||
finalize: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface DiskUsage {
|
||||
available: number;
|
||||
free: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export const IStorageRepository = 'IStorageRepository';
|
||||
|
||||
export interface WatchEvents {
|
||||
onReady(): void;
|
||||
onAdd(path: string): void;
|
||||
onChange(path: string): void;
|
||||
onUnlink(path: string): void;
|
||||
onError(error: Error): void;
|
||||
}
|
||||
|
||||
export interface IStorageRepository {
|
||||
createZipStream(): ImmichZipStream;
|
||||
createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream>;
|
||||
readFile(filepath: string, options?: FileReadOptions<Buffer>): Promise<Buffer>;
|
||||
createFile(filepath: string, buffer: Buffer): Promise<void>;
|
||||
createWriteStream(filepath: string): Writable;
|
||||
createOrOverwriteFile(filepath: string, buffer: Buffer): Promise<void>;
|
||||
overwriteFile(filepath: string, buffer: Buffer): Promise<void>;
|
||||
realpath(filepath: string): Promise<string>;
|
||||
unlink(filepath: string): Promise<void>;
|
||||
unlinkDir(folder: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
|
||||
removeEmptyDirs(folder: string, self?: boolean): Promise<void>;
|
||||
checkFileExists(filepath: string, mode?: number): Promise<boolean>;
|
||||
mkdirSync(filepath: string): void;
|
||||
checkDiskUsage(folder: string): Promise<DiskUsage>;
|
||||
readdir(folder: string): Promise<string[]>;
|
||||
stat(filepath: string): Promise<Stats>;
|
||||
crawl(options: CrawlOptionsDto): Promise<string[]>;
|
||||
walk(options: WalkOptionsDto): AsyncGenerator<string[]>;
|
||||
copyFile(source: string, target: string): Promise<void>;
|
||||
rename(source: string, target: string): Promise<void>;
|
||||
watch(paths: string[], options: WatchOptions, events: Partial<WatchEvents>): () => Promise<void>;
|
||||
utimes(filepath: string, atime: Date, mtime: Date): Promise<void>;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { IBulkAsset } from 'src/utils/asset.util';
|
||||
|
||||
export const ITagRepository = 'ITagRepository';
|
||||
|
||||
export type AssetTagItem = { assetId: string; tagId: string };
|
||||
|
||||
export interface ITagRepository extends IBulkAsset {
|
||||
getAll(userId: string): Promise<TagEntity[]>;
|
||||
getByValue(userId: string, value: string): Promise<TagEntity | null>;
|
||||
upsertValue(request: { userId: string; value: string; parent?: TagEntity }): Promise<TagEntity>;
|
||||
|
||||
create(tag: Partial<TagEntity>): Promise<TagEntity>;
|
||||
get(id: string): Promise<TagEntity | null>;
|
||||
update(tag: { id: string } & Partial<TagEntity>): Promise<TagEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
|
||||
upsertAssetTags({ assetId, tagIds }: { assetId: string; tagIds: string[] }): Promise<void>;
|
||||
upsertAssetIds(items: AssetTagItem[]): Promise<AssetTagItem[]>;
|
||||
deleteEmptyTags(): Promise<void>;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Insertable, Updateable } from 'kysely';
|
||||
import { Users } from 'src/db';
|
||||
import { UserMetadata } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export interface UserListFilter {
|
||||
withDeleted?: boolean;
|
||||
}
|
||||
|
||||
export interface UserStatsQueryResponse {
|
||||
userId: string;
|
||||
userName: string;
|
||||
photos: number;
|
||||
videos: number;
|
||||
usage: number;
|
||||
usagePhotos: number;
|
||||
usageVideos: number;
|
||||
quotaSizeInBytes: number | null;
|
||||
}
|
||||
|
||||
export interface UserFindOptions {
|
||||
withDeleted?: boolean;
|
||||
}
|
||||
|
||||
export const IUserRepository = 'IUserRepository';
|
||||
|
||||
export interface IUserRepository {
|
||||
get(id: string, options: UserFindOptions): Promise<UserEntity | undefined>;
|
||||
getAdmin(): Promise<UserEntity | undefined>;
|
||||
hasAdmin(): Promise<boolean>;
|
||||
getByEmail(email: string, withPassword?: boolean): Promise<UserEntity | undefined>;
|
||||
getByStorageLabel(storageLabel: string): Promise<UserEntity | undefined>;
|
||||
getByOAuthId(oauthId: string): Promise<UserEntity | undefined>;
|
||||
getDeletedUsers(): Promise<UserEntity[]>;
|
||||
getList(filter?: UserListFilter): Promise<UserEntity[]>;
|
||||
getUserStats(): Promise<UserStatsQueryResponse[]>;
|
||||
create(user: Insertable<Users>): Promise<UserEntity>;
|
||||
update(id: string, user: Updateable<Users>): Promise<UserEntity>;
|
||||
restore(id: string): Promise<UserEntity>;
|
||||
upsertMetadata<T extends keyof UserMetadata>(id: string, item: { key: T; value: UserMetadata[T] }): Promise<void>;
|
||||
deleteMetadata<T extends keyof UserMetadata>(id: string, key: T): Promise<void>;
|
||||
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
|
||||
updateUsage(id: string, delta: number): Promise<void>;
|
||||
syncUsage(id?: string): Promise<void>;
|
||||
}
|
||||
@@ -5,18 +5,19 @@ import { AssetMediaResponseDto, AssetMediaStatus } from 'src/dtos/asset-media-re
|
||||
import { ImmichHeader } from 'src/enum';
|
||||
import { AuthenticatedRequest } from 'src/middleware/auth.guard';
|
||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||
import { fromMaybeArray, getReqRes } from 'src/utils/request';
|
||||
import { fromMaybeArray } from 'src/utils/request';
|
||||
|
||||
@Injectable()
|
||||
export class AssetUploadInterceptor implements NestInterceptor {
|
||||
constructor(private service: AssetMediaService) {}
|
||||
|
||||
async intercept(context: ExecutionContext, next: CallHandler<any>) {
|
||||
const { type, req, res } = getReqRes<AuthenticatedRequest, Response<AssetMediaResponseDto>>(context);
|
||||
const req = context.switchToHttp().getRequest<AuthenticatedRequest>();
|
||||
const res = context.switchToHttp().getResponse<Response<AssetMediaResponseDto>>();
|
||||
|
||||
const checksum = fromMaybeArray(req.headers[ImmichHeader.CHECKSUM]);
|
||||
const response = await this.service.getUploadAssetIdByChecksum(req.user, checksum);
|
||||
if (response && type === 'http') {
|
||||
if (response) {
|
||||
res.status(200);
|
||||
return of({ status: AssetMediaStatus.DUPLICATE, id: response.id });
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ImmichQuery, MetadataKey, Permission } from 'src/enum';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||
import { getReqRes } from 'src/utils/request';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
||||
type AdminRoute = { admin?: true };
|
||||
@@ -36,8 +35,7 @@ export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator =
|
||||
};
|
||||
|
||||
export const Auth = createParamDecorator((data, context: ExecutionContext): AuthDto => {
|
||||
const { req } = getReqRes<AuthenticatedRequest>(context);
|
||||
return req.user;
|
||||
return context.switchToHttp().getRequest<AuthenticatedRequest>().user;
|
||||
});
|
||||
|
||||
export const FileResponse = () =>
|
||||
@@ -88,12 +86,12 @@ export class AuthGuard implements CanActivate {
|
||||
sharedLink: sharedLinkRoute,
|
||||
permission,
|
||||
} = { sharedLink: false, admin: false, ...options };
|
||||
const { req } = getReqRes<AuthenticatedRequest>(context);
|
||||
const request = context.switchToHttp().getRequest<AuthRequest>();
|
||||
|
||||
req.user = await this.authService.authenticate({
|
||||
headers: req.headers,
|
||||
queryParams: req.query as Record<string, string>,
|
||||
metadata: { adminRoute, sharedLinkRoute, permission, uri: req.path },
|
||||
request.user = await this.authService.authenticate({
|
||||
headers: request.headers,
|
||||
queryParams: request.query as Record<string, string>,
|
||||
metadata: { adminRoute, sharedLinkRoute, permission, uri: request.path },
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
@@ -10,14 +10,10 @@ import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import { RouteKey } from 'src/enum';
|
||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { AssetMediaService, UploadFile } from 'src/services/asset-media.service';
|
||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||
import { ImmichFile, UploadFile, UploadFiles } from 'src/types';
|
||||
import { asRequest, mapToUploadFile } from 'src/utils/asset.util';
|
||||
|
||||
export interface UploadFiles {
|
||||
assetData: ImmichFile[];
|
||||
sidecarData: ImmichFile[];
|
||||
}
|
||||
|
||||
export function getFile(files: UploadFiles, property: 'assetData' | 'sidecarData') {
|
||||
const file = files[property]?.[0];
|
||||
return file ? mapToUploadFile(file) : file;
|
||||
@@ -30,12 +26,6 @@ export function getFiles(files: UploadFiles) {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ImmichFile extends Express.Multer.File {
|
||||
/** sha1 hash of file */
|
||||
uuid: string;
|
||||
checksum: Buffer;
|
||||
}
|
||||
|
||||
type DiskStorageCallback = (error: Error | null, result: string) => void;
|
||||
|
||||
type ImmichMulterFile = Express.Multer.File & { uuid: string };
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
|
||||
import { GqlContextType } from '@nestjs/graphql';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { Response } from 'express';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { logGlobalError } from 'src/utils/logger';
|
||||
import { getReqRes } from 'src/utils/request';
|
||||
|
||||
type StructuredError = {
|
||||
status: number;
|
||||
body: {
|
||||
[key: string]: unknown;
|
||||
message?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@Catch()
|
||||
export class GlobalExceptionFilter implements ExceptionFilter<Error> {
|
||||
@@ -24,20 +14,15 @@ export class GlobalExceptionFilter implements ExceptionFilter<Error> {
|
||||
}
|
||||
|
||||
catch(error: Error, host: ArgumentsHost) {
|
||||
const { res } = getReqRes(host);
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const { status, body } = this.fromError(error);
|
||||
const message = { ...body, statusCode: status, correlationId: this.cls.getId() };
|
||||
|
||||
if (host.getType<GqlContextType>() === 'graphql') {
|
||||
throw new GraphQLError(body?.message || 'Error', { extensions: message });
|
||||
}
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.status(status).json(message);
|
||||
if (!response.headersSent) {
|
||||
response.status(status).json({ ...body, statusCode: status, correlationId: this.cls.getId() });
|
||||
}
|
||||
}
|
||||
|
||||
private fromError(error: Error): StructuredError {
|
||||
private fromError(error: Error) {
|
||||
logGlobalError(this.logger, error);
|
||||
|
||||
if (error instanceof HttpException) {
|
||||
@@ -49,7 +34,7 @@ export class GlobalExceptionFilter implements ExceptionFilter<Error> {
|
||||
body = { message: body };
|
||||
}
|
||||
|
||||
return { status, body } as StructuredError;
|
||||
return { status, body };
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { Observable, finalize } from 'rxjs';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { getReqRes } from 'src/utils/request';
|
||||
|
||||
const maxArrayLength = 100;
|
||||
const replacer = (key: string, value: unknown) => {
|
||||
@@ -23,7 +23,10 @@ export class LoggingInterceptor implements NestInterceptor {
|
||||
}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> {
|
||||
const { req, res } = getReqRes(context);
|
||||
const handler = context.switchToHttp();
|
||||
const req = handler.getRequest<Request>();
|
||||
const res = handler.getResponse<Response>();
|
||||
|
||||
const { method, ip, url } = req;
|
||||
|
||||
const start = performance.now();
|
||||
@@ -32,7 +35,9 @@ export class LoggingInterceptor implements NestInterceptor {
|
||||
finalize(() => {
|
||||
const finish = performance.now();
|
||||
const duration = (finish - start).toFixed(2);
|
||||
this.logger.debug(`${method} ${url} ${res?.statusCode || ''} ${duration}ms ${ip}`);
|
||||
const { statusCode } = res;
|
||||
|
||||
this.logger.debug(`${method} ${url} ${statusCode} ${duration}ms ${ip}`);
|
||||
if (req.body && Object.keys(req.body).length > 0) {
|
||||
this.logger.verbose(JSON.stringify(req.body, replacer));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||
import { DatabaseExtension } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||
import { DatabaseExtension } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||
import { DatabaseExtension } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
|
||||
18
server/src/migrations/1737845696644-NullableDates.ts
Normal file
18
server/src/migrations/1737845696644-NullableDates.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class NullableDates1737845696644 implements MigrationInterface {
|
||||
name = 'NullableDates1737845696644'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileCreatedAt" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "localDateTime" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileModifiedAt" DROP NOT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileModifiedAt" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "localDateTime" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileCreatedAt" SET NOT NULL`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
import { UserAvatarColor } from 'src/enum';
|
||||
|
||||
registerEnumType(UserAvatarColor, {
|
||||
name: 'UserAvatarColor',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class User {
|
||||
@Field()
|
||||
id!: string;
|
||||
|
||||
@Field()
|
||||
name!: string;
|
||||
|
||||
@Field()
|
||||
email!: string;
|
||||
|
||||
@Field(() => UserAvatarColor)
|
||||
avatarColor!: UserAvatarColor;
|
||||
|
||||
@Field()
|
||||
profileImagePath!: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
profileChangedAt!: Date;
|
||||
}
|
||||
@@ -43,3 +43,6 @@ where
|
||||
and "activity"."albumId" = $2
|
||||
and "activity"."isLiked" = $3
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
|
||||
@@ -3,29 +3,28 @@
|
||||
-- ApiKeyRepository.getKey
|
||||
select
|
||||
"api_keys"."id",
|
||||
"api_keys"."key",
|
||||
"api_keys"."userId",
|
||||
"api_keys"."permissions",
|
||||
to_json("user") as "user"
|
||||
from
|
||||
"api_keys"
|
||||
inner join lateral (
|
||||
(
|
||||
select
|
||||
"users".*,
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
array_agg("user_metadata") as "metadata"
|
||||
"users"."id",
|
||||
"users"."name",
|
||||
"users"."email",
|
||||
"users"."isAdmin",
|
||||
"users"."quotaUsageInBytes",
|
||||
"users"."quotaSizeInBytes"
|
||||
from
|
||||
"user_metadata"
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "user_metadata"."userId"
|
||||
) as "metadata"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "api_keys"."userId"
|
||||
and "users"."deletedAt" is null
|
||||
) as "user" on true
|
||||
"users"."id" = "api_keys"."userId"
|
||||
and "users"."deletedAt" is null
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"api_keys"
|
||||
where
|
||||
"api_keys"."key" = $1
|
||||
|
||||
|
||||
@@ -159,6 +159,9 @@ where
|
||||
"ownerId" = $1::uuid
|
||||
and "deviceId" = $2
|
||||
and "isVisible" = $3
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "deletedAt" is null
|
||||
|
||||
-- AssetRepository.getLivePhotoCount
|
||||
@@ -260,6 +263,9 @@ with
|
||||
where
|
||||
"assets"."deletedAt" is null
|
||||
and "assets"."isVisible" = $2
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
)
|
||||
select
|
||||
"timeBucket",
|
||||
|
||||
@@ -13,6 +13,9 @@ where
|
||||
and "assets"."isFavorite" = $4
|
||||
and "assets"."isArchived" = $5
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
order by
|
||||
"assets"."fileCreatedAt" desc
|
||||
limit
|
||||
@@ -34,6 +37,9 @@ offset
|
||||
and "assets"."isFavorite" = $4
|
||||
and "assets"."isArchived" = $5
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "assets"."id" < $6
|
||||
order by
|
||||
random()
|
||||
@@ -54,6 +60,9 @@ union all
|
||||
and "assets"."isFavorite" = $11
|
||||
and "assets"."isArchived" = $12
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "assets"."id" > $13
|
||||
order by
|
||||
random()
|
||||
@@ -77,6 +86,9 @@ where
|
||||
and "assets"."isFavorite" = $4
|
||||
and "assets"."isArchived" = $5
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
order by
|
||||
smart_search.embedding <=> $6
|
||||
limit
|
||||
|
||||
@@ -10,41 +10,29 @@ where
|
||||
|
||||
-- SessionRepository.getByToken
|
||||
select
|
||||
"sessions".*,
|
||||
to_json("user") as "user"
|
||||
from
|
||||
"sessions"
|
||||
inner join lateral (
|
||||
"sessions"."id",
|
||||
"sessions"."updatedAt",
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt",
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
array_agg("user_metadata") as "metadata"
|
||||
"users"."id",
|
||||
"users"."name",
|
||||
"users"."email",
|
||||
"users"."isAdmin",
|
||||
"users"."quotaUsageInBytes",
|
||||
"users"."quotaSizeInBytes"
|
||||
from
|
||||
"user_metadata"
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "user_metadata"."userId"
|
||||
) as "metadata"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "sessions"."userId"
|
||||
and "users"."deletedAt" is null
|
||||
) as "user" on true
|
||||
"users"."id" = "sessions"."userId"
|
||||
and "users"."deletedAt" is null
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"sessions"
|
||||
where
|
||||
"sessions"."token" = $1
|
||||
|
||||
|
||||
@@ -153,12 +153,19 @@ where
|
||||
"shared_links"."type" = $2
|
||||
or "album"."id" is not null
|
||||
)
|
||||
and "shared_links"."albumId" = $3
|
||||
order by
|
||||
"shared_links"."createdAt" desc
|
||||
|
||||
-- SharedLinkRepository.getByKey
|
||||
select
|
||||
"shared_links".*,
|
||||
"shared_links"."id",
|
||||
"shared_links"."userId",
|
||||
"shared_links"."expiresAt",
|
||||
"shared_links"."showExif",
|
||||
"shared_links"."allowUpload",
|
||||
"shared_links"."allowDownload",
|
||||
"shared_links"."password",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
@@ -166,20 +173,11 @@ select
|
||||
(
|
||||
select
|
||||
"users"."id",
|
||||
"users"."email",
|
||||
"users"."createdAt",
|
||||
"users"."profileImagePath",
|
||||
"users"."isAdmin",
|
||||
"users"."shouldChangePassword",
|
||||
"users"."deletedAt",
|
||||
"users"."oauthId",
|
||||
"users"."updatedAt",
|
||||
"users"."storageLabel",
|
||||
"users"."name",
|
||||
"users"."quotaSizeInBytes",
|
||||
"users"."email",
|
||||
"users"."isAdmin",
|
||||
"users"."quotaUsageInBytes",
|
||||
"users"."status",
|
||||
"users"."profileChangedAt"
|
||||
"users"."quotaSizeInBytes"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
|
||||
@@ -10,6 +10,9 @@ where
|
||||
and "isVisible" = $3
|
||||
and "isArchived" = $4
|
||||
and "deletedAt" is null
|
||||
and "fileModifiedAt" is not null
|
||||
and "fileModifiedAt" is not null
|
||||
and "localDateTime" is not null
|
||||
|
||||
-- ViewRepository.getAssetsByOriginalPath
|
||||
select
|
||||
@@ -23,6 +26,9 @@ where
|
||||
and "isVisible" = $2
|
||||
and "isArchived" = $3
|
||||
and "deletedAt" is null
|
||||
and "fileModifiedAt" is not null
|
||||
and "fileModifiedAt" is not null
|
||||
and "localDateTime" is not null
|
||||
and "originalPath" like $4
|
||||
and "originalPath" not like $5
|
||||
order by
|
||||
|
||||
@@ -65,6 +65,9 @@ export class ActivityRepository {
|
||||
.where('activity.albumId', '=', albumId)
|
||||
.where('activity.isLiked', '=', false)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return count as number;
|
||||
|
||||
@@ -6,7 +6,17 @@ import { Albums, DB } from 'src/db';
|
||||
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
|
||||
export interface AlbumAssetCount {
|
||||
albumId: string;
|
||||
assetCount: number;
|
||||
startDate: Date | null;
|
||||
endDate: Date | null;
|
||||
}
|
||||
|
||||
export interface AlbumInfoOptions {
|
||||
withAssets: boolean;
|
||||
}
|
||||
|
||||
const userColumns = [
|
||||
'id',
|
||||
@@ -71,7 +81,7 @@ const withAssets = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AlbumRepository implements IAlbumRepository {
|
||||
export class AlbumRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, { withAssets: true }] })
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { ApiKeys, DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
const columns = ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'] as const;
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
@@ -33,29 +33,15 @@ export class ApiKeyRepository {
|
||||
getKey(hashedToken: string) {
|
||||
return this.db
|
||||
.selectFrom('api_keys')
|
||||
.innerJoinLateral(
|
||||
(eb) =>
|
||||
.select((eb) => [
|
||||
...columns.authApiKey,
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('users')
|
||||
.selectAll('users')
|
||||
.select((eb) =>
|
||||
eb
|
||||
.selectFrom('user_metadata')
|
||||
.whereRef('users.id', '=', 'user_metadata.userId')
|
||||
.select((eb) => eb.fn('array_agg', [eb.table('user_metadata')]).as('metadata'))
|
||||
.as('metadata'),
|
||||
)
|
||||
.select(columns.authUser)
|
||||
.whereRef('users.id', '=', 'api_keys.userId')
|
||||
.where('users.deletedAt', 'is', null)
|
||||
.as('user'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select((eb) => [
|
||||
'api_keys.id',
|
||||
'api_keys.key',
|
||||
'api_keys.userId',
|
||||
'api_keys.permissions',
|
||||
eb.fn.toJson('user').as('user'),
|
||||
.where('users.deletedAt', 'is', null),
|
||||
).as('user'),
|
||||
])
|
||||
.where('api_keys.key', '=', hashedToken)
|
||||
.executeTakeFirst();
|
||||
@@ -65,7 +51,7 @@ export class ApiKeyRepository {
|
||||
getById(userId: string, id: string) {
|
||||
return this.db
|
||||
.selectFrom('api_keys')
|
||||
.select(columns)
|
||||
.select(columns.apiKey)
|
||||
.where('id', '=', asUuid(id))
|
||||
.where('userId', '=', userId)
|
||||
.executeTakeFirst();
|
||||
@@ -75,7 +61,7 @@ export class ApiKeyRepository {
|
||||
getByUserId(userId: string) {
|
||||
return this.db
|
||||
.selectFrom('api_keys')
|
||||
.select(columns)
|
||||
.select(columns.apiKey)
|
||||
.where('userId', '=', userId)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.execute();
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
|
||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import {
|
||||
AssetEntity,
|
||||
AssetEntityPlaceholder,
|
||||
hasPeople,
|
||||
searchAssetBuilder,
|
||||
truncatedDate,
|
||||
@@ -21,34 +22,138 @@ import {
|
||||
withTagId,
|
||||
withTags,
|
||||
} from 'src/entities/asset.entity';
|
||||
import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
|
||||
import {
|
||||
AssetDeltaSyncOptions,
|
||||
AssetExploreFieldOptions,
|
||||
AssetFullSyncOptions,
|
||||
AssetGetByChecksumOptions,
|
||||
AssetStats,
|
||||
AssetStatsOptions,
|
||||
AssetUpdateDuplicateOptions,
|
||||
DayOfYearAssets,
|
||||
DuplicateGroup,
|
||||
GetByIdsRelations,
|
||||
IAssetRepository,
|
||||
LivePhotoSearchOptions,
|
||||
MonthDay,
|
||||
TimeBucketItem,
|
||||
TimeBucketOptions,
|
||||
TimeBucketSize,
|
||||
WithProperty,
|
||||
WithoutProperty,
|
||||
} from 'src/interfaces/asset.interface';
|
||||
import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/interfaces/search.interface';
|
||||
import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
|
||||
import { MapMarker, MapMarkerSearchOptions } from 'src/repositories/map.repository';
|
||||
import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/repositories/search.repository';
|
||||
import { anyUuid, asUuid, mapUpsertColumns } from 'src/utils/database';
|
||||
import { Paginated, PaginationOptions, paginationHelper } from 'src/utils/pagination';
|
||||
|
||||
export type AssetStats = Record<AssetType, number>;
|
||||
|
||||
export interface AssetStatsOptions {
|
||||
isFavorite?: boolean;
|
||||
isArchived?: boolean;
|
||||
isTrashed?: boolean;
|
||||
}
|
||||
|
||||
export interface LivePhotoSearchOptions {
|
||||
ownerId: string;
|
||||
libraryId?: string | null;
|
||||
livePhotoCID: string;
|
||||
otherAssetId: string;
|
||||
type: AssetType;
|
||||
}
|
||||
|
||||
export enum WithoutProperty {
|
||||
THUMBNAIL = 'thumbnail',
|
||||
ENCODED_VIDEO = 'encoded-video',
|
||||
EXIF = 'exif',
|
||||
SMART_SEARCH = 'smart-search',
|
||||
DUPLICATE = 'duplicate',
|
||||
FACES = 'faces',
|
||||
SIDECAR = 'sidecar',
|
||||
}
|
||||
|
||||
export enum WithProperty {
|
||||
SIDECAR = 'sidecar',
|
||||
}
|
||||
|
||||
export enum TimeBucketSize {
|
||||
DAY = 'DAY',
|
||||
MONTH = 'MONTH',
|
||||
}
|
||||
|
||||
export interface AssetBuilderOptions {
|
||||
isArchived?: boolean;
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
isDuplicate?: boolean;
|
||||
albumId?: string;
|
||||
tagId?: string;
|
||||
personId?: string;
|
||||
userIds?: string[];
|
||||
withStacked?: boolean;
|
||||
exifInfo?: boolean;
|
||||
status?: AssetStatus;
|
||||
assetType?: AssetType;
|
||||
}
|
||||
|
||||
export interface TimeBucketOptions extends AssetBuilderOptions {
|
||||
size: TimeBucketSize;
|
||||
order?: AssetOrder;
|
||||
}
|
||||
|
||||
export interface TimeBucketItem {
|
||||
timeBucket: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface MonthDay {
|
||||
day: number;
|
||||
month: number;
|
||||
}
|
||||
|
||||
export interface AssetExploreFieldOptions {
|
||||
maxFields: number;
|
||||
minAssetsPerField: number;
|
||||
}
|
||||
|
||||
export interface AssetFullSyncOptions {
|
||||
ownerId: string;
|
||||
lastId?: string;
|
||||
updatedUntil: Date;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface AssetDeltaSyncOptions {
|
||||
userIds: string[];
|
||||
updatedAfter: Date;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface AssetUpdateDuplicateOptions {
|
||||
targetDuplicateId: string | null;
|
||||
assetIds: string[];
|
||||
duplicateIds: string[];
|
||||
}
|
||||
|
||||
export interface UpsertFileOptions {
|
||||
assetId: string;
|
||||
type: AssetFileType;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface AssetGetByChecksumOptions {
|
||||
ownerId: string;
|
||||
checksum: Buffer;
|
||||
libraryId?: string;
|
||||
}
|
||||
|
||||
export type AssetPathEntity = Pick<AssetEntity, 'id' | 'originalPath' | 'isOffline'>;
|
||||
|
||||
export interface GetByIdsRelations {
|
||||
exifInfo?: boolean;
|
||||
faces?: { person?: boolean };
|
||||
files?: boolean;
|
||||
library?: boolean;
|
||||
owner?: boolean;
|
||||
smartSearch?: boolean;
|
||||
stack?: { assets?: boolean };
|
||||
tags?: boolean;
|
||||
}
|
||||
|
||||
export interface DuplicateGroup {
|
||||
duplicateId: string;
|
||||
assets: AssetEntity[];
|
||||
}
|
||||
|
||||
export interface DayOfYearAssets {
|
||||
yearsAgo: number;
|
||||
assets: AssetEntity[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AssetRepository implements IAssetRepository {
|
||||
export class AssetRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
async upsertExif(exif: Insertable<Exif>): Promise<void> {
|
||||
@@ -79,8 +184,12 @@ export class AssetRepository implements IAssetRepository {
|
||||
.execute();
|
||||
}
|
||||
|
||||
create(asset: Insertable<Assets>): Promise<AssetEntity> {
|
||||
return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirst() as any as Promise<AssetEntity>;
|
||||
create(asset: Insertable<Assets>): Promise<AssetEntityPlaceholder> {
|
||||
return this.db
|
||||
.insertInto('assets')
|
||||
.values(asset)
|
||||
.returningAll()
|
||||
.executeTakeFirst() as any as Promise<AssetEntityPlaceholder>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
|
||||
@@ -291,6 +400,9 @@ export class AssetRepository implements IAssetRepository {
|
||||
.where('ownerId', '=', asUuid(ownerId))
|
||||
.where('deviceId', '=', deviceId)
|
||||
.where('isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.where('deletedAt', 'is', null)
|
||||
.execute();
|
||||
|
||||
@@ -458,7 +570,10 @@ export class AssetRepository implements IAssetRepository {
|
||||
.where('job_status.duplicatesDetectedAt', 'is', null)
|
||||
.where('job_status.previewAt', 'is not', null)
|
||||
.where((eb) => eb.exists(eb.selectFrom('smart_search').where('assetId', '=', eb.ref('assets.id'))))
|
||||
.where('assets.isVisible', '=', true),
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null),
|
||||
)
|
||||
.$if(property === WithoutProperty.ENCODED_VIDEO, (qb) =>
|
||||
qb
|
||||
@@ -552,6 +667,9 @@ export class AssetRepository implements IAssetRepository {
|
||||
.select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.VIDEO).as(AssetType.VIDEO))
|
||||
.select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.OTHER).as(AssetType.OTHER))
|
||||
.where('ownerId', '=', asUuid(ownerId))
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.where('isVisible', '=', true)
|
||||
.$if(isArchived !== undefined, (qb) => qb.where('isArchived', '=', isArchived!))
|
||||
.$if(isFavorite !== undefined, (qb) => qb.where('isFavorite', '=', isFavorite!))
|
||||
@@ -584,6 +702,9 @@ export class AssetRepository implements IAssetRepository {
|
||||
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
||||
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.$if(!!options.albumId, (qb) =>
|
||||
qb
|
||||
.innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId')
|
||||
|
||||
@@ -13,9 +13,16 @@ import { Notice } from 'postgres';
|
||||
import { citiesFile, excludePaths, IWorker } from 'src/constants';
|
||||
import { Telemetry } from 'src/decorators';
|
||||
import { EnvDto } from 'src/dtos/env.dto';
|
||||
import { ImmichEnvironment, ImmichHeader, ImmichTelemetry, ImmichWorker, LogLevel } from 'src/enum';
|
||||
import { DatabaseConnectionParams, DatabaseExtension, VectorExtension } from 'src/interfaces/database.interface';
|
||||
import { QueueName } from 'src/interfaces/job.interface';
|
||||
import {
|
||||
DatabaseExtension,
|
||||
ImmichEnvironment,
|
||||
ImmichHeader,
|
||||
ImmichTelemetry,
|
||||
ImmichWorker,
|
||||
LogLevel,
|
||||
QueueName,
|
||||
} from 'src/enum';
|
||||
import { DatabaseConnectionParams, VectorExtension } from 'src/types';
|
||||
import { setDifference } from 'src/utils/set';
|
||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
|
||||
|
||||
|
||||
@@ -2,11 +2,10 @@ import { Injectable } from '@nestjs/common';
|
||||
import { compareSync, hash } from 'bcrypt';
|
||||
import { createHash, createPublicKey, createVerify, randomBytes, randomUUID } from 'node:crypto';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
|
||||
@Injectable()
|
||||
export class CryptoRepository implements ICryptoRepository {
|
||||
randomUUID() {
|
||||
export class CryptoRepository {
|
||||
randomUUID(): string {
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,26 +4,18 @@ import AsyncLock from 'async-lock';
|
||||
import { Kysely, sql } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import semver from 'semver';
|
||||
import { POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants';
|
||||
import { EXTENSION_NAMES, POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants';
|
||||
import { DB } from 'src/db';
|
||||
import {
|
||||
DatabaseExtension,
|
||||
DatabaseLock,
|
||||
EXTENSION_NAMES,
|
||||
ExtensionVersion,
|
||||
IDatabaseRepository,
|
||||
VectorExtension,
|
||||
VectorIndex,
|
||||
VectorUpdateResult,
|
||||
} from 'src/interfaces/database.interface';
|
||||
import { DatabaseExtension, DatabaseLock, VectorIndex } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { ExtensionVersion, VectorExtension, VectorUpdateResult } from 'src/types';
|
||||
import { UPSERT_COLUMNS } from 'src/utils/database';
|
||||
import { isValidInteger } from 'src/validation';
|
||||
import { DataSource, EntityManager, EntityMetadata, QueryRunner } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseRepository implements IDatabaseRepository {
|
||||
export class DatabaseRepository {
|
||||
private vectorExtension: VectorExtension;
|
||||
private readonly asyncLock = new AsyncLock();
|
||||
|
||||
|
||||
@@ -10,21 +10,15 @@ import {
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import _ from 'lodash';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { EventConfig } from 'src/decorators';
|
||||
import { ImmichWorker, MetadataKey } from 'src/enum';
|
||||
import {
|
||||
ArgsOf,
|
||||
ClientEventMap,
|
||||
EmitEvent,
|
||||
EmitHandler,
|
||||
EventItem,
|
||||
IEventRepository,
|
||||
serverEvents,
|
||||
ServerEvents,
|
||||
} from 'src/interfaces/event.interface';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { ImmichWorker, MetadataKey, QueueName } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { JobItem } from 'src/types';
|
||||
import { handlePromiseError } from 'src/utils/misc';
|
||||
|
||||
type EmitHandlers = Partial<{ [T in EmitEvent]: Array<EventItem<T>> }>;
|
||||
@@ -37,14 +31,99 @@ type Item<T extends EmitEvent> = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
type EventMap = {
|
||||
// app events
|
||||
'app.bootstrap': [];
|
||||
'app.shutdown': [];
|
||||
|
||||
'config.init': [{ newConfig: SystemConfig }];
|
||||
// config events
|
||||
'config.update': [
|
||||
{
|
||||
newConfig: SystemConfig;
|
||||
oldConfig: SystemConfig;
|
||||
},
|
||||
];
|
||||
'config.validate': [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
|
||||
|
||||
// album events
|
||||
'album.update': [{ id: string; recipientIds: string[] }];
|
||||
'album.invite': [{ id: string; userId: string }];
|
||||
|
||||
// asset events
|
||||
'asset.tag': [{ assetId: string }];
|
||||
'asset.untag': [{ assetId: string }];
|
||||
'asset.hide': [{ assetId: string; userId: string }];
|
||||
'asset.show': [{ assetId: string; userId: string }];
|
||||
'asset.trash': [{ assetId: string; userId: string }];
|
||||
'asset.delete': [{ assetId: string; userId: string }];
|
||||
|
||||
// asset bulk events
|
||||
'assets.trash': [{ assetIds: string[]; userId: string }];
|
||||
'assets.delete': [{ assetIds: string[]; userId: string }];
|
||||
'assets.restore': [{ assetIds: string[]; userId: string }];
|
||||
|
||||
'job.start': [QueueName, JobItem];
|
||||
|
||||
// session events
|
||||
'session.delete': [{ sessionId: string }];
|
||||
|
||||
// stack events
|
||||
'stack.create': [{ stackId: string; userId: string }];
|
||||
'stack.update': [{ stackId: string; userId: string }];
|
||||
'stack.delete': [{ stackId: string; userId: string }];
|
||||
|
||||
// stack bulk events
|
||||
'stacks.delete': [{ stackIds: string[]; userId: string }];
|
||||
|
||||
// user events
|
||||
'user.signup': [{ notify: boolean; id: string; tempPassword?: string }];
|
||||
|
||||
// websocket events
|
||||
'websocket.connect': [{ userId: string }];
|
||||
};
|
||||
|
||||
export const serverEvents = ['config.update'] as const;
|
||||
export type ServerEvents = (typeof serverEvents)[number];
|
||||
|
||||
export type EmitEvent = keyof EventMap;
|
||||
export type EmitHandler<T extends EmitEvent> = (...args: ArgsOf<T>) => Promise<void> | void;
|
||||
export type ArgOf<T extends EmitEvent> = EventMap[T][0];
|
||||
export type ArgsOf<T extends EmitEvent> = EventMap[T];
|
||||
|
||||
export interface ClientEventMap {
|
||||
on_upload_success: [AssetResponseDto];
|
||||
on_user_delete: [string];
|
||||
on_asset_delete: [string];
|
||||
on_asset_trash: [string[]];
|
||||
on_asset_update: [AssetResponseDto];
|
||||
on_asset_hidden: [string];
|
||||
on_asset_restore: [string[]];
|
||||
on_asset_stack_update: string[];
|
||||
on_person_thumbnail: [string];
|
||||
on_server_version: [ServerVersionResponseDto];
|
||||
on_config_update: [];
|
||||
on_new_release: [ReleaseNotification];
|
||||
on_session_delete: [string];
|
||||
}
|
||||
|
||||
export type EventItem<T extends EmitEvent> = {
|
||||
event: T;
|
||||
handler: EmitHandler<T>;
|
||||
server: boolean;
|
||||
};
|
||||
|
||||
export type AuthFn = (client: Socket) => Promise<AuthDto>;
|
||||
|
||||
@WebSocketGateway({
|
||||
cors: true,
|
||||
path: '/api/socket.io',
|
||||
transports: ['websocket'],
|
||||
})
|
||||
@Injectable()
|
||||
export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, IEventRepository {
|
||||
export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
|
||||
private emitHandlers: EmitHandlers = {};
|
||||
private authFn?: AuthFn;
|
||||
|
||||
@WebSocketServer()
|
||||
private server?: Server;
|
||||
@@ -122,11 +201,7 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
|
||||
async handleConnection(client: Socket) {
|
||||
try {
|
||||
this.logger.log(`Websocket Connect: ${client.id}`);
|
||||
const auth = await this.moduleRef.get(AuthService).authenticate({
|
||||
headers: client.request.headers,
|
||||
queryParams: {},
|
||||
metadata: { adminRoute: false, sharedLinkRoute: false, uri: '/api/socket.io' },
|
||||
});
|
||||
const auth = await this.authenticate(client);
|
||||
await client.join(auth.user.id);
|
||||
if (auth.session) {
|
||||
await client.join(auth.session.id);
|
||||
@@ -182,4 +257,16 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
|
||||
this.logger.debug(`Server event: ${event} (send)`);
|
||||
this.server?.serverSideEmit(event, ...args);
|
||||
}
|
||||
|
||||
setAuthFn(fn: (client: Socket) => Promise<AuthDto>) {
|
||||
this.authFn = fn;
|
||||
}
|
||||
|
||||
private async authenticate(client: Socket) {
|
||||
if (!this.authFn) {
|
||||
throw new Error('Auth function not set');
|
||||
}
|
||||
|
||||
return this.authFn(client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository } from 'src/interfaces/job.interface';
|
||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
import { IStackRepository } from 'src/interfaces/stack.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
@@ -58,44 +41,41 @@ import { ViewRepository } from 'src/repositories/view-repository';
|
||||
export const repositories = [
|
||||
AccessRepository,
|
||||
ActivityRepository,
|
||||
AlbumRepository,
|
||||
AlbumUserRepository,
|
||||
AuditRepository,
|
||||
ApiKeyRepository,
|
||||
AssetRepository,
|
||||
ConfigRepository,
|
||||
CronRepository,
|
||||
CryptoRepository,
|
||||
DatabaseRepository,
|
||||
EventRepository,
|
||||
JobRepository,
|
||||
LibraryRepository,
|
||||
LoggingRepository,
|
||||
MachineLearningRepository,
|
||||
MapRepository,
|
||||
MediaRepository,
|
||||
MemoryRepository,
|
||||
MetadataRepository,
|
||||
MoveRepository,
|
||||
NotificationRepository,
|
||||
OAuthRepository,
|
||||
PartnerRepository,
|
||||
PersonRepository,
|
||||
ProcessRepository,
|
||||
SearchRepository,
|
||||
SessionRepository,
|
||||
ServerInfoRepository,
|
||||
SharedLinkRepository,
|
||||
StackRepository,
|
||||
StorageRepository,
|
||||
SystemMetadataRepository,
|
||||
TagRepository,
|
||||
TelemetryRepository,
|
||||
TrashRepository,
|
||||
UserRepository,
|
||||
ViewRepository,
|
||||
VersionHistoryRepository,
|
||||
];
|
||||
|
||||
export const providers = [
|
||||
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
||||
{ provide: IAssetRepository, useClass: AssetRepository },
|
||||
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
||||
{ provide: IEventRepository, useClass: EventRepository },
|
||||
{ provide: IJobRepository, useClass: JobRepository },
|
||||
{ provide: ILibraryRepository, useClass: LibraryRepository },
|
||||
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
|
||||
{ provide: IMoveRepository, useClass: MoveRepository },
|
||||
{ provide: IPartnerRepository, useClass: PartnerRepository },
|
||||
{ provide: IPersonRepository, useClass: PersonRepository },
|
||||
{ provide: ISearchRepository, useClass: SearchRepository },
|
||||
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
||||
{ provide: IStackRepository, useClass: StackRepository },
|
||||
{ provide: IStorageRepository, useClass: StorageRepository },
|
||||
{ provide: ITagRepository, useClass: TagRepository },
|
||||
{ provide: IUserRepository, useClass: UserRepository },
|
||||
];
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
import { getQueueToken } from '@nestjs/bullmq';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ModuleRef, Reflector } from '@nestjs/core';
|
||||
import { JobsOptions, Queue, Worker } from 'bullmq';
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { JobConfig } from 'src/decorators';
|
||||
import { MetadataKey } from 'src/enum';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
IEntityJob,
|
||||
IJobRepository,
|
||||
JobCounts,
|
||||
JobItem,
|
||||
JobName,
|
||||
JobOf,
|
||||
JobStatus,
|
||||
QueueCleanType,
|
||||
QueueName,
|
||||
QueueStatus,
|
||||
} from 'src/interfaces/job.interface';
|
||||
import { JobName, JobStatus, MetadataKey, QueueCleanType, QueueName } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { IEntityJob, JobCounts, JobItem, JobOf, QueueStatus } from 'src/types';
|
||||
import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/misc';
|
||||
|
||||
type JobMapItem = {
|
||||
@@ -31,14 +20,14 @@ type JobMapItem = {
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class JobRepository implements IJobRepository {
|
||||
export class JobRepository {
|
||||
private workers: Partial<Record<QueueName, Worker>> = {};
|
||||
private handlers: Partial<Record<JobName, JobMapItem>> = {};
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
private eventRepository: EventRepository,
|
||||
private logger: LoggingRepository,
|
||||
) {
|
||||
this.logger.setContext(JobRepository.name);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { AssetType } from 'src/enum';
|
||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
|
||||
const userColumns = [
|
||||
'users.id',
|
||||
@@ -34,7 +33,7 @@ const withOwner = (eb: ExpressionBuilder<DB, 'libraries'>) => {
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class LibraryRepository implements ILibraryRepository {
|
||||
export class LibraryRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
|
||||
@@ -1,21 +1,60 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { CLIPConfig } from 'src/dtos/model-config.dto';
|
||||
import {
|
||||
ClipTextualResponse,
|
||||
ClipVisualResponse,
|
||||
FaceDetectionOptions,
|
||||
FacialRecognitionResponse,
|
||||
IMachineLearningRepository,
|
||||
MachineLearningRequest,
|
||||
ModelPayload,
|
||||
ModelTask,
|
||||
ModelType,
|
||||
} from 'src/interfaces/machine-learning.interface';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
export interface BoundingBox {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
}
|
||||
|
||||
export enum ModelTask {
|
||||
FACIAL_RECOGNITION = 'facial-recognition',
|
||||
SEARCH = 'clip',
|
||||
}
|
||||
|
||||
export enum ModelType {
|
||||
DETECTION = 'detection',
|
||||
PIPELINE = 'pipeline',
|
||||
RECOGNITION = 'recognition',
|
||||
TEXTUAL = 'textual',
|
||||
VISUAL = 'visual',
|
||||
}
|
||||
|
||||
export type ModelPayload = { imagePath: string } | { text: string };
|
||||
|
||||
type ModelOptions = { modelName: string };
|
||||
|
||||
export type FaceDetectionOptions = ModelOptions & { minScore: number };
|
||||
|
||||
type VisualResponse = { imageHeight: number; imageWidth: number };
|
||||
export type ClipVisualRequest = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: ModelOptions } };
|
||||
export type ClipVisualResponse = { [ModelTask.SEARCH]: string } & VisualResponse;
|
||||
|
||||
export type ClipTextualRequest = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: ModelOptions } };
|
||||
export type ClipTextualResponse = { [ModelTask.SEARCH]: string };
|
||||
|
||||
export type FacialRecognitionRequest = {
|
||||
[ModelTask.FACIAL_RECOGNITION]: {
|
||||
[ModelType.DETECTION]: ModelOptions & { options: { minScore: number } };
|
||||
[ModelType.RECOGNITION]: ModelOptions;
|
||||
};
|
||||
};
|
||||
|
||||
export interface Face {
|
||||
boundingBox: BoundingBox;
|
||||
embedding: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export type FacialRecognitionResponse = { [ModelTask.FACIAL_RECOGNITION]: Face[] } & VisualResponse;
|
||||
export type DetectedFaces = { faces: Face[] } & VisualResponse;
|
||||
export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest;
|
||||
|
||||
@Injectable()
|
||||
export class MachineLearningRepository implements IMachineLearningRepository {
|
||||
export class MachineLearningRepository {
|
||||
constructor(private logger: LoggingRepository) {
|
||||
this.logger.setContext(MachineLearningRepository.name);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB, Memories } from 'src/db';
|
||||
import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { IBulkAsset } from 'src/utils/asset.util';
|
||||
import { IBulkAsset } from 'src/types';
|
||||
|
||||
@Injectable()
|
||||
export class MemoryRepository implements IBulkAsset {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BinaryField, DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
|
||||
import geotz from 'geo-tz';
|
||||
import { LogLevel } from 'src/enum';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
interface ExifDuration {
|
||||
@@ -101,6 +102,9 @@ export class MetadataRepository {
|
||||
}
|
||||
|
||||
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
|
||||
if (this.logger.isLevelEnabled(LogLevel.VERBOSE)) {
|
||||
this.logger.verbose(`Writing tags ${JSON.stringify(tags)} to ${path}`);
|
||||
}
|
||||
try {
|
||||
await this.exiftool.write(path, tags);
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,10 +5,11 @@ import { DB, MoveHistory } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { MoveEntity } from 'src/entities/move.entity';
|
||||
import { PathType } from 'src/enum';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
|
||||
export type MoveCreate = Pick<MoveEntity, 'oldPath' | 'newPath' | 'entityId' | 'pathType'> & Partial<MoveEntity>;
|
||||
|
||||
@Injectable()
|
||||
export class MoveRepository implements IMoveRepository {
|
||||
export class MoveRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
create(entity: Insertable<MoveHistory>): Promise<MoveEntity> {
|
||||
|
||||
@@ -7,12 +7,7 @@ import { AlbumUpdateEmail } from 'src/emails/album-update.email';
|
||||
import { TestEmail } from 'src/emails/test.email';
|
||||
import { WelcomeEmail } from 'src/emails/welcome.email';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
export type EmailImageAttachment = {
|
||||
filename: string;
|
||||
path: string;
|
||||
cid: string;
|
||||
};
|
||||
import { EmailImageAttachment } from 'src/types';
|
||||
|
||||
export type SendEmailOptions = {
|
||||
from: string;
|
||||
|
||||
@@ -5,7 +5,16 @@ import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB, Partners, Users } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { PartnerEntity } from 'src/entities/partner.entity';
|
||||
import { IPartnerRepository, PartnerIds } from 'src/interfaces/partner.interface';
|
||||
|
||||
export interface PartnerIds {
|
||||
sharedById: string;
|
||||
sharedWithId: string;
|
||||
}
|
||||
|
||||
export enum PartnerDirection {
|
||||
SharedBy = 'shared-by',
|
||||
SharedWith = 'shared-with',
|
||||
}
|
||||
|
||||
const columns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const;
|
||||
|
||||
@@ -28,7 +37,7 @@ const withSharedWith = (eb: ExpressionBuilder<DB, 'partners'>) => {
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PartnerRepository implements IPartnerRepository {
|
||||
export class PartnerRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Insertable, Kysely, sql } from 'kysely';
|
||||
import { ExpressionBuilder, Insertable, Kysely, Selectable, sql } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
|
||||
@@ -7,23 +7,53 @@ import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { SourceType } from 'src/enum';
|
||||
import {
|
||||
AssetFaceId,
|
||||
DeleteFacesOptions,
|
||||
IPersonRepository,
|
||||
PeopleStatistics,
|
||||
PersonNameResponse,
|
||||
PersonNameSearchOptions,
|
||||
PersonSearchOptions,
|
||||
PersonStatistics,
|
||||
SelectFaceOptions,
|
||||
UnassignFacesOptions,
|
||||
UpdateFacesData,
|
||||
} from 'src/interfaces/person.interface';
|
||||
import { mapUpsertColumns } from 'src/utils/database';
|
||||
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
||||
import { FindOptionsRelations } from 'typeorm';
|
||||
|
||||
export interface PersonSearchOptions {
|
||||
minimumFaceCount: number;
|
||||
withHidden: boolean;
|
||||
closestFaceAssetId?: string;
|
||||
}
|
||||
|
||||
export interface PersonNameSearchOptions {
|
||||
withHidden?: boolean;
|
||||
}
|
||||
|
||||
export interface PersonNameResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AssetFaceId {
|
||||
assetId: string;
|
||||
personId: string;
|
||||
}
|
||||
|
||||
export interface UpdateFacesData {
|
||||
oldPersonId?: string;
|
||||
faceIds?: string[];
|
||||
newPersonId: string;
|
||||
}
|
||||
|
||||
export interface PersonStatistics {
|
||||
assets: number;
|
||||
}
|
||||
|
||||
export interface PeopleStatistics {
|
||||
total: number;
|
||||
hidden: number;
|
||||
}
|
||||
|
||||
export interface DeleteFacesOptions {
|
||||
sourceType: SourceType;
|
||||
}
|
||||
|
||||
export type UnassignFacesOptions = DeleteFacesOptions;
|
||||
|
||||
export type SelectFaceOptions = (keyof Selectable<AssetFaces>)[];
|
||||
|
||||
const withPerson = (eb: ExpressionBuilder<DB, 'asset_faces'>) => {
|
||||
return jsonObjectFrom(
|
||||
eb.selectFrom('person').selectAll('person').whereRef('person.id', '=', 'asset_faces.personId'),
|
||||
@@ -43,7 +73,7 @@ const withFaceSearch = (eb: ExpressionBuilder<DB, 'asset_faces'>) => {
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PersonRepository implements IPersonRepository {
|
||||
export class PersonRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
|
||||
|
||||
@@ -6,26 +6,202 @@ import { DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetEntity, searchAssetBuilder } from 'src/entities/asset.entity';
|
||||
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||
import { AssetType } from 'src/enum';
|
||||
import {
|
||||
AssetDuplicateSearch,
|
||||
AssetSearchOptions,
|
||||
FaceEmbeddingSearch,
|
||||
GetCameraMakesOptions,
|
||||
GetCameraModelsOptions,
|
||||
GetCitiesOptions,
|
||||
GetStatesOptions,
|
||||
ISearchRepository,
|
||||
SearchPaginationOptions,
|
||||
SmartSearchOptions,
|
||||
} from 'src/interfaces/search.interface';
|
||||
import { AssetStatus, AssetType } from 'src/enum';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { anyUuid, asUuid } from 'src/utils/database';
|
||||
import { Paginated } from 'src/utils/pagination';
|
||||
import { isValidInteger } from 'src/validation';
|
||||
|
||||
export interface SearchResult<T> {
|
||||
/** total matches */
|
||||
total: number;
|
||||
/** collection size */
|
||||
count: number;
|
||||
/** current page */
|
||||
page: number;
|
||||
/** items for page */
|
||||
items: T[];
|
||||
/** score */
|
||||
distances: number[];
|
||||
facets: SearchFacet[];
|
||||
}
|
||||
|
||||
export interface SearchFacet {
|
||||
fieldName: string;
|
||||
counts: Array<{
|
||||
count: number;
|
||||
value: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export type SearchExploreItemSet<T> = Array<{
|
||||
value: string;
|
||||
data: T;
|
||||
}>;
|
||||
|
||||
export interface SearchExploreItem<T> {
|
||||
fieldName: string;
|
||||
items: SearchExploreItemSet<T>;
|
||||
}
|
||||
|
||||
export interface SearchAssetIDOptions {
|
||||
checksum?: Buffer;
|
||||
deviceAssetId?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface SearchUserIdOptions {
|
||||
deviceId?: string;
|
||||
libraryId?: string | null;
|
||||
userIds?: string[];
|
||||
}
|
||||
|
||||
export type SearchIdOptions = SearchAssetIDOptions & SearchUserIdOptions;
|
||||
|
||||
export interface SearchStatusOptions {
|
||||
isArchived?: boolean;
|
||||
isEncoded?: boolean;
|
||||
isFavorite?: boolean;
|
||||
isMotion?: boolean;
|
||||
isOffline?: boolean;
|
||||
isVisible?: boolean;
|
||||
isNotInAlbum?: boolean;
|
||||
type?: AssetType;
|
||||
status?: AssetStatus;
|
||||
withArchived?: boolean;
|
||||
withDeleted?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchOneToOneRelationOptions {
|
||||
withExif?: boolean;
|
||||
withStacked?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchRelationOptions extends SearchOneToOneRelationOptions {
|
||||
withFaces?: boolean;
|
||||
withPeople?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchDateOptions {
|
||||
createdBefore?: Date;
|
||||
createdAfter?: Date;
|
||||
takenBefore?: Date;
|
||||
takenAfter?: Date;
|
||||
trashedBefore?: Date;
|
||||
trashedAfter?: Date;
|
||||
updatedBefore?: Date;
|
||||
updatedAfter?: Date;
|
||||
}
|
||||
|
||||
export interface SearchPathOptions {
|
||||
encodedVideoPath?: string;
|
||||
originalFileName?: string;
|
||||
originalPath?: string;
|
||||
previewPath?: string;
|
||||
thumbnailPath?: string;
|
||||
}
|
||||
|
||||
export interface SearchExifOptions {
|
||||
city?: string | null;
|
||||
country?: string | null;
|
||||
lensModel?: string | null;
|
||||
make?: string | null;
|
||||
model?: string | null;
|
||||
state?: string | null;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface SearchEmbeddingOptions {
|
||||
embedding: string;
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
export interface SearchPeopleOptions {
|
||||
personIds?: string[];
|
||||
}
|
||||
|
||||
export interface SearchTagOptions {
|
||||
tagIds?: string[];
|
||||
}
|
||||
|
||||
export interface SearchOrderOptions {
|
||||
orderDirection?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface SearchPaginationOptions {
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
type BaseAssetSearchOptions = SearchDateOptions &
|
||||
SearchIdOptions &
|
||||
SearchExifOptions &
|
||||
SearchOrderOptions &
|
||||
SearchPathOptions &
|
||||
SearchStatusOptions &
|
||||
SearchUserIdOptions &
|
||||
SearchPeopleOptions &
|
||||
SearchTagOptions;
|
||||
|
||||
export type AssetSearchOptions = BaseAssetSearchOptions & SearchRelationOptions;
|
||||
|
||||
export type AssetSearchOneToOneRelationOptions = BaseAssetSearchOptions & SearchOneToOneRelationOptions;
|
||||
|
||||
export type AssetSearchBuilderOptions = Omit<AssetSearchOptions, 'orderDirection'>;
|
||||
|
||||
export type SmartSearchOptions = SearchDateOptions &
|
||||
SearchEmbeddingOptions &
|
||||
SearchExifOptions &
|
||||
SearchOneToOneRelationOptions &
|
||||
SearchStatusOptions &
|
||||
SearchUserIdOptions &
|
||||
SearchPeopleOptions &
|
||||
SearchTagOptions;
|
||||
|
||||
export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
||||
hasPerson?: boolean;
|
||||
numResults: number;
|
||||
maxDistance: number;
|
||||
}
|
||||
|
||||
export interface AssetDuplicateSearch {
|
||||
assetId: string;
|
||||
embedding: string;
|
||||
maxDistance: number;
|
||||
type: AssetType;
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
export interface FaceSearchResult {
|
||||
distance: number;
|
||||
id: string;
|
||||
personId: string | null;
|
||||
}
|
||||
|
||||
export interface AssetDuplicateResult {
|
||||
assetId: string;
|
||||
duplicateId: string | null;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
export interface GetStatesOptions {
|
||||
country?: string;
|
||||
}
|
||||
|
||||
export interface GetCitiesOptions extends GetStatesOptions {
|
||||
state?: string;
|
||||
}
|
||||
|
||||
export interface GetCameraModelsOptions {
|
||||
make?: string;
|
||||
}
|
||||
|
||||
export interface GetCameraMakesOptions {
|
||||
model?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SearchRepository implements ISearchRepository {
|
||||
export class SearchRepository {
|
||||
constructor(
|
||||
private logger: LoggingRepository,
|
||||
@InjectKysely() private db: Kysely<DB>,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { DB, Sessions } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { withUser } from 'src/entities/session.entity';
|
||||
@@ -25,9 +27,16 @@ export class SessionRepository {
|
||||
getByToken(token: string) {
|
||||
return this.db
|
||||
.selectFrom('sessions')
|
||||
.innerJoinLateral(withUser, (join) => join.onTrue())
|
||||
.selectAll('sessions')
|
||||
.select((eb) => eb.fn.toJson('user').as('user'))
|
||||
.select((eb) => [
|
||||
...columns.authSession,
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('users')
|
||||
.select(columns.authUser)
|
||||
.whereRef('users.id', '=', 'sessions.userId')
|
||||
.where('users.deletedAt', 'is', null),
|
||||
).as('user'),
|
||||
])
|
||||
.where('sessions.token', '=', token)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@@ -3,14 +3,19 @@ import { Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import _ from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { DB, SharedLinks } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
import { ISharedLinkRepository, SharedLinkSearchOptions } from 'src/interfaces/shared-link.interface';
|
||||
|
||||
export type SharedLinkSearchOptions = {
|
||||
userId: string;
|
||||
albumId?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class SharedLinkRepository implements ISharedLinkRepository {
|
||||
export class SharedLinkRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
@@ -92,7 +97,7 @@ export class SharedLinkRepository implements ISharedLinkRepository {
|
||||
.executeTakeFirst() as Promise<SharedLinkEntity | undefined>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
@GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }] })
|
||||
getAll({ userId, albumId }: SharedLinkSearchOptions): Promise<SharedLinkEntity[]> {
|
||||
return this.db
|
||||
.selectFrom('shared_links')
|
||||
@@ -156,39 +161,20 @@ export class SharedLinkRepository implements ISharedLinkRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.BUFFER] })
|
||||
async getByKey(key: Buffer): Promise<SharedLinkEntity | undefined> {
|
||||
async getByKey(key: Buffer) {
|
||||
return this.db
|
||||
.selectFrom('shared_links')
|
||||
.selectAll('shared_links')
|
||||
.where('shared_links.key', '=', key)
|
||||
.leftJoin('albums', 'albums.id', 'shared_links.albumId')
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.select((eb) =>
|
||||
.select((eb) => [
|
||||
...columns.authSharedLink,
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('users')
|
||||
.select([
|
||||
'users.id',
|
||||
'users.email',
|
||||
'users.createdAt',
|
||||
'users.profileImagePath',
|
||||
'users.isAdmin',
|
||||
'users.shouldChangePassword',
|
||||
'users.deletedAt',
|
||||
'users.oauthId',
|
||||
'users.updatedAt',
|
||||
'users.storageLabel',
|
||||
'users.name',
|
||||
'users.quotaSizeInBytes',
|
||||
'users.quotaUsageInBytes',
|
||||
'users.status',
|
||||
'users.profileChangedAt',
|
||||
])
|
||||
.whereRef('users.id', '=', 'shared_links.userId'),
|
||||
eb.selectFrom('users').select(columns.authUser).whereRef('users.id', '=', 'shared_links.userId'),
|
||||
).as('user'),
|
||||
)
|
||||
])
|
||||
.where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('albums.id', 'is not', null)]))
|
||||
.executeTakeFirst() as Promise<SharedLinkEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async create(entity: Insertable<SharedLinks> & { assetIds?: string[] }): Promise<SharedLinkEntity> {
|
||||
|
||||
@@ -5,9 +5,13 @@ import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
import { IStackRepository, StackSearch } from 'src/interfaces/stack.interface';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
export interface StackSearch {
|
||||
ownerId: string;
|
||||
primaryAssetId?: string;
|
||||
}
|
||||
|
||||
const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false) => {
|
||||
return jsonArrayFrom(
|
||||
eb
|
||||
@@ -35,7 +39,7 @@ const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false)
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class StackRepository implements IStackRepository {
|
||||
export class StackRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [{ ownerId: DummyValue.UUID }] })
|
||||
|
||||
@@ -5,20 +5,38 @@ import { escapePath, glob, globStream } from 'fast-glob';
|
||||
import { constants, createReadStream, createWriteStream, existsSync, mkdirSync } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { Writable } from 'node:stream';
|
||||
import { Readable, Writable } from 'node:stream';
|
||||
import { CrawlOptionsDto, WalkOptionsDto } from 'src/dtos/library.dto';
|
||||
import {
|
||||
DiskUsage,
|
||||
IStorageRepository,
|
||||
ImmichReadStream,
|
||||
ImmichZipStream,
|
||||
WatchEvents,
|
||||
} from 'src/interfaces/storage.interface';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
|
||||
export interface WatchEvents {
|
||||
onReady(): void;
|
||||
onAdd(path: string): void;
|
||||
onChange(path: string): void;
|
||||
onUnlink(path: string): void;
|
||||
onError(error: Error): void;
|
||||
}
|
||||
|
||||
export interface ImmichReadStream {
|
||||
stream: Readable;
|
||||
type?: string;
|
||||
length?: number;
|
||||
}
|
||||
|
||||
export interface ImmichZipStream extends ImmichReadStream {
|
||||
addFile: (inputPath: string, filename: string) => void;
|
||||
finalize: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface DiskUsage {
|
||||
available: number;
|
||||
free: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class StorageRepository implements IStorageRepository {
|
||||
export class StorageRepository {
|
||||
constructor(private logger: LoggingRepository) {
|
||||
this.logger.setContext(StorageRepository.name);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { DataSource, In, Repository } from 'typeorm';
|
||||
|
||||
export type AssetTagItem = { assetId: string; tagId: string };
|
||||
|
||||
@Injectable()
|
||||
export class TagRepository implements ITagRepository {
|
||||
export class TagRepository {
|
||||
constructor(
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
@InjectRepository(TagEntity) private repository: Repository<TagEntity>,
|
||||
|
||||
@@ -3,15 +3,9 @@ import { Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB, UserMetadata as DbUserMetadata, Users } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { UserMetadata } from 'src/entities/user-metadata.entity';
|
||||
import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity, withMetadata } from 'src/entities/user.entity';
|
||||
import { UserStatus } from 'src/enum';
|
||||
import {
|
||||
IUserRepository,
|
||||
UserFindOptions,
|
||||
UserListFilter,
|
||||
UserStatsQueryResponse,
|
||||
} from 'src/interfaces/user.interface';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
const columns = [
|
||||
@@ -34,8 +28,27 @@ const columns = [
|
||||
|
||||
type Upsert = Insertable<DbUserMetadata>;
|
||||
|
||||
export interface UserListFilter {
|
||||
withDeleted?: boolean;
|
||||
}
|
||||
|
||||
export interface UserStatsQueryResponse {
|
||||
userId: string;
|
||||
userName: string;
|
||||
photos: number;
|
||||
videos: number;
|
||||
usage: number;
|
||||
usagePhotos: number;
|
||||
usageVideos: number;
|
||||
quotaSizeInBytes: number | null;
|
||||
}
|
||||
|
||||
export interface UserFindOptions {
|
||||
withDeleted?: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository implements IUserRepository {
|
||||
export class UserRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BOOLEAN] })
|
||||
@@ -51,6 +64,14 @@ export class UserRepository implements IUserRepository {
|
||||
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
||||
}
|
||||
|
||||
getMetadata(userId: string) {
|
||||
return this.db
|
||||
.selectFrom('user_metadata')
|
||||
.select(['key', 'value'])
|
||||
.where('user_metadata.userId', '=', userId)
|
||||
.execute() as Promise<UserMetadataItem[]>;
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
getAdmin(): Promise<UserEntity | undefined> {
|
||||
return this.db
|
||||
@@ -250,7 +271,7 @@ export class UserRepository implements IUserRepository {
|
||||
eb
|
||||
.selectFrom('assets')
|
||||
.leftJoin('exif', 'exif.assetId', 'assets.id')
|
||||
.select((eb) => eb.fn.coalesce(eb.fn.sum('exif.fileSizeInByte'), eb.lit(0)).as('usage'))
|
||||
.select((eb) => eb.fn.coalesce(eb.fn.sum<number>('exif.fileSizeInByte'), eb.lit(0)).as('usage'))
|
||||
.where('assets.libraryId', 'is', null)
|
||||
.where('assets.ownerId', '=', eb.ref('users.id')),
|
||||
updatedAt: new Date(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user