Compare commits
49 Commits
vet
...
mobile-get
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73ea1e8fb5 | ||
|
|
ae6653392e | ||
|
|
d7a782da34 | ||
|
|
08b5952c87 | ||
|
|
584e5894bf | ||
|
|
52d4b2fe57 | ||
|
|
92f0973a46 | ||
|
|
75c83cb704 | ||
|
|
0b22d3348e | ||
|
|
abde0fbe60 | ||
|
|
eaa0e07329 | ||
|
|
9fd2c5220d | ||
|
|
7fcab4b251 | ||
|
|
e3995fb5f4 | ||
|
|
6d3f3d8616 | ||
|
|
4412680679 | ||
|
|
7df2c9c905 | ||
|
|
7a1e8ce6d8 | ||
|
|
8aea07b750 | ||
|
|
94dba29298 | ||
|
|
9e49783e49 | ||
|
|
43e3075f93 | ||
|
|
d03647904b | ||
|
|
206545356d | ||
|
|
3e372500b0 | ||
|
|
8943ec23ba | ||
|
|
04b03f2924 | ||
|
|
cf2c0260a6 | ||
|
|
ae8af84101 | ||
|
|
4794eeca88 | ||
|
|
ac65d46ec6 | ||
|
|
e5ca79dd44 | ||
|
|
49be6d7fd8 | ||
|
|
15c6506aee | ||
|
|
2c31a11e41 | ||
|
|
b6c5a03533 | ||
|
|
75bc32b47b | ||
|
|
fdbe6d649f | ||
|
|
2b131fe935 | ||
|
|
6ae24fbbd4 | ||
|
|
7f116d8e98 | ||
|
|
bd0840c411 | ||
|
|
a5123dec1a | ||
|
|
ffd18c5459 | ||
|
|
8242ff9bab | ||
|
|
8203b6c450 | ||
|
|
b352cf3336 | ||
|
|
96ed9a8c4a | ||
|
|
e7a5b96ed0 |
@@ -1,4 +1,4 @@
|
||||
ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:2ef23730ec68d8511ec8e6e0b82550ca728b256805d81f60ed890f3bfb21cfb9
|
||||
ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:b0b88ef6a5abf21194343d2c5b2829dddd9be1142f65f6a5e4390a51d5a70dd8
|
||||
FROM ${BASEIMAGE}
|
||||
|
||||
# Flutter SDK
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3
|
||||
uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3
|
||||
uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 # v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -72,6 +72,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3
|
||||
uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -518,7 +518,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Run existing migrations
|
||||
run: npm run typeorm:migrations:run
|
||||
run: npm run migrations:run
|
||||
|
||||
- name: Test npm run schema:reset command works
|
||||
run: npm run typeorm:schema:reset
|
||||
@@ -532,7 +532,7 @@ jobs:
|
||||
id: verify-changed-files
|
||||
with:
|
||||
files: |
|
||||
server/src/migrations/
|
||||
server/src
|
||||
- name: Verify migration files have not changed
|
||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||
run: |
|
||||
|
||||
48
cli/package-lock.json
generated
48
cli/package-lock.json
generated
@@ -27,7 +27,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -61,7 +61,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -1012,9 +1012,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgr/core": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz",
|
||||
"integrity": "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==",
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.1.tgz",
|
||||
"integrity": "sha512-VzgHzGblFmUeBmmrk55zPyrQIArQN4vujc9shWytaPdB3P7qhi0cpaiKIr7tlCmFv2lYUwnLospIqjL9ZSAhhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1362,13 +1362,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.15.tgz",
|
||||
"integrity": "sha512-imAbQEEbVni6i6h6Bd5xkCRwLqFc8hihCsi2GbtDoAtUcAFQ6Zs4pFXTZUUbroTkXdImczWM9AI8eZUuybXE3w==",
|
||||
"version": "22.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
|
||||
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/normalize-package-data": {
|
||||
@@ -2297,14 +2297,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.2.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz",
|
||||
"integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==",
|
||||
"version": "5.2.6",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz",
|
||||
"integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.0",
|
||||
"synckit": "^0.10.2"
|
||||
"synckit": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
@@ -3884,20 +3884,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz",
|
||||
"integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==",
|
||||
"version": "0.11.3",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.3.tgz",
|
||||
"integrity": "sha512-szhWDqNNI9etJUvbZ1/cx1StnZx8yMmFxme48SwR4dty4ioSY50KEZlpv0qAfgc1fpRzuh9hBXEzoCpJ779dLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.2.0",
|
||||
"@pkgr/core": "^0.2.1",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
@@ -4073,9 +4073,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -4144,9 +4144,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
|
||||
"integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
|
||||
"version": "6.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
|
||||
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
|
||||
@@ -102,7 +102,7 @@ services:
|
||||
command: [ './run.sh', '-disable-reporting' ]
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:11.5.2-ubuntu@sha256:8b5858c447e06fd7a89006b562ba7bba7c4d5813600c7982374c41852adefaeb
|
||||
image: grafana/grafana:11.6.0-ubuntu@sha256:fd8fa48213c624e1a95122f1d93abbf1cf1cbe85fc73212c1e599dbd76c63ff8
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
@@ -70,3 +70,6 @@ If you get an error `can't set healthcheck.start_interval as feature require Doc
|
||||
## Next Steps
|
||||
|
||||
Read the [Post Installation](/docs/install/post-install.mdx) steps and [upgrade instructions](/docs/install/upgrading.md).
|
||||
|
||||
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||
|
||||
@@ -24,9 +24,6 @@ To clean up disk space, the old version's obsolete container images can be delet
|
||||
docker image prune
|
||||
```
|
||||
|
||||
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||
[watchtower]: https://containrrr.dev/watchtower/
|
||||
[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Achangelog%3Abreaking-change+sort%3Adate_created
|
||||
[container-auth]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry
|
||||
[releases]: https://github.com/immich-app/immich/releases
|
||||
|
||||
50
e2e/package-lock.json
generated
50
e2e/package-lock.json
generated
@@ -15,7 +15,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -66,7 +66,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -100,7 +100,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -1088,9 +1088,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgr/core": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz",
|
||||
"integrity": "sha512-vsJDAkYR6qCPu+ioGScGiMYR7LvZYIXh/dlQeviqoTWNCVfKTLYD/LkNWH4Mxsv2a5vpIRc77FN5DnmK1eBggQ==",
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.1.tgz",
|
||||
"integrity": "sha512-VzgHzGblFmUeBmmrk55zPyrQIArQN4vujc9shWytaPdB3P7qhi0cpaiKIr7tlCmFv2lYUwnLospIqjL9ZSAhhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1566,9 +1566,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.0.tgz",
|
||||
"integrity": "sha512-RtEj20xRyG7cRp142MkQpV3GRF8Wo2MtDkKLz65MQs7rM1Lh8bz+HtfPXCCJEYpnDFu6VwAq/Iv2Ikyp9Jw/hw==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz",
|
||||
"integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1585,13 +1585,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.15.tgz",
|
||||
"integrity": "sha512-imAbQEEbVni6i6h6Bd5xkCRwLqFc8hihCsi2GbtDoAtUcAFQ6Zs4pFXTZUUbroTkXdImczWM9AI8eZUuybXE3w==",
|
||||
"version": "22.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
|
||||
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/normalize-package-data": {
|
||||
@@ -3094,14 +3094,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.2.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz",
|
||||
"integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==",
|
||||
"version": "5.2.6",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz",
|
||||
"integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.0",
|
||||
"synckit": "^0.10.2"
|
||||
"synckit": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
@@ -6039,20 +6039,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/synckit": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz",
|
||||
"integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==",
|
||||
"version": "0.11.3",
|
||||
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.3.tgz",
|
||||
"integrity": "sha512-szhWDqNNI9etJUvbZ1/cx1StnZx8yMmFxme48SwR4dty4ioSY50KEZlpv0qAfgc1fpRzuh9hBXEzoCpJ779dLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pkgr/core": "^0.2.0",
|
||||
"@pkgr/core": "^0.2.1",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unts"
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
@@ -6316,9 +6316,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
|
||||
@@ -1141,7 +1141,7 @@ describe('/asset', () => {
|
||||
fNumber: 8,
|
||||
focalLength: 97,
|
||||
iso: 100,
|
||||
lensModel: 'E PZ 18-105mm F4 G OSS',
|
||||
lensModel: 'Sony E PZ 18-105mm F4 G OSS',
|
||||
fileSizeInByte: 25_001_984,
|
||||
dateTimeOriginal: '2016-09-27T10:51:44+00:00',
|
||||
orientation: '1',
|
||||
@@ -1163,7 +1163,7 @@ describe('/asset', () => {
|
||||
fNumber: 22,
|
||||
focalLength: 25,
|
||||
iso: 100,
|
||||
lensModel: 'E 25mm F2',
|
||||
lensModel: 'Zeiss Batis 25mm F2',
|
||||
fileSizeInByte: 49_512_448,
|
||||
dateTimeOriginal: '2016-01-08T14:08:01+00:00',
|
||||
orientation: '1',
|
||||
@@ -1234,7 +1234,7 @@ describe('/asset', () => {
|
||||
focalLength: 18.3,
|
||||
iso: 100,
|
||||
latitude: 36.613_24,
|
||||
lensModel: 'GR LENS 18.3mm F2.8',
|
||||
lensModel: '18.3mm F2.8',
|
||||
longitude: -121.897_85,
|
||||
make: 'RICOH IMAGING COMPANY, LTD.',
|
||||
model: 'RICOH GR III',
|
||||
|
||||
@@ -246,15 +246,7 @@ describe('/shared-links', () => {
|
||||
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithMetadata.key });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.assets).toHaveLength(1);
|
||||
expect(body.assets[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
originalFileName: 'example.png',
|
||||
localDateTime: expect.any(String),
|
||||
fileCreatedAt: expect.any(String),
|
||||
exifInfo: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
expect(body.assets).toHaveLength(0);
|
||||
expect(body.album).toBeDefined();
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ test.describe('Shared Links', () => {
|
||||
await page.waitForSelector('[data-group] svg');
|
||||
await page.getByRole('checkbox').click();
|
||||
await page.getByRole('button', { name: 'Download' }).click();
|
||||
await page.getByText('DOWNLOADING', { exact: true }).waitFor();
|
||||
await page.waitForEvent('download');
|
||||
});
|
||||
|
||||
test('download all from shared link', async ({ page }) => {
|
||||
@@ -56,6 +56,7 @@ test.describe('Shared Links', () => {
|
||||
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
||||
await page.getByRole('button', { name: 'Download' }).click();
|
||||
await page.getByText('DOWNLOADING', { exact: true }).waitFor();
|
||||
await page.waitForEvent('download');
|
||||
});
|
||||
|
||||
test('enter password for a shared link', async ({ page }) => {
|
||||
|
||||
@@ -1371,6 +1371,7 @@
|
||||
"view_next_asset": "View next asset",
|
||||
"view_previous_asset": "View previous asset",
|
||||
"view_stack": "View Stack",
|
||||
"view_qr_code": "View QR code",
|
||||
"visibility_changed": "Visibility changed for {count, plural, one {# person} other {# people}}",
|
||||
"waiting": "Waiting",
|
||||
"warning": "Warning",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:ebfa8696e47a68cffebb31e370a93ce57c01bc753f246ceaaef72801d1661351 AS builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:0a9d314ae6e976351bd37b702bf6b0a89bb58e6304e5df35b960059b12531419 AS builder-cpu
|
||||
|
||||
FROM builder-cpu AS builder-openvino
|
||||
|
||||
@@ -54,7 +54,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends g++
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest@sha256:fb91e82e8643382d5bce074ba0d167677d678faff4bd518dac670476d19b159c /uv /uvx /bin/
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest@sha256:0b6dc79013b689f3bc0cbf12807cb1c901beaafe80f2ee10a1d76aa3842afb92 /uv /uvx /bin/
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
@@ -63,11 +63,11 @@ RUN if [ "$DEVICE" = "rocm" ]; then \
|
||||
uv pip install /opt/onnxruntime_rocm-*.whl; \
|
||||
fi
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:7029b00486ac40bed03e36775b864d3f3d39dcbdf19cd45e6a52d541e6c178f0 AS prod-cpu
|
||||
FROM python:3.11-slim-bookworm@sha256:49d73c49616929b0a4f37c50fee0056eb4b0f15de624591e8d9bf84b4dfdd3ce AS prod-cpu
|
||||
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:7029b00486ac40bed03e36775b864d3f3d39dcbdf19cd45e6a52d541e6c178f0 AS prod-openvino
|
||||
FROM python:3.11-slim-bookworm@sha256:49d73c49616929b0a4f37c50fee0056eb4b0f15de624591e8d9bf84b4dfdd3ce AS prod-openvino
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||
|
||||
@@ -278,8 +278,8 @@ class TestOrtSession:
|
||||
|
||||
assert session.provider_options == []
|
||||
|
||||
def test_sets_default_sess_options(self) -> None:
|
||||
session = OrtSession("ViT-B-32__openai")
|
||||
def test_sets_default_sess_options_if_cpu(self) -> None:
|
||||
session = OrtSession("ViT-B-32__openai", providers=["CPUExecutionProvider"])
|
||||
|
||||
assert session.sess_options.execution_mode == ort.ExecutionMode.ORT_SEQUENTIAL
|
||||
assert session.sess_options.inter_op_num_threads == 1
|
||||
|
||||
212
machine-learning/uv.lock
generated
212
machine-learning/uv.lock
generated
@@ -876,7 +876,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "0.29.3"
|
||||
version = "0.30.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
@@ -887,9 +887,9 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e5/f9/851f34b02970e8143d41d4001b2d49e54ef113f273902103823b8bc95ada/huggingface_hub-0.29.3.tar.gz", hash = "sha256:64519a25716e0ba382ba2d3fb3ca082e7c7eb4a2fc634d200e8380006e0760e5", size = 390123 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/22/8eb91736b1dcb83d879bd49050a09df29a57cc5cd9f38e48a4b1c45ee890/huggingface_hub-0.30.2.tar.gz", hash = "sha256:9a7897c5b6fd9dad3168a794a8998d6378210f5b9688d0dfc180b1a228dc2466", size = 400868 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/40/0c/37d380846a2e5c9a3c6a73d26ffbcfdcad5fc3eacf42fdf7cff56f2af634/huggingface_hub-0.29.3-py3-none-any.whl", hash = "sha256:0b25710932ac649c08cdbefa6c6ccb8e88eef82927cacdb048efb726429453aa", size = 468997 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/27/1fb384a841e9661faad1c31cbfa62864f59632e876df5d795234da51c395/huggingface_hub-0.30.2-py3-none-any.whl", hash = "sha256:68ff05969927058cfa41df4f2155d4bb48f5f54f719dd0390103eefa9b191e28", size = 481433 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1789,7 +1789,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.1"
|
||||
version = "2.11.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
@@ -1797,96 +1797,96 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/a3/698b87a4d4d303d7c5f62ea5fbf7a79cab236ccfbd0a17847b7f77f8163e/pydantic-2.11.1.tar.gz", hash = "sha256:442557d2910e75c991c39f4b4ab18963d57b9b55122c8b2a9cd176d8c29ce968", size = 782817 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/12/f9221a949f2419e2e23847303c002476c26fbcfd62dc7f3d25d0bec5ca99/pydantic-2.11.1-py3-none-any.whl", hash = "sha256:5b6c415eee9f8123a14d859be0c84363fec6b1feb6b688d6435801230b56e0b8", size = 442648 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.33.0"
|
||||
version = "2.33.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/05/91ce14dfd5a3a99555fce436318cc0fd1f08c4daa32b3248ad63669ea8b4/pydantic_core-2.33.0.tar.gz", hash = "sha256:40eb8af662ba409c3cbf4a8150ad32ae73514cd7cb1f1a2113af39763dd616b3", size = 434080 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/29/43/0649ad07e66b36a3fb21442b425bd0348ac162c5e686b36471f363201535/pydantic_core-2.33.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71dffba8fe9ddff628c68f3abd845e91b028361d43c5f8e7b3f8b91d7d85413e", size = 2042968 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/a6/975fea4774a459e495cb4be288efd8b041ac756a0a763f0b976d0861334b/pydantic_core-2.33.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:abaeec1be6ed535a5d7ffc2e6c390083c425832b20efd621562fbb5bff6dc518", size = 1860347 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/49/7858dadad305101a077ec4d0c606b6425a2b134ea8d858458a6d287fd871/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759871f00e26ad3709efc773ac37b4d571de065f9dfb1778012908bcc36b3a73", size = 1910060 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/4f/6522527911d9c5fe6d76b084d8b388d5c84b09d113247b39f91937500b34/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dcfebee69cd5e1c0b76a17e17e347c84b00acebb8dd8edb22d4a03e88e82a207", size = 1997129 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/d0/06f396da053e3d73001ea4787e56b4d7132a87c0b5e2e15a041e808c35cd/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b1262b912435a501fa04cd213720609e2cefa723a07c92017d18693e69bf00b", size = 2140389 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/6b/b9ff5b69cd4ef007cf665463f3be2e481dc7eb26c4a55b2f57a94308c31a/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4726f1f3f42d6a25678c67da3f0b10f148f5655813c5aca54b0d1742ba821b8f", size = 2754237 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/80/b4879de375cdf3718d05fcb60c9aa1f119d28e261dafa51b6a69c78f7178/pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e790954b5093dff1e3a9a2523fddc4e79722d6f07993b4cd5547825c3cbf97b5", size = 2007433 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/24/54054713dc0af98a94eab37e0f4294dfd5cd8f70b2ca9dcdccd15709fd7e/pydantic_core-2.33.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34e7fb3abe375b5c4e64fab75733d605dda0f59827752debc99c17cb2d5f3276", size = 2123980 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/4c/257c1cb89e14cfa6e95ebcb91b308eb1dd2b348340ff76a6e6fcfa9969e1/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ecb158fb9b9091b515213bed3061eb7deb1d3b4e02327c27a0ea714ff46b0760", size = 2087433 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/62/927df8a39ad78ef7b82c5446e01dec9bb0043e1ad71d8f426062f5f014db/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:4d9149e7528af8bbd76cc055967e6e04617dcb2a2afdaa3dea899406c5521faa", size = 2260242 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/f2/389414f7c77a100954e84d6f52a82bd1788ae69db72364376d8a73b38765/pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e81a295adccf73477220e15ff79235ca9dcbcee4be459eb9d4ce9a2763b8386c", size = 2258227 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/99/94516313e15d906a1264bb40faf24a01a4af4e2ca8a7c10dd173b6513c5a/pydantic_core-2.33.0-cp310-cp310-win32.whl", hash = "sha256:f22dab23cdbce2005f26a8f0c71698457861f97fc6318c75814a50c75e87d025", size = 1925523 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/67/cc789611c6035a0b71305a1ec6ba196256ced76eba8375f316f840a70456/pydantic_core-2.33.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cb2390355ba084c1ad49485d18449b4242da344dea3e0fe10babd1f0db7dcfc", size = 1951872 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/93/9e97af2619b4026596487a79133e425c7d3c374f0a7f100f3d76bcdf9c83/pydantic_core-2.33.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a608a75846804271cf9c83e40bbb4dab2ac614d33c6fd5b0c6187f53f5c593ef", size = 2042784 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/b4/0bba8412fd242729feeb80e7152e24f0e1a1c19f4121ca3d4a307f4e6222/pydantic_core-2.33.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e1c69aa459f5609dec2fa0652d495353accf3eda5bdb18782bc5a2ae45c9273a", size = 1858179 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/1f/c1c40305d929bd08af863df64b0a26203b70b352a1962d86f3bcd52950fe/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9ec80eb5a5f45a2211793f1c4aeddff0c3761d1c70d684965c1807e923a588b", size = 1909396 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/99/d2e727375c329c1e652b5d450fbb9d56e8c3933a397e4bd46e67c68c2cd5/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e925819a98318d17251776bd3d6aa9f3ff77b965762155bdad15d1a9265c4cfd", size = 1998264 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/2e/3119a33931278d96ecc2e9e1b9d50c240636cfeb0c49951746ae34e4de74/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bf68bb859799e9cec3d9dd8323c40c00a254aabb56fe08f907e437005932f2b", size = 2140588 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/bd/9267bd1ba55f17c80ef6cb7e07b3890b4acbe8eb6014f3102092d53d9300/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b2ea72dea0825949a045fa4071f6d5b3d7620d2a208335207793cf29c5a182d", size = 2746296 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/ed/ef37de6478a412ee627cbebd73e7b72a680f45bfacce9ff1199de6e17e88/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1583539533160186ac546b49f5cde9ffc928062c96920f58bd95de32ffd7bffd", size = 2005555 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/84/72c8d1439585d8ee7bc35eb8f88a04a4d302ee4018871f1f85ae1b0c6625/pydantic_core-2.33.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23c3e77bf8a7317612e5c26a3b084c7edeb9552d645742a54a5867635b4f2453", size = 2124452 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/8f/cb13de30c6a3e303423751a529a3d1271c2effee4b98cf3e397a66ae8498/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7a7f2a3f628d2f7ef11cb6188bcf0b9e1558151d511b974dfea10a49afe192b", size = 2087001 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/d0/e93dc8884bf288a63fedeb8040ac8f29cb71ca52e755f48e5170bb63e55b/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:f1fb026c575e16f673c61c7b86144517705865173f3d0907040ac30c4f9f5915", size = 2261663 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/ba/4b7739c95efa0b542ee45fd872c8f6b1884ab808cf04ce7ac6621b6df76e/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:635702b2fed997e0ac256b2cfbdb4dd0bf7c56b5d8fba8ef03489c03b3eb40e2", size = 2257786 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/98/73cbca1d2360c27752cfa2fcdcf14d96230e92d7d48ecd50499865c56bf7/pydantic_core-2.33.0-cp311-cp311-win32.whl", hash = "sha256:07b4ced28fccae3f00626eaa0c4001aa9ec140a29501770a88dbbb0966019a86", size = 1925697 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/26/d85a40edeca5d8830ffc33667d6fef329fd0f4bc0c5181b8b0e206cfe488/pydantic_core-2.33.0-cp311-cp311-win_amd64.whl", hash = "sha256:4927564be53239a87770a5f86bdc272b8d1fbb87ab7783ad70255b4ab01aa25b", size = 1949859 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/0b/5a381605f0b9870465b805f2c86c06b0a7c191668ebe4117777306c2c1e5/pydantic_core-2.33.0-cp311-cp311-win_arm64.whl", hash = "sha256:69297418ad644d521ea3e1aa2e14a2a422726167e9ad22b89e8f1130d68e1e9a", size = 1907978 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/c4/c9381323cbdc1bb26d352bc184422ce77c4bc2f2312b782761093a59fafc/pydantic_core-2.33.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6c32a40712e3662bebe524abe8abb757f2fa2000028d64cc5a1006016c06af43", size = 2025127 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/bd/af35278080716ecab8f57e84515c7dc535ed95d1c7f52c1c6f7b313a9dab/pydantic_core-2.33.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ec86b5baa36f0a0bfb37db86c7d52652f8e8aa076ab745ef7725784183c3fdd", size = 1851687 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/e4/a01461225809c3533c23bd1916b1e8c2e21727f0fea60ab1acbffc4e2fca/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4deac83a8cc1d09e40683be0bc6d1fa4cde8df0a9bf0cda5693f9b0569ac01b6", size = 1892232 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/17/3d53d62a328fb0a49911c2962036b9e7a4f781b7d15e9093c26299e5f76d/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:175ab598fb457a9aee63206a1993874badf3ed9a456e0654273e56f00747bbd6", size = 1977896 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/98/01f9d86e02ec4a38f4b02086acf067f2c776b845d43f901bd1ee1c21bc4b/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f36afd0d56a6c42cf4e8465b6441cf546ed69d3a4ec92724cc9c8c61bd6ecf4", size = 2127717 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/43/6f381575c61b7c58b0fd0b92134c5a1897deea4cdfc3d47567b3ff460a4e/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a98257451164666afafc7cbf5fb00d613e33f7e7ebb322fbcd99345695a9a61", size = 2680287 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/42/c0d10d1451d161a9a0da9bbef023b8005aa26e9993a8cc24dc9e3aa96c93/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecc6d02d69b54a2eb83ebcc6f29df04957f734bcf309d346b4f83354d8376862", size = 2008276 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/ca/e08df9dba546905c70bae44ced9f3bea25432e34448d95618d41968f40b7/pydantic_core-2.33.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a69b7596c6603afd049ce7f3835bcf57dd3892fc7279f0ddf987bebed8caa5a", size = 2115305 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/1f/9b01d990730a98833113581a78e595fd40ed4c20f9693f5a658fb5f91eff/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea30239c148b6ef41364c6f51d103c2988965b643d62e10b233b5efdca8c0099", size = 2068999 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/18/fe752476a709191148e8b1e1139147841ea5d2b22adcde6ee6abb6c8e7cf/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:abfa44cf2f7f7d7a199be6c6ec141c9024063205545aa09304349781b9a125e6", size = 2241488 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/22/14738ad0a0bf484b928c9e52004f5e0b81dd8dabbdf23b843717b37a71d1/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20d4275f3c4659d92048c70797e5fdc396c6e4446caf517ba5cad2db60cd39d3", size = 2248430 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/27/be7571e215ac8d321712f2433c445b03dbcd645366a18f67b334df8912bc/pydantic_core-2.33.0-cp312-cp312-win32.whl", hash = "sha256:918f2013d7eadea1d88d1a35fd4a1e16aaf90343eb446f91cb091ce7f9b431a2", size = 1908353 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/3a/be78f28732f93128bd0e3944bdd4b3970b389a1fbd44907c97291c8dcdec/pydantic_core-2.33.0-cp312-cp312-win_amd64.whl", hash = "sha256:aec79acc183865bad120b0190afac467c20b15289050648b876b07777e67ea48", size = 1955956 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/26/b8911ac74faa994694b76ee6a22875cc7a4abea3c381fdba4edc6c6bef84/pydantic_core-2.33.0-cp312-cp312-win_arm64.whl", hash = "sha256:5461934e895968655225dfa8b3be79e7e927e95d4bd6c2d40edd2fa7052e71b6", size = 1903259 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/20/de2ad03ce8f5b3accf2196ea9b44f31b0cd16ac6e8cfc6b21976ed45ec35/pydantic_core-2.33.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f00e8b59e1fc8f09d05594aa7d2b726f1b277ca6155fc84c0396db1b373c4555", size = 2032214 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/af/6817dfda9aac4958d8b516cbb94af507eb171c997ea66453d4d162ae8948/pydantic_core-2.33.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a73be93ecef45786d7d95b0c5e9b294faf35629d03d5b145b09b81258c7cd6d", size = 1852338 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/f3/49193a312d9c49314f2b953fb55740b7c530710977cabe7183b8ef111b7f/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff48a55be9da6930254565ff5238d71d5e9cd8c5487a191cb85df3bdb8c77365", size = 1896913 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/e0/c746677825b2e29a2fa02122a8991c83cdd5b4c5f638f0664d4e35edd4b2/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4ea04195638dcd8c53dadb545d70badba51735b1594810e9768c2c0b4a5da", size = 1986046 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/ec/44914e7ff78cef16afb5e5273d480c136725acd73d894affdbe2a1bbaad5/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41d698dcbe12b60661f0632b543dbb119e6ba088103b364ff65e951610cb7ce0", size = 2128097 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/f5/c6247d424d01f605ed2e3802f338691cae17137cee6484dce9f1ac0b872b/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae62032ef513fe6281ef0009e30838a01057b832dc265da32c10469622613885", size = 2681062 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/85/114a2113b126fdd7cf9a9443b1b1fe1b572e5bd259d50ba9d5d3e1927fa9/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f225f3a3995dbbc26affc191d0443c6c4aa71b83358fd4c2b7d63e2f6f0336f9", size = 2007487 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/40/3c05ed28d225c7a9acd2b34c5c8010c279683a870219b97e9f164a5a8af0/pydantic_core-2.33.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bdd36b362f419c78d09630cbaebc64913f66f62bda6d42d5fbb08da8cc4f181", size = 2121382 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/22/e70c086f41eebd323e6baa92cc906c3f38ddce7486007eb2bdb3b11c8f64/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a0147c0bef783fd9abc9f016d66edb6cac466dc54a17ec5f5ada08ff65caf5d", size = 2072473 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/84/d1614dedd8fe5114f6a0e348bcd1535f97d76c038d6102f271433cd1361d/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c860773a0f205926172c6644c394e02c25421dc9a456deff16f64c0e299487d3", size = 2249468 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/c0/787061eef44135e00fddb4b56b387a06c303bfd3884a6df9bea5cb730230/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:138d31e3f90087f42aa6286fb640f3c7a8eb7bdae829418265e7e7474bd2574b", size = 2254716 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/e2/27262eb04963201e89f9c280f1e10c493a7a37bc877e023f31aa72d2f911/pydantic_core-2.33.0-cp313-cp313-win32.whl", hash = "sha256:d20cbb9d3e95114325780f3cfe990f3ecae24de7a2d75f978783878cce2ad585", size = 1916450 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/8d/25ff96f1e89b19e0b70b3cd607c9ea7ca27e1dcb810a9cd4255ed6abf869/pydantic_core-2.33.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca1103d70306489e3d006b0f79db8ca5dd3c977f6f13b2c59ff745249431a606", size = 1956092 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/64/66a2efeff657b04323ffcd7b898cb0354d36dae3a561049e092134a83e9c/pydantic_core-2.33.0-cp313-cp313-win_arm64.whl", hash = "sha256:6291797cad239285275558e0a27872da735b05c75d5237bbade8736f80e4c225", size = 1908367 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/54/295e38769133363d7ec4a5863a4d579f331728c71a6644ff1024ee529315/pydantic_core-2.33.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b79af799630af263eca9ec87db519426d8c9b3be35016eddad1832bac812d87", size = 1813331 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/9c/0c8ea02db8d682aa1ef48938abae833c1d69bdfa6e5ec13b21734b01ae70/pydantic_core-2.33.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf946a4739b5237f4f56d77fa6668263bc466d06a8036c055587c130a46f7b", size = 1986653 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/4f/3fb47d6cbc08c7e00f92300e64ba655428c05c56b8ab6723bd290bae6458/pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7", size = 1931234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/77/85e173b715e1a277ce934f28d877d82492df13e564fa68a01c96f36a47ad/pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e2762c568596332fdab56b07060c8ab8362c56cf2a339ee54e491cd503612c50", size = 2040129 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/e7/33da5f8a94bbe2191cfcd15bd6d16ecd113e67da1b8c78d3cc3478112dab/pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bf637300ff35d4f59c006fff201c510b2b5e745b07125458a5389af3c0dff8c", size = 1872656 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/7a/9600f222bea840e5b9ba1f17c0acc79b669b24542a78c42c6a10712c0aae/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c151ce3d59ed56ebd7ce9ce5986a409a85db697d25fc232f8e81f195aa39a1", size = 1903731 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/d2/94c7ca4e24c5dcfb74df92e0836c189e9eb6814cf62d2f26a75ea0a906db/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee65f0cc652261744fd07f2c6e6901c914aa6c5ff4dcfaf1136bc394d0dd26b", size = 2083966 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/74/a0259989d220e8865ed6866a6d40539e40fa8f507e587e35d2414cc081f8/pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:024d136ae44d233e6322027bbf356712b3940bee816e6c948ce4b90f18471b3d", size = 2118951 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/4c/87405ed04d6d07597920b657f082a8e8e58bf3034178bb9044b4d57a91e2/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e37f10f6d4bc67c58fbd727108ae1d8b92b397355e68519f1e4a7babb1473442", size = 2079632 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/4c/bcb02970ef91d4cd6de7c6893101302637da456bc8b52c18ea0d047b55ce/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:502ed542e0d958bd12e7c3e9a015bce57deaf50eaa8c2e1c439b512cb9db1e3a", size = 2250541 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/2b/dbe5450c4cd904be5da736dcc7f2357b828199e29e38de19fc81f988b288/pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:715c62af74c236bf386825c0fdfa08d092ab0f191eb5b4580d11c3189af9d330", size = 2255685 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/a6/ca1d35f695d81f639c5617fc9efb44caad21a9463383fa45364b3044175a/pydantic_core-2.33.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bccc06fa0372151f37f6b69834181aa9eb57cf8665ed36405fb45fbf6cac3bae", size = 2082395 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b2/553e42762e7b08771fca41c0230c1ac276f9e79e78f57628e1b7d328551d/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d8dc9f63a26f7259b57f46a7aab5af86b2ad6fbe48487500bb1f4b27e051e4c", size = 2041207 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/81/a91a57bbf3efe53525ab75f65944b8950e6ef84fe3b9a26c1ec173363263/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30369e54d6d0113d2aa5aee7a90d17f225c13d87902ace8fcd7bbf99b19124db", size = 1873736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d2/5ab52e9f551cdcbc1ee99a0b3ef595f56d031f66f88e5ca6726c49f9ce65/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb479354c62067afa62f53bb387827bee2f75c9c79ef25eef6ab84d4b1ae3b", size = 1903794 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/5f/a81742d3f3821b16f1265f057d6e0b68a3ab13a814fe4bffac536a1f26fd/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0310524c833d91403c960b8a3cf9f46c282eadd6afd276c8c5edc617bd705dc9", size = 2083457 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2f/e872005bc0fc47f9c036b67b12349a8522d32e3bda928e82d676e2a594d1/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eddb18a00bbb855325db27b4c2a89a4ba491cd6a0bd6d852b225172a1f54b36c", size = 2119537 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/13/183f13ce647202eaf3dada9e42cdfc59cbb95faedd44d25f22b931115c7f/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ade5dbcf8d9ef8f4b28e682d0b29f3008df9842bb5ac48ac2c17bc55771cc976", size = 2080069 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/8b/b6be91243da44a26558d9c3a9007043b3750334136c6550551e8092d6d96/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2c0afd34f928383e3fd25740f2050dbac9d077e7ba5adbaa2227f4d4f3c8da5c", size = 2251618 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c5/fbcf1977035b834f63eb542e74cd6c807177f383386175b468f0865bcac4/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7da333f21cd9df51d5731513a6d39319892947604924ddf2e24a4612975fb936", size = 2255374 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/f8/66f328e411f1c9574b13c2c28ab01f308b53688bbbe6ca8fb981e6cabc42/pydantic_core-2.33.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b6d77c75a57f041c5ee915ff0b0bb58eabb78728b69ed967bc5b780e8f701b8", size = 2082099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1960,15 +1960,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "6.0.0"
|
||||
version = "6.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coverage", extra = ["toml"] },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2225,27 +2225,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.2"
|
||||
version = "0.11.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/5b/3ae20f89777115944e89c2d8c2e795dcc5b9e04052f76d5347e35e0da66e/ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407", size = 3933063 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/db/baee59ac88f57527fcbaad3a7b309994e42329c6bc4d4d2b681a3d7b5426/ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2", size = 10106493 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/d6/9a0962cbb347f4ff98b33d699bf1193ff04ca93bed4b4222fd881b502154/ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc", size = 10876382 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/8f/62bab0c7d7e1ae3707b69b157701b41c1ccab8f83e8501734d12ea8a839f/ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906", size = 10237050 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/96/e296965ae9705af19c265d4d441958ed65c0c58fc4ec340c27cc9d2a1f5b/ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f", size = 10424984 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/56/644595eb57d855afed6e54b852e2df8cd5ca94c78043b2f29bdfb29882d5/ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e", size = 9957438 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/83/9d3f3bed0118aef3e871ded9e5687fb8c5776bde233427fd9ce0a45db2d4/ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223", size = 11547282 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/e6/0c6e4f5ae72fac5ccb44d72c0111f294a5c2c8cc5024afcb38e6bda5f4b3/ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e", size = 12182020 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/92/4aed0e460aeb1df5ea0c2fbe8d04f9725cccdb25d8da09a0d3f5b8764bf8/ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d", size = 11679154 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/d3/7316aa2609f2c592038e2543483eafbc62a0e1a6a6965178e284808c095c/ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99", size = 13905985 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/80/734d3d17546e47ff99871f44ea7540ad2bbd7a480ed197fe8a1c8a261075/ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222", size = 11348343 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/7b/70fc7f09a0161dce9613a4671d198f609e653d6f4ff9eee14d64c4c240fb/ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304", size = 10308487 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/22/1cdd62dabd678d75842bf4944fd889cf794dc9e58c18cc547f9eb28f95ed/ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019", size = 9929091 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/20/40e0563506332313148e783bbc1e4276d657962cc370657b2fff20e6e058/ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896", size = 10924659 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/41/eef9b7aac8819d9e942f617f9db296f13d2c4576806d604aba8db5a753f1/ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751", size = 11428160 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/61/c488943414fb2b8754c02f3879de003e26efdd20f38167ded3fb3fc1cda3/ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270", size = 10311496 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2b/2a1c8deb5f5dfa3871eb7daa41492c4d2b2824a74d2b38e788617612a66d/ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb", size = 11399146 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/03/3aec4846226d54a37822e4c7ea39489e4abd6f88388fba74e3d4abe77300/ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc", size = 10450306 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||
<uses-permission android:name="android.permission.MANAGE_MEDIA" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
@@ -124,4 +125,4 @@
|
||||
<data android:scheme="geo" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
package app.alextran.immich
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
import java.security.MessageDigest
|
||||
import java.io.FileInputStream
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Android plugin for Dart `BackgroundService`
|
||||
*
|
||||
* Receives messages/method calls from the foreground Dart side to manage
|
||||
* the background service, e.g. start (enqueue), stop (cancel)
|
||||
* Android plugin for Dart `BackgroundService` and file trash operations
|
||||
*/
|
||||
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener {
|
||||
|
||||
private var methodChannel: MethodChannel? = null
|
||||
private var fileTrashChannel: MethodChannel? = null
|
||||
private var context: Context? = null
|
||||
private var pendingResult: Result? = null
|
||||
private val PERMISSION_REQUEST_CODE = 1001
|
||||
private var activityBinding: ActivityPluginBinding? = null
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
|
||||
@@ -29,6 +44,10 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
context = ctx
|
||||
methodChannel = MethodChannel(messenger, "immich/foregroundChannel")
|
||||
methodChannel?.setMethodCallHandler(this)
|
||||
|
||||
// Add file trash channel
|
||||
fileTrashChannel = MethodChannel(messenger, "file_trash")
|
||||
fileTrashChannel?.setMethodCallHandler(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
@@ -38,11 +57,14 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private fun onDetachedFromEngine() {
|
||||
methodChannel?.setMethodCallHandler(null)
|
||||
methodChannel = null
|
||||
fileTrashChannel?.setMethodCallHandler(null)
|
||||
fileTrashChannel = null
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||
val ctx = context!!
|
||||
when (call.method) {
|
||||
// Existing BackgroundService methods
|
||||
"enable" -> {
|
||||
val args = call.arguments<ArrayList<*>>()!!
|
||||
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||
@@ -114,10 +136,180 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// File Trash methods moved from MainActivity
|
||||
"moveToTrash" -> {
|
||||
val fileName = call.argument<String>("fileName")
|
||||
if (fileName != null) {
|
||||
if (hasManageStoragePermission()) {
|
||||
val success = moveToTrash(fileName)
|
||||
result.success(success)
|
||||
} else {
|
||||
result.error("PERMISSION_DENIED", "Storage permission required", null)
|
||||
}
|
||||
} else {
|
||||
result.error("INVALID_NAME", "The file name is not specified.", null)
|
||||
}
|
||||
}
|
||||
|
||||
"restoreFromTrash" -> {
|
||||
val fileName = call.argument<String>("fileName")
|
||||
if (fileName != null) {
|
||||
if (hasManageStoragePermission()) {
|
||||
val success = untrashImage(fileName)
|
||||
result.success(success)
|
||||
} else {
|
||||
result.error("PERMISSION_DENIED", "Storage permission required", null)
|
||||
}
|
||||
} else {
|
||||
result.error("INVALID_NAME", "The file name is not specified.", null)
|
||||
}
|
||||
}
|
||||
|
||||
"requestManageStoragePermission" -> {
|
||||
if (!hasManageStoragePermission()) {
|
||||
requestManageStoragePermission(result)
|
||||
} else {
|
||||
Log.e("Manage storage permission", "Permission already granted")
|
||||
result.success(true)
|
||||
}
|
||||
}
|
||||
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
// File Trash methods moved from MainActivity
|
||||
private fun hasManageStoragePermission(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Environment.isExternalStorageManager()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestManageStoragePermission(result: Result) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
pendingResult = result // Store the result callback
|
||||
val activity = activityBinding?.activity ?: return
|
||||
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
intent.data = Uri.parse("package:${activity.packageName}")
|
||||
activity.startActivityForResult(intent, PERMISSION_REQUEST_CODE)
|
||||
} else {
|
||||
result.success(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveToTrash(fileName: String): Boolean {
|
||||
val contentResolver = context?.contentResolver ?: return false
|
||||
val uri = getFileUri(fileName)
|
||||
Log.e("FILE_URI", uri.toString())
|
||||
return uri?.let { moveToTrash(it) } ?: false
|
||||
}
|
||||
|
||||
private fun moveToTrash(contentUri: Uri): Boolean {
|
||||
val contentResolver = context?.contentResolver ?: return false
|
||||
return try {
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.IS_TRASHED, 1) // Move to trash
|
||||
}
|
||||
val updated = contentResolver.update(contentUri, values, null, null)
|
||||
updated > 0
|
||||
} catch (e: Exception) {
|
||||
Log.e("TrashError", "Error moving to trash", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileUri(fileName: String): Uri? {
|
||||
val contentResolver = context?.contentResolver ?: return null
|
||||
val contentUri = MediaStore.Files.getContentUri("external")
|
||||
val projection = arrayOf(MediaStore.Images.Media._ID)
|
||||
val selection = "${MediaStore.Images.Media.DISPLAY_NAME} = ?"
|
||||
val selectionArgs = arrayOf(fileName)
|
||||
var fileUri: Uri? = null
|
||||
|
||||
contentResolver.query(contentUri, projection, selection, selectionArgs, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
|
||||
fileUri = ContentUris.withAppendedId(contentUri, id)
|
||||
}
|
||||
}
|
||||
return fileUri
|
||||
}
|
||||
|
||||
private fun untrashImage(name: String): Boolean {
|
||||
val contentResolver = context?.contentResolver ?: return false
|
||||
val uri = getTrashedFileUri(contentResolver, name)
|
||||
Log.e("FILE_URI", uri.toString())
|
||||
return uri?.let { untrashImage(it) } ?: false
|
||||
}
|
||||
|
||||
private fun untrashImage(contentUri: Uri): Boolean {
|
||||
val contentResolver = context?.contentResolver ?: return false
|
||||
return try {
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.IS_TRASHED, 0) // Restore file
|
||||
}
|
||||
val updated = contentResolver.update(contentUri, values, null, null)
|
||||
updated > 0
|
||||
} catch (e: Exception) {
|
||||
Log.e("TrashError", "Error restoring file", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTrashedFileUri(contentResolver: ContentResolver, fileName: String): Uri? {
|
||||
val contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||
val projection = arrayOf(MediaStore.Files.FileColumns._ID)
|
||||
|
||||
val queryArgs = Bundle().apply {
|
||||
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?")
|
||||
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName))
|
||||
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
|
||||
}
|
||||
|
||||
contentResolver.query(contentUri, projection, queryArgs, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
|
||||
return ContentUris.withAppendedId(contentUri, id)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// ActivityAware implementation
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activityBinding = binding
|
||||
binding.addActivityResultListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activityBinding?.removeActivityResultListener(this)
|
||||
activityBinding = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activityBinding = binding
|
||||
binding.addActivityResultListener(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
activityBinding?.removeActivityResultListener(this)
|
||||
activityBinding = null
|
||||
}
|
||||
|
||||
// ActivityResultListener implementation
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||
val granted = hasManageStoragePermission()
|
||||
pendingResult?.success(granted)
|
||||
pendingResult = null
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "BackgroundServicePlugin"
|
||||
private const val BUFFER_SIZE = 2 * 1024 * 1024;
|
||||
private const val BUFFER_SIZE = 2 * 1024 * 1024
|
||||
|
||||
@@ -2,14 +2,12 @@ package app.alextran.immich
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import android.os.Bundle
|
||||
import android.content.Intent
|
||||
import androidx.annotation.NonNull
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
flutterEngine.plugins.add(BackgroundServicePlugin())
|
||||
// No need to set up method channel here as it's now handled in the plugin
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"advanced_settings_tile_title": "Advanced",
|
||||
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
||||
"advanced_settings_troubleshooting_title": "Troubleshooting",
|
||||
"advanced_settings_sync_remote_deletions_title": "Sync remote deletions [EXPERIMENTAL]",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "Automatically delete or restore an asset on this device when that action is taken on the web",
|
||||
"album_info_card_backup_album_excluded": "EXCLUDED",
|
||||
"album_info_card_backup_album_included": "INCLUDED",
|
||||
"albums": "Albums",
|
||||
|
||||
@@ -65,6 +65,7 @@ enum StoreKey<T> {
|
||||
|
||||
// Video settings
|
||||
loadOriginalVideo<bool>._(136),
|
||||
manageLocalMediaAndroid<bool>._(137),
|
||||
|
||||
// Experimental stuff
|
||||
photoManagerCustomFilter<bool>._(1000);
|
||||
|
||||
5
mobile/lib/interfaces/local_files_manager.interface.dart
Normal file
5
mobile/lib/interfaces/local_files_manager.interface.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
abstract interface class ILocalFilesManager {
|
||||
Future<bool> moveToTrash(String fileName);
|
||||
Future<bool> restoreFromTrash(String fileName);
|
||||
Future<bool> requestManageStoragePermission();
|
||||
}
|
||||
@@ -23,6 +23,7 @@ enum PendingAction {
|
||||
assetDelete,
|
||||
assetUploaded,
|
||||
assetHidden,
|
||||
assetTrash,
|
||||
}
|
||||
|
||||
class PendingChange {
|
||||
@@ -160,7 +161,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
socket.on('on_upload_success', _handleOnUploadSuccess);
|
||||
socket.on('on_config_update', _handleOnConfigUpdate);
|
||||
socket.on('on_asset_delete', _handleOnAssetDelete);
|
||||
socket.on('on_asset_trash', _handleServerUpdates);
|
||||
socket.on('on_asset_trash', _handleOnAssetTrash);
|
||||
socket.on('on_asset_restore', _handleServerUpdates);
|
||||
socket.on('on_asset_update', _handleServerUpdates);
|
||||
socket.on('on_asset_stack_update', _handleServerUpdates);
|
||||
@@ -207,6 +208,26 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
_debounce.run(handlePendingChanges);
|
||||
}
|
||||
|
||||
Future<void> _handlePendingTrashes() async {
|
||||
final trashChanges = state.pendingChanges
|
||||
.where((c) => c.action == PendingAction.assetTrash)
|
||||
.toList();
|
||||
if (trashChanges.isNotEmpty) {
|
||||
List<String> remoteIds = trashChanges
|
||||
.expand((a) => (a.value as List).map((e) => e.toString()))
|
||||
.toList();
|
||||
|
||||
await _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds);
|
||||
await _ref.read(assetProvider.notifier).getAllAsset();
|
||||
|
||||
state = state.copyWith(
|
||||
pendingChanges: state.pendingChanges
|
||||
.whereNot((c) => trashChanges.contains(c))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePendingDeletes() async {
|
||||
final deleteChanges = state.pendingChanges
|
||||
.where((c) => c.action == PendingAction.assetDelete)
|
||||
@@ -267,6 +288,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
await _handlePendingUploaded();
|
||||
await _handlePendingDeletes();
|
||||
await _handlingPendingHidden();
|
||||
await _handlePendingTrashes();
|
||||
}
|
||||
|
||||
void _handleOnConfigUpdate(dynamic _) {
|
||||
@@ -285,6 +307,10 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
void _handleOnAssetDelete(dynamic data) =>
|
||||
addPendingChange(PendingAction.assetDelete, data);
|
||||
|
||||
void _handleOnAssetTrash(dynamic data) {
|
||||
addPendingChange(PendingAction.assetTrash, data);
|
||||
}
|
||||
|
||||
void _handleOnAssetHidden(dynamic data) =>
|
||||
addPendingChange(PendingAction.assetHidden, data);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
||||
|
||||
@override
|
||||
Future<List<String>> getAssetIds(String albumId) async {
|
||||
final album = await AssetPathEntity.fromId(albumId);
|
||||
final album = await AssetPathEntity.obtainPathFromProperties(id: albumId);
|
||||
final List<AssetEntity> assets =
|
||||
await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
|
||||
return assets.map((e) => e.id).toList();
|
||||
@@ -38,7 +38,7 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
||||
|
||||
@override
|
||||
Future<int> getAssetCount(String albumId) async {
|
||||
final album = await AssetPathEntity.fromId(albumId);
|
||||
final album = await AssetPathEntity.obtainPathFromProperties(id: albumId);
|
||||
return album.assetCountAsync;
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
||||
DateTime? modifiedUntil,
|
||||
bool orderByModificationDate = false,
|
||||
}) async {
|
||||
final onDevice = await AssetPathEntity.fromId(
|
||||
albumId,
|
||||
filterOption: FilterOptionGroup(
|
||||
final onDevice = await AssetPathEntity.obtainPathFromProperties(
|
||||
id: albumId,
|
||||
optionGroup: FilterOptionGroup(
|
||||
imageOption: const FilterOption(needTitle: true),
|
||||
videoOption: const FilterOption(needTitle: true),
|
||||
containsPathModified: true,
|
||||
@@ -80,7 +80,8 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
||||
DateTime? modifiedFrom,
|
||||
DateTime? modifiedUntil,
|
||||
}) async {
|
||||
final assetPathEntity = await AssetPathEntity.fromId(id);
|
||||
final assetPathEntity =
|
||||
await AssetPathEntity.obtainPathFromProperties(id: id);
|
||||
return _toAlbum(assetPathEntity);
|
||||
}
|
||||
|
||||
|
||||
23
mobile/lib/repositories/local_files_manager.repository.dart
Normal file
23
mobile/lib/repositories/local_files_manager.repository.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
|
||||
import 'package:immich_mobile/utils/local_files_manager.dart';
|
||||
|
||||
final localFilesManagerRepositoryProvider =
|
||||
Provider((ref) => LocalFilesManagerRepository());
|
||||
|
||||
class LocalFilesManagerRepository implements ILocalFilesManager {
|
||||
@override
|
||||
Future<bool> moveToTrash(String fileName) async {
|
||||
return await LocalFilesManager.moveToTrash(fileName);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> restoreFromTrash(String fileName) async {
|
||||
return await LocalFilesManager.restoreFromTrash(fileName);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> requestManageStoragePermission() async {
|
||||
return await LocalFilesManager.requestManageStoragePermission();
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ enum AppSettingsEnum<T> {
|
||||
0,
|
||||
),
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
||||
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
|
||||
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
|
||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
|
||||
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -16,6 +17,8 @@ import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/interfaces/partner.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
|
||||
@@ -25,6 +28,8 @@ import 'package:immich_mobile/repositories/album_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/etag.repository.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/repositories/partner.repository.dart';
|
||||
import 'package:immich_mobile/repositories/partner_api.repository.dart';
|
||||
import 'package:immich_mobile/services/entity.service.dart';
|
||||
@@ -48,6 +53,8 @@ final syncServiceProvider = Provider(
|
||||
ref.watch(userRepositoryProvider),
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(etagRepositoryProvider),
|
||||
ref.watch(appSettingsServiceProvider),
|
||||
ref.watch(localFilesManagerRepositoryProvider),
|
||||
ref.watch(partnerApiRepositoryProvider),
|
||||
ref.watch(userApiRepositoryProvider),
|
||||
),
|
||||
@@ -69,6 +76,8 @@ class SyncService {
|
||||
final IUserApiRepository _userApiRepository;
|
||||
final AsyncMutex _lock = AsyncMutex();
|
||||
final Logger _log = Logger('SyncService');
|
||||
final AppSettingsService _appSettingsService;
|
||||
final ILocalFilesManager _localFilesManager;
|
||||
|
||||
SyncService(
|
||||
this._hashService,
|
||||
@@ -82,6 +91,8 @@ class SyncService {
|
||||
this._userRepository,
|
||||
this._userService,
|
||||
this._eTagRepository,
|
||||
this._appSettingsService,
|
||||
this._localFilesManager,
|
||||
this._partnerApiRepository,
|
||||
this._userApiRepository,
|
||||
);
|
||||
@@ -238,8 +249,19 @@ class SyncService {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _moveToTrashMatchedAssets(Iterable<String> idsToDelete) async {
|
||||
final List<Asset> localAssets = await _assetRepository.getAllLocal();
|
||||
final List<Asset> matchedAssets = localAssets
|
||||
.where((asset) => idsToDelete.contains(asset.remoteId))
|
||||
.toList();
|
||||
|
||||
for (var asset in matchedAssets) {
|
||||
_localFilesManager.moveToTrash(asset.fileName);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes remote-only assets, updates merged assets to be local-only
|
||||
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) {
|
||||
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) async {
|
||||
return _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteAllByRemoteId(
|
||||
idsToDelete,
|
||||
@@ -249,6 +271,12 @@ class SyncService {
|
||||
idsToDelete,
|
||||
state: AssetState.merged,
|
||||
);
|
||||
if (Platform.isAndroid &&
|
||||
_appSettingsService.getSetting<bool>(
|
||||
AppSettingsEnum.manageLocalMediaAndroid,
|
||||
)) {
|
||||
await _moveToTrashMatchedAssets(idsToDelete);
|
||||
}
|
||||
if (merged.isEmpty) return;
|
||||
for (final Asset asset in merged) {
|
||||
asset.remoteId = null;
|
||||
@@ -790,9 +818,27 @@ class SyncService {
|
||||
return (existing, toUpsert);
|
||||
}
|
||||
|
||||
Future<void> _toggleTrashStatusForAssets(List<Asset> assetsList) async {
|
||||
for (var asset in assetsList) {
|
||||
if (asset.isTrashed) {
|
||||
_localFilesManager.moveToTrash(asset.fileName);
|
||||
} else {
|
||||
_localFilesManager.restoreFromTrash(asset.fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts or updates the assets in the database with their ExifInfo (if any)
|
||||
Future<void> upsertAssetsWithExif(List<Asset> assets) async {
|
||||
if (assets.isEmpty) return;
|
||||
|
||||
if (Platform.isAndroid &&
|
||||
_appSettingsService.getSetting<bool>(
|
||||
AppSettingsEnum.manageLocalMediaAndroid,
|
||||
)) {
|
||||
_toggleTrashStatusForAssets(assets);
|
||||
}
|
||||
|
||||
final exifInfos = assets.map((e) => e.exifInfo).nonNulls.toList();
|
||||
try {
|
||||
await _assetRepository.transaction(() async {
|
||||
|
||||
39
mobile/lib/utils/local_files_manager.dart
Normal file
39
mobile/lib/utils/local_files_manager.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class LocalFilesManager {
|
||||
static const MethodChannel _channel = MethodChannel('file_trash');
|
||||
|
||||
static Future<bool> moveToTrash(String fileName) async {
|
||||
try {
|
||||
final bool success =
|
||||
await _channel.invokeMethod('moveToTrash', {'fileName': fileName});
|
||||
return success;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('Error moving to trash: ${e.message}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> restoreFromTrash(String fileName) async {
|
||||
try {
|
||||
final bool success = await _channel
|
||||
.invokeMethod('restoreFromTrash', {'fileName': fileName});
|
||||
return success;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('Error restoring file: ${e.message}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> requestManageStoragePermission() async {
|
||||
try {
|
||||
final bool success =
|
||||
await _channel.invokeMethod('requestManageStoragePermission');
|
||||
return success;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('Error requesting permission: ${e.message}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:punycode/punycode.dart';
|
||||
|
||||
String sanitizeUrl(String url) {
|
||||
// Add schema if none is set
|
||||
@@ -11,13 +12,80 @@ String sanitizeUrl(String url) {
|
||||
}
|
||||
|
||||
String? getServerUrl() {
|
||||
final serverUrl = Store.tryGet(StoreKey.serverEndpoint);
|
||||
final serverUrl = punycodeDecodeUrl(Store.tryGet(StoreKey.serverEndpoint));
|
||||
final serverUri = serverUrl != null ? Uri.tryParse(serverUrl) : null;
|
||||
if (serverUri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return serverUri.hasPort
|
||||
? "${serverUri.scheme}://${serverUri.host}:${serverUri.port}"
|
||||
: "${serverUri.scheme}://${serverUri.host}";
|
||||
return Uri.decodeFull(
|
||||
serverUri.hasPort
|
||||
? "${serverUri.scheme}://${serverUri.host}:${serverUri.port}"
|
||||
: "${serverUri.scheme}://${serverUri.host}",
|
||||
);
|
||||
}
|
||||
|
||||
/// Converts a Unicode URL to its ASCII-compatible encoding (Punycode).
|
||||
///
|
||||
/// This is especially useful for internationalized domain names (IDNs),
|
||||
/// where parts of the URL (typically the host) contain non-ASCII characters.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final encodedUrl = punycodeEncodeUrl('https://bücher.de');
|
||||
/// print(encodedUrl); // Outputs: https://xn--bcher-kva.de
|
||||
/// ```
|
||||
///
|
||||
/// Notes:
|
||||
/// - If the input URL is invalid, an empty string is returned.
|
||||
/// - Only the host part of the URL is converted to Punycode; the scheme,
|
||||
/// path, and port remain unchanged.
|
||||
///
|
||||
String punycodeEncodeUrl(String serverUrl) {
|
||||
final serverUri = Uri.tryParse(serverUrl);
|
||||
if (serverUri == null || serverUri.host.isEmpty) return '';
|
||||
|
||||
final encodedHost = Uri.decodeComponent(serverUri.host).split('.').map(
|
||||
(segment) {
|
||||
// If segment is already ASCII, then return as it is.
|
||||
if (segment.runes.every((c) => c < 0x80)) return segment;
|
||||
return 'xn--${punycodeEncode(segment)}';
|
||||
},
|
||||
).join('.');
|
||||
|
||||
return serverUri.replace(host: encodedHost).toString();
|
||||
}
|
||||
|
||||
/// Decodes an ASCII-compatible (Punycode) URL back to its original Unicode representation.
|
||||
///
|
||||
/// This method is useful for converting internationalized domain names (IDNs)
|
||||
/// that were previously encoded with Punycode back to their human-readable Unicode form.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final decodedUrl = punycodeDecodeUrl('https://xn--bcher-kva.de');
|
||||
/// print(decodedUrl); // Outputs: https://bücher.de
|
||||
/// ```
|
||||
///
|
||||
/// Notes:
|
||||
/// - If the input URL is invalid the method returns `null`.
|
||||
/// - Only the host part of the URL is decoded. The scheme and port (if any) are preserved.
|
||||
/// - The method assumes that the input URL only contains: scheme, host, port (optional).
|
||||
/// - Query parameters, fragments, and user info are not handled (by design, as per constraints).
|
||||
///
|
||||
String? punycodeDecodeUrl(String? serverUrl) {
|
||||
final serverUri = serverUrl != null ? Uri.tryParse(serverUrl) : null;
|
||||
if (serverUri == null || serverUri.host.isEmpty) return null;
|
||||
|
||||
final decodedHost = serverUri.host.split('.').map(
|
||||
(segment) {
|
||||
if (segment.toLowerCase().startsWith('xn--')) {
|
||||
return punycodeDecode(segment.substring(4));
|
||||
}
|
||||
// If segment is not punycode encoded, then return as it is.
|
||||
return segment;
|
||||
},
|
||||
).join('.');
|
||||
|
||||
return Uri.decodeFull(serverUri.replace(host: decodedHost).toString());
|
||||
}
|
||||
|
||||
@@ -8,25 +8,25 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/control_bottom_app_bar.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import 'asset_grid_data_structure.dart';
|
||||
@@ -107,6 +107,8 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||
final Set<Asset> _draggedAssets =
|
||||
HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
|
||||
|
||||
ScrollPhysics? _scrollPhysics;
|
||||
|
||||
Set<Asset> _getSelectedAssets() {
|
||||
return Set.from(_selectedAssets);
|
||||
}
|
||||
@@ -265,6 +267,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||
),
|
||||
itemBuilder: _itemBuilder,
|
||||
itemPositionsListener: _itemPositionsListener,
|
||||
physics: _scrollPhysics,
|
||||
itemScrollController: _itemScrollController,
|
||||
scrollOffsetController: _scrollOffsetController,
|
||||
itemCount: widget.renderList.elements.length +
|
||||
@@ -439,6 +442,7 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||
|
||||
void _setDragStartIndex(AssetIndex index) {
|
||||
setState(() {
|
||||
_scrollPhysics = const ClampingScrollPhysics();
|
||||
_dragAnchorAssetIndex = index.rowIndex;
|
||||
_dragAnchorSectionIndex = index.sectionIndex;
|
||||
_dragging = true;
|
||||
@@ -446,6 +450,12 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||
}
|
||||
|
||||
void _stopDrag() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Update the physics post frame to prevent sudden change in physics on iOS.
|
||||
setState(() {
|
||||
_scrollPhysics = null;
|
||||
});
|
||||
});
|
||||
setState(() {
|
||||
_dragging = false;
|
||||
_draggedAssets.clear();
|
||||
|
||||
@@ -34,17 +34,24 @@ class DescriptionInput extends HookConsumerWidget {
|
||||
final owner = ref.watch(currentUserProvider);
|
||||
final hasError = useState(false);
|
||||
final assetWithExif = ref.watch(assetDetailProvider(asset));
|
||||
final hasDescription = useState(false);
|
||||
final isOwner = fastHash(owner?.id ?? '') == asset.ownerId;
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
assetService
|
||||
.getDescription(asset)
|
||||
.then((value) => controller.text = value);
|
||||
assetService.getDescription(asset).then((value) {
|
||||
controller.text = value;
|
||||
hasDescription.value = value.isNotEmpty;
|
||||
});
|
||||
return null;
|
||||
},
|
||||
[assetWithExif.value],
|
||||
);
|
||||
|
||||
if (!isOwner && !hasDescription.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
submitDescription(String description) async {
|
||||
hasError.value = false;
|
||||
try {
|
||||
@@ -82,7 +89,7 @@ class DescriptionInput extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return TextField(
|
||||
enabled: fastHash(owner?.id ?? '') == asset.ownerId,
|
||||
enabled: isOwner,
|
||||
focusNode: focusNode,
|
||||
onTap: () => isFocus.value = true,
|
||||
onChanged: (value) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
|
||||
class TopControlAppBar extends HookConsumerWidget {
|
||||
const TopControlAppBar({
|
||||
@@ -166,6 +167,9 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
bool isInHomePage = ref.read(tabProvider.notifier).state == TabEnum.home;
|
||||
bool? isInTrash = ref.read(currentAssetProvider)?.isTrashed;
|
||||
|
||||
return AppBar(
|
||||
foregroundColor: Colors.grey[100],
|
||||
backgroundColor: Colors.transparent,
|
||||
@@ -174,7 +178,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
shape: const Border(),
|
||||
actions: [
|
||||
if (asset.isRemote && isOwner) buildFavoriteButton(a),
|
||||
if (isOwner && ref.read(tabProvider.notifier).state != TabEnum.home)
|
||||
if (isOwner && !isInHomePage && !(isInTrash ?? false))
|
||||
buildLocateButton(),
|
||||
if (asset.livePhotoVideoId != null) const MotionPhotoButton(),
|
||||
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -7,18 +8,18 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/oauth.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/oauth.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/provider_utils.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
import 'package:immich_mobile/utils/version_compatibility.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_logo.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
import 'package:immich_mobile/widgets/forms/login/email_input.dart';
|
||||
import 'package:immich_mobile/widgets/forms/login/loading_icon.dart';
|
||||
import 'package:immich_mobile/widgets/forms/login/login_button.dart';
|
||||
@@ -82,7 +83,8 @@ class LoginForm extends HookConsumerWidget {
|
||||
/// Fetch the server login credential and enables oAuth login if necessary
|
||||
/// Returns true if successful, false otherwise
|
||||
Future<void> getServerAuthSettings() async {
|
||||
final serverUrl = sanitizeUrl(serverEndpointController.text);
|
||||
final sanitizeServerUrl = sanitizeUrl(serverEndpointController.text);
|
||||
final serverUrl = punycodeEncodeUrl(sanitizeServerUrl);
|
||||
|
||||
// Guard empty URL
|
||||
if (serverUrl.isEmpty) {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||
@@ -25,6 +27,8 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||
|
||||
final advancedTroubleshooting =
|
||||
useAppSettingsState(AppSettingsEnum.advancedTroubleshooting);
|
||||
final manageLocalMediaAndroid =
|
||||
useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid);
|
||||
final levelId = useAppSettingsState(AppSettingsEnum.logLevel);
|
||||
final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage);
|
||||
final allowSelfSignedSSLCert =
|
||||
@@ -40,6 +44,16 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||
LogService.I.setlogLevel(Level.LEVELS[levelId.value].toLogLevel()),
|
||||
);
|
||||
|
||||
Future<bool> checkAndroidVersion() async {
|
||||
if (Platform.isAndroid) {
|
||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
int sdkVersion = androidInfo.version.sdkInt;
|
||||
return sdkVersion >= 30;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
final advancedSettings = [
|
||||
SettingsSwitchListTile(
|
||||
enabled: true,
|
||||
@@ -47,6 +61,29 @@ class AdvancedSettings extends HookConsumerWidget {
|
||||
title: "advanced_settings_troubleshooting_title".tr(),
|
||||
subtitle: "advanced_settings_troubleshooting_subtitle".tr(),
|
||||
),
|
||||
FutureBuilder<bool>(
|
||||
future: checkAndroidVersion(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data == true) {
|
||||
return SettingsSwitchListTile(
|
||||
enabled: true,
|
||||
valueNotifier: manageLocalMediaAndroid,
|
||||
title: "advanced_settings_sync_remote_deletions_title".tr(),
|
||||
subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(),
|
||||
onChanged: (value) async {
|
||||
if (value) {
|
||||
final result = await ref
|
||||
.read(localFilesManagerRepositoryProvider)
|
||||
.requestManageStoragePermission();
|
||||
manageLocalMediaAndroid.value = result;
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
),
|
||||
SettingsSliderListTile(
|
||||
text: "advanced_settings_log_level_title".tr(args: [logLevel]),
|
||||
valueNotifier: levelId,
|
||||
|
||||
@@ -51,6 +51,7 @@ dependencies:
|
||||
permission_handler: ^11.4.0
|
||||
photo_manager: ^3.6.4
|
||||
photo_manager_image_provider: ^2.2.0
|
||||
punycode: ^1.0.0
|
||||
riverpod_annotation: ^2.6.1
|
||||
scrollable_positioned_list: ^0.3.8
|
||||
share_handler: ^0.0.22
|
||||
|
||||
@@ -60,6 +60,9 @@ void main() {
|
||||
final MockAlbumMediaRepository albumMediaRepository =
|
||||
MockAlbumMediaRepository();
|
||||
final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository();
|
||||
final MockAppSettingService appSettingService = MockAppSettingService();
|
||||
final MockLocalFilesManagerRepository localFilesManagerRepository =
|
||||
MockLocalFilesManagerRepository();
|
||||
final MockPartnerApiRepository partnerApiRepository =
|
||||
MockPartnerApiRepository();
|
||||
final MockUserApiRepository userApiRepository = MockUserApiRepository();
|
||||
@@ -106,6 +109,8 @@ void main() {
|
||||
userRepository,
|
||||
userService,
|
||||
eTagRepository,
|
||||
appSettingService,
|
||||
localFilesManagerRepository,
|
||||
partnerApiRepository,
|
||||
userApiRepository,
|
||||
);
|
||||
|
||||
138
mobile/test/modules/utils/url_helper_test.dart
Normal file
138
mobile/test/modules/utils/url_helper_test.dart
Normal file
@@ -0,0 +1,138 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
|
||||
void main() {
|
||||
group('punycodeEncodeUrl', () {
|
||||
test('should return empty string for invalid URL', () {
|
||||
expect(punycodeEncodeUrl('not a url'), equals(''));
|
||||
});
|
||||
|
||||
test('should handle empty input', () {
|
||||
expect(punycodeEncodeUrl(''), equals(''));
|
||||
});
|
||||
|
||||
test('should return ASCII-only URL unchanged', () {
|
||||
const url = 'https://example.com';
|
||||
expect(punycodeEncodeUrl(url), equals(url));
|
||||
});
|
||||
|
||||
test('should encode single-segment Unicode host', () {
|
||||
const url = 'https://bücher';
|
||||
const expected = 'https://xn--bcher-kva';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should encode multi-segment Unicode host', () {
|
||||
const url = 'https://bücher.de';
|
||||
const expected = 'https://xn--bcher-kva.de';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test(
|
||||
'should encode multi-segment Unicode host with multiple non-ASCII segments',
|
||||
() {
|
||||
const url = 'https://bücher.münchen';
|
||||
const expected = 'https://xn--bcher-kva.xn--mnchen-3ya';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle URL with port', () {
|
||||
const url = 'https://bücher.de:8080';
|
||||
const expected = 'https://xn--bcher-kva.de:8080';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle URL with path', () {
|
||||
const url = 'https://bücher.de/path/to/resource';
|
||||
const expected = 'https://xn--bcher-kva.de/path/to/resource';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle URL with port and path', () {
|
||||
const url = 'https://bücher.de:3000/path';
|
||||
const expected = 'https://xn--bcher-kva.de:3000/path';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should not encode ASCII segment in multi-segment host', () {
|
||||
const url = 'https://shop.bücher.de';
|
||||
const expected = 'https://shop.xn--bcher-kva.de';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle host with hyphen in Unicode segment', () {
|
||||
const url = 'https://bü-cher.de';
|
||||
const expected = 'https://xn--b-cher-3ya.de';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle host with numbers in Unicode segment', () {
|
||||
const url = 'https://bücher123.de';
|
||||
const expected = 'https://xn--bcher123-65a.de';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should encode the domain of the original issue poster :)', () {
|
||||
const url = 'https://фото.большойчлен.рф/';
|
||||
const expected = 'https://xn--n1aalg.xn--90ailhbncb6fh7b.xn--p1ai/';
|
||||
expect(punycodeEncodeUrl(url), expected);
|
||||
});
|
||||
});
|
||||
|
||||
group('punycodeDecodeUrl', () {
|
||||
test('should return null for null input', () {
|
||||
expect(punycodeDecodeUrl(null), isNull);
|
||||
});
|
||||
|
||||
test('should return null for an invalid URL', () {
|
||||
// "not a url" should fail to parse.
|
||||
expect(punycodeDecodeUrl('not a url'), isNull);
|
||||
});
|
||||
|
||||
test('should return null for a URL with empty host', () {
|
||||
// "https://" is a valid scheme but with no host.
|
||||
expect(punycodeDecodeUrl('https://'), isNull);
|
||||
});
|
||||
|
||||
test('should return ASCII-only URL unchanged', () {
|
||||
const url = 'https://example.com';
|
||||
expect(punycodeDecodeUrl(url), equals(url));
|
||||
});
|
||||
|
||||
test('should decode a single-segment Punycode domain', () {
|
||||
const input = 'https://xn--bcher-kva.de';
|
||||
const expected = 'https://bücher.de';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should decode a multi-segment Punycode domain', () {
|
||||
const input = 'https://shop.xn--bcher-kva.de';
|
||||
const expected = 'https://shop.bücher.de';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should decode URL with port', () {
|
||||
const input = 'https://xn--bcher-kva.de:8080';
|
||||
const expected = 'https://bücher.de:8080';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should decode domains with uppercase punycode prefix correctly', () {
|
||||
const input = 'https://XN--BCHER-KVA.de';
|
||||
const expected = 'https://bücher.de';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle mixed segments with no punycode in some parts', () {
|
||||
const input = 'https://news.xn--bcher-kva.de';
|
||||
const expected = 'https://news.bücher.de';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should decode the domain of the original issue poster :)', () {
|
||||
const url = 'https://xn--n1aalg.xn--90ailhbncb6fh7b.xn--p1ai/';
|
||||
const expected = 'https://фото.большойчлен.рф/';
|
||||
expect(punycodeDecodeUrl(url), expected);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/interfaces/auth_api.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/partner.interface.dart';
|
||||
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
@@ -41,6 +42,9 @@ class MockAuthApiRepository extends Mock implements IAuthApiRepository {}
|
||||
|
||||
class MockAuthRepository extends Mock implements IAuthRepository {}
|
||||
|
||||
class MockPartnerRepository extends Mock implements IPartnerRepository {}
|
||||
|
||||
class MockPartnerApiRepository extends Mock implements IPartnerApiRepository {}
|
||||
|
||||
class MockPartnerRepository extends Mock implements IPartnerRepository {}
|
||||
class MockLocalFilesManagerRepository extends Mock
|
||||
implements ILocalFilesManager {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:immich_mobile/services/backup.service.dart';
|
||||
import 'package:immich_mobile/services/entity.service.dart';
|
||||
@@ -25,4 +26,7 @@ class MockNetworkService extends Mock implements NetworkService {}
|
||||
|
||||
class MockSearchApi extends Mock implements SearchApi {}
|
||||
|
||||
class MockAppSettingService extends Mock implements AppSettingsService {}
|
||||
|
||||
class MockBackgroundService extends Mock implements BackgroundService {}
|
||||
|
||||
|
||||
16
open-api/typescript-sdk/package-lock.json
generated
16
open-api/typescript-sdk/package-lock.json
generated
@@ -12,7 +12,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -23,13 +23,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.15.tgz",
|
||||
"integrity": "sha512-imAbQEEbVni6i6h6Bd5xkCRwLqFc8hihCsi2GbtDoAtUcAFQ6Zs4pFXTZUUbroTkXdImczWM9AI8eZUuybXE3w==",
|
||||
"version": "22.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
|
||||
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
@@ -47,9 +47,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
@@ -4,6 +4,7 @@ FROM ghcr.io/immich-app/base-server-dev:202503251114@sha256:10e8973e8603c5729436
|
||||
RUN apt-get install --no-install-recommends -yqq tini
|
||||
WORKDIR /usr/src/app
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
COPY server/patches ./patches
|
||||
RUN npm ci && \
|
||||
# exiftool-vendored.pl, sharp-linux-x64 and sharp-linux-arm64 are the only ones we need
|
||||
# they're marked as optional dependencies, so we need to copy them manually after pruning
|
||||
@@ -56,7 +57,7 @@ COPY server/resources resources
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
COPY server/start*.sh ./
|
||||
COPY "docker/scripts/get-cpus.sh" ./
|
||||
RUN npm link && npm install -g @immich/cli && npm cache clean --force
|
||||
RUN npm install -g @immich/cli && npm cache clean --force
|
||||
COPY LICENSE /licenses/LICENSE.txt
|
||||
COPY LICENSE /LICENSE
|
||||
ENV PATH="${PATH}:/usr/src/app/bin"
|
||||
|
||||
821
server/package-lock.json
generated
821
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -25,15 +25,15 @@
|
||||
"lifecycle": "node ./dist/utils/lifecycle.js",
|
||||
"migrations:generate": "node ./dist/bin/migrations.js generate",
|
||||
"migrations:create": "node ./dist/bin/migrations.js create",
|
||||
"typeorm:migrations:run": "typeorm migration:run -d ./dist/bin/database.js",
|
||||
"migrations:run": "node ./dist/bin/migrations.js run",
|
||||
"typeorm:migrations:revert": "typeorm migration:revert -d ./dist/bin/database.js",
|
||||
"typeorm:schema:drop": "typeorm query -d ./dist/bin/database.js 'DROP schema public cascade; CREATE schema public;'",
|
||||
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run",
|
||||
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run migrations:run",
|
||||
"kysely:codegen": "npx kysely-codegen --include-pattern=\"(public|vectors).*\" --dialect postgres --url postgres://postgres:postgres@localhost/immich --log-level debug --out-file=./src/db.d.ts",
|
||||
"sync:open-api": "node ./dist/bin/sync-open-api.js",
|
||||
"sync:sql": "node ./dist/bin/sync-sql.js",
|
||||
"email:dev": "email dev -p 3050 --dir src/emails",
|
||||
"postinstall": "[ \"$npm_config_global\" != \"true\" ] && patch-package || true"
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/bullmq": "^11.0.1",
|
||||
@@ -49,7 +49,7 @@
|
||||
"@opentelemetry/context-async-hooks": "^2.0.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.200.0",
|
||||
"@opentelemetry/sdk-node": "^0.200.0",
|
||||
"@react-email/components": "^0.0.34",
|
||||
"@react-email/components": "^0.0.35",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"archiver": "^7.0.0",
|
||||
"async-lock": "^1.4.0",
|
||||
@@ -116,7 +116,7 @@
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^3.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
|
||||
@@ -1,39 +1,48 @@
|
||||
diff --git a/node_modules/postgres/cf/src/connection.js b/node_modules/postgres/cf/src/connection.js
|
||||
index ee8b1e6..d03b9dd 100644
|
||||
index ee8b1e6..acf4566 100644
|
||||
--- a/node_modules/postgres/cf/src/connection.js
|
||||
+++ b/node_modules/postgres/cf/src/connection.js
|
||||
@@ -387,6 +387,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
||||
@@ -387,8 +387,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
||||
}
|
||||
|
||||
function queryError(query, err) {
|
||||
+ if (!query || typeof query !== 'object') throw err
|
||||
+ if (!query || typeof query !== 'object' || !query.reject) throw err
|
||||
+
|
||||
'query' in err || 'parameters' in err || Object.defineProperties(err, {
|
||||
stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug },
|
||||
- stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug },
|
||||
+ stack: { value: err.stack + (query.origin || '').replace(/.*\n/, '\n'), enumerable: options.debug },
|
||||
query: { value: query.string, enumerable: options.debug },
|
||||
parameters: { value: query.parameters, enumerable: options.debug },
|
||||
args: { value: query.args, enumerable: options.debug },
|
||||
diff --git a/node_modules/postgres/cjs/src/connection.js b/node_modules/postgres/cjs/src/connection.js
|
||||
index f7f58d1..8a37571 100644
|
||||
index f7f58d1..b7f2d65 100644
|
||||
--- a/node_modules/postgres/cjs/src/connection.js
|
||||
+++ b/node_modules/postgres/cjs/src/connection.js
|
||||
@@ -385,6 +385,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
||||
@@ -385,8 +385,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
||||
}
|
||||
|
||||
function queryError(query, err) {
|
||||
+ if (!query || typeof query !== 'object') throw err
|
||||
+ if (!query || typeof query !== 'object' || !query.reject) throw err
|
||||
+
|
||||
'query' in err || 'parameters' in err || Object.defineProperties(err, {
|
||||
stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug },
|
||||
- stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug },
|
||||
+ stack: { value: err.stack + (query.origin || '').replace(/.*\n/, '\n'), enumerable: options.debug },
|
||||
query: { value: query.string, enumerable: options.debug },
|
||||
parameters: { value: query.parameters, enumerable: options.debug },
|
||||
args: { value: query.args, enumerable: options.debug },
|
||||
diff --git a/node_modules/postgres/src/connection.js b/node_modules/postgres/src/connection.js
|
||||
index 97cc97e..58f5298 100644
|
||||
index 97cc97e..26f508e 100644
|
||||
--- a/node_modules/postgres/src/connection.js
|
||||
+++ b/node_modules/postgres/src/connection.js
|
||||
@@ -385,6 +385,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
||||
@@ -385,8 +385,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
|
||||
}
|
||||
|
||||
function queryError(query, err) {
|
||||
+ if (!query || typeof query !== 'object') throw err
|
||||
+ if (!query || typeof query !== 'object' || !query.reject) throw err
|
||||
+
|
||||
'query' in err || 'parameters' in err || Object.defineProperties(err, {
|
||||
stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug },
|
||||
- stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug },
|
||||
+ stack: { value: err.stack + (query.origin || '').replace(/.*\n/, '\n'), enumerable: options.debug },
|
||||
query: { value: query.string, enumerable: options.debug },
|
||||
parameters: { value: query.parameters, enumerable: options.debug },
|
||||
args: { value: query.args, enumerable: options.debug },
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
process.env.DB_URL = 'postgres://postgres:postgres@localhost:5432/immich';
|
||||
process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich';
|
||||
|
||||
import { Kysely } from 'kysely';
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { basename, dirname, extname, join } from 'node:path';
|
||||
import postgres from 'postgres';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import 'src/schema/tables';
|
||||
import { DatabaseTable, schemaDiff, schemaFromDatabase, schemaFromDecorators } from 'src/sql-tools';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import 'src/schema';
|
||||
import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools';
|
||||
|
||||
const main = async () => {
|
||||
const command = process.argv[2];
|
||||
const name = process.argv[3] || 'Migration';
|
||||
const path = process.argv[3] || 'src/Migration';
|
||||
|
||||
switch (command) {
|
||||
case 'debug': {
|
||||
@@ -17,13 +22,19 @@ const main = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
case 'run': {
|
||||
const only = process.argv[3] as 'kysely' | 'typeorm' | undefined;
|
||||
await run(only);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'create': {
|
||||
create(name, [], []);
|
||||
create(path, [], []);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'generate': {
|
||||
await generate(name);
|
||||
await generate(path);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,32 +42,57 @@ const main = async () => {
|
||||
console.log(`Usage:
|
||||
node dist/bin/migrations.js create <name>
|
||||
node dist/bin/migrations.js generate <name>
|
||||
node dist/bin/migrations.js run
|
||||
`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const run = async (only?: 'kysely' | 'typeorm') => {
|
||||
const configRepository = new ConfigRepository();
|
||||
const { database } = configRepository.getEnv();
|
||||
const logger = new LoggingRepository(undefined, configRepository);
|
||||
const db = new Kysely<any>({
|
||||
dialect: new PostgresJSDialect({ postgres: postgres(database.config.kysely) }),
|
||||
log(event) {
|
||||
if (event.level === 'error') {
|
||||
console.error('Query failed :', {
|
||||
durationMs: event.queryDurationMillis,
|
||||
error: event.error,
|
||||
sql: event.query.sql,
|
||||
params: event.query.parameters,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
const databaseRepository = new DatabaseRepository(db, logger, configRepository);
|
||||
|
||||
await databaseRepository.runMigrations({ only });
|
||||
};
|
||||
|
||||
const debug = async () => {
|
||||
const { up, down } = await compare();
|
||||
const { up } = await compare();
|
||||
const upSql = '-- UP\n' + up.asSql({ comments: true }).join('\n');
|
||||
const downSql = '-- DOWN\n' + down.asSql({ comments: true }).join('\n');
|
||||
writeFileSync('./migrations.sql', upSql + '\n\n' + downSql);
|
||||
// const downSql = '-- DOWN\n' + down.asSql({ comments: true }).join('\n');
|
||||
writeFileSync('./migrations.sql', upSql + '\n\n');
|
||||
console.log('Wrote migrations.sql');
|
||||
};
|
||||
|
||||
const generate = async (name: string) => {
|
||||
const generate = async (path: string) => {
|
||||
const { up, down } = await compare();
|
||||
if (up.items.length === 0) {
|
||||
console.log('No changes detected');
|
||||
return;
|
||||
}
|
||||
create(name, up.asSql(), down.asSql());
|
||||
create(path, up.asSql(), down.asSql());
|
||||
};
|
||||
|
||||
const create = (name: string, up: string[], down: string[]) => {
|
||||
const create = (path: string, up: string[], down: string[]) => {
|
||||
const timestamp = Date.now();
|
||||
const name = basename(path, extname(path));
|
||||
const filename = `${timestamp}-${name}.ts`;
|
||||
const fullPath = `./src/${filename}`;
|
||||
const folder = dirname(path);
|
||||
const fullPath = join(folder, filename);
|
||||
writeFileSync(fullPath, asMigration('kysely', { name, timestamp, up, down }));
|
||||
console.log(`Wrote ${fullPath}`);
|
||||
};
|
||||
@@ -66,16 +102,25 @@ const compare = async () => {
|
||||
const { database } = configRepository.getEnv();
|
||||
const db = postgres(database.config.kysely);
|
||||
|
||||
const source = schemaFromDecorators();
|
||||
const source = schemaFromCode();
|
||||
const target = await schemaFromDatabase(db, {});
|
||||
|
||||
const sourceParams = new Set(source.parameters.map(({ name }) => name));
|
||||
target.parameters = target.parameters.filter(({ name }) => sourceParams.has(name));
|
||||
|
||||
const sourceTables = new Set(source.tables.map(({ name }) => name));
|
||||
target.tables = target.tables.filter(({ name }) => sourceTables.has(name));
|
||||
|
||||
console.log(source.warnings.join('\n'));
|
||||
|
||||
const isIncluded = (table: DatabaseTable) => source.tables.some(({ name }) => table.name === name);
|
||||
target.tables = target.tables.filter((table) => isIncluded(table));
|
||||
|
||||
const up = schemaDiff(source, target, { ignoreExtraTables: true });
|
||||
const down = schemaDiff(target, source, { ignoreExtraTables: false });
|
||||
const up = schemaDiff(source, target, {
|
||||
tables: { ignoreExtra: true },
|
||||
functions: { ignoreExtra: false },
|
||||
});
|
||||
const down = schemaDiff(target, source, {
|
||||
tables: { ignoreExtra: false },
|
||||
functions: { ignoreExtra: false },
|
||||
});
|
||||
|
||||
return { up, down };
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||
import { AssetStatus, AssetType, Permission, UserStatus } from 'src/enum';
|
||||
import { Selectable } from 'kysely';
|
||||
import { Exif as DatabaseExif } from 'src/db';
|
||||
import { AlbumUserRole, AssetFileType, AssetStatus, AssetType, MemoryType, Permission, UserStatus } from 'src/enum';
|
||||
import { OnThisDayData, UserMetadataItem } from 'src/types';
|
||||
|
||||
export type AuthUser = {
|
||||
id: string;
|
||||
@@ -10,6 +12,17 @@ export type AuthUser = {
|
||||
quotaSizeInBytes: number | null;
|
||||
};
|
||||
|
||||
export type AlbumUser = {
|
||||
user: User;
|
||||
role: AlbumUserRole;
|
||||
};
|
||||
|
||||
export type AssetFile = {
|
||||
id: string;
|
||||
type: AssetFileType;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type Library = {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
@@ -29,6 +42,19 @@ export type AuthApiKey = {
|
||||
permissions: Permission[];
|
||||
};
|
||||
|
||||
export type Activity = {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
albumId: string;
|
||||
userId: string;
|
||||
user: User;
|
||||
assetId: string | null;
|
||||
comment: string | null;
|
||||
isLiked: boolean;
|
||||
updateId: string;
|
||||
};
|
||||
|
||||
export type ApiKey = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -38,6 +64,31 @@ export type ApiKey = {
|
||||
permissions: Permission[];
|
||||
};
|
||||
|
||||
export type Tag = {
|
||||
id: string;
|
||||
value: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
color: string | null;
|
||||
parentId: string | null;
|
||||
};
|
||||
|
||||
export type Memory = {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt: Date | null;
|
||||
memoryAt: Date;
|
||||
seenAt: Date | null;
|
||||
showAt: Date | null;
|
||||
hideAt: Date | null;
|
||||
type: MemoryType;
|
||||
data: OnThisDayData;
|
||||
ownerId: string;
|
||||
isSaved: boolean;
|
||||
assets: Asset[];
|
||||
};
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -57,7 +108,7 @@ export type UserAdmin = User & {
|
||||
quotaSizeInBytes: number | null;
|
||||
quotaUsageInBytes: number;
|
||||
status: UserStatus;
|
||||
metadata: UserMetadataEntity[];
|
||||
metadata: UserMetadataItem[];
|
||||
};
|
||||
|
||||
export type Asset = {
|
||||
@@ -92,6 +143,13 @@ export type Asset = {
|
||||
type: AssetType;
|
||||
};
|
||||
|
||||
export type SidecarWriteAsset = {
|
||||
id: string;
|
||||
sidecarPath: string | null;
|
||||
originalPath: string;
|
||||
tags: Array<{ value: string }>;
|
||||
};
|
||||
|
||||
export type AuthSharedLink = {
|
||||
id: string;
|
||||
expiresAt: Date | null;
|
||||
@@ -117,6 +175,30 @@ export type Partner = {
|
||||
inTimeline: boolean;
|
||||
};
|
||||
|
||||
export type Place = {
|
||||
admin1Code: string | null;
|
||||
admin1Name: string | null;
|
||||
admin2Code: string | null;
|
||||
admin2Name: string | null;
|
||||
alternateNames: string | null;
|
||||
countryCode: string;
|
||||
id: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
modificationDate: Date;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Session = {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deviceOS: string;
|
||||
deviceType: string;
|
||||
};
|
||||
|
||||
export type Exif = Omit<Selectable<DatabaseExif>, 'updatedAt' | 'updateId'>;
|
||||
|
||||
const userColumns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const;
|
||||
|
||||
export const columns = {
|
||||
@@ -140,6 +222,7 @@ export const columns = {
|
||||
'shared_links.password',
|
||||
],
|
||||
user: userColumns,
|
||||
userWithPrefix: ['users.id', 'users.name', 'users.email', 'users.profileImagePath', 'users.profileChangedAt'],
|
||||
userAdmin: [
|
||||
...userColumns,
|
||||
'createdAt',
|
||||
|
||||
35
server/src/db.d.ts
vendored
35
server/src/db.d.ts
vendored
@@ -4,16 +4,25 @@
|
||||
*/
|
||||
|
||||
import type { ColumnType } from 'kysely';
|
||||
import { AssetType, MemoryType, Permission, SyncEntityType } from 'src/enum';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
AssetFileType,
|
||||
AssetOrder,
|
||||
AssetStatus,
|
||||
AssetType,
|
||||
MemoryType,
|
||||
Permission,
|
||||
SharedLinkType,
|
||||
SourceType,
|
||||
SyncEntityType,
|
||||
} from 'src/enum';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import { OnThisDayData } from 'src/types';
|
||||
import { OnThisDayData, UserMetadataItem } from 'src/types';
|
||||
|
||||
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 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>;
|
||||
|
||||
@@ -31,8 +40,6 @@ export type JsonPrimitive = boolean | number | string | null;
|
||||
|
||||
export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
|
||||
|
||||
export type Sourcetype = 'exif' | 'machine-learning' | 'manual';
|
||||
|
||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||
|
||||
export interface Activity {
|
||||
@@ -58,7 +65,7 @@ export interface Albums {
|
||||
description: Generated<string>;
|
||||
id: Generated<string>;
|
||||
isActivityEnabled: Generated<boolean>;
|
||||
order: Generated<string>;
|
||||
order: Generated<AssetOrder>;
|
||||
ownerId: string;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
updateId: Generated<string>;
|
||||
@@ -72,7 +79,7 @@ export interface AlbumsAssetsAssets {
|
||||
|
||||
export interface AlbumsSharedUsersUsers {
|
||||
albumsId: string;
|
||||
role: Generated<string>;
|
||||
role: Generated<AlbumUserRole>;
|
||||
usersId: string;
|
||||
}
|
||||
|
||||
@@ -98,7 +105,7 @@ export interface AssetFaces {
|
||||
imageHeight: Generated<number>;
|
||||
imageWidth: Generated<number>;
|
||||
personId: string | null;
|
||||
sourceType: Generated<Sourcetype>;
|
||||
sourceType: Generated<SourceType>;
|
||||
}
|
||||
|
||||
export interface AssetFiles {
|
||||
@@ -106,7 +113,7 @@ export interface AssetFiles {
|
||||
createdAt: Generated<Timestamp>;
|
||||
id: Generated<string>;
|
||||
path: string;
|
||||
type: string;
|
||||
type: AssetFileType;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
updateId: Generated<string>;
|
||||
}
|
||||
@@ -152,7 +159,7 @@ export interface Assets {
|
||||
ownerId: string;
|
||||
sidecarPath: string | null;
|
||||
stackId: string | null;
|
||||
status: Generated<AssetsStatusEnum>;
|
||||
status: Generated<AssetStatus>;
|
||||
thumbhash: Buffer | null;
|
||||
type: AssetType;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
@@ -350,7 +357,7 @@ export interface SharedLinks {
|
||||
key: Buffer;
|
||||
password: string | null;
|
||||
showExif: Generated<boolean>;
|
||||
type: string;
|
||||
type: SharedLinkType;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
@@ -405,10 +412,8 @@ export interface TypeormMetadata {
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
export interface UserMetadata {
|
||||
key: string;
|
||||
export interface UserMetadata extends UserMetadataItem {
|
||||
userId: string;
|
||||
value: Json;
|
||||
}
|
||||
|
||||
export interface UsersAudit {
|
||||
|
||||
@@ -4,8 +4,24 @@ import _ from 'lodash';
|
||||
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
||||
import { ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum';
|
||||
import { EmitEvent } from 'src/repositories/event.repository';
|
||||
import { immich_uuid_v7, updated_at } from 'src/schema/functions';
|
||||
import { BeforeUpdateTrigger, Column, ColumnOptions } from 'src/sql-tools';
|
||||
import { setUnion } from 'src/utils/set';
|
||||
|
||||
const GeneratedUuidV7Column = (options: Omit<ColumnOptions, 'type' | 'default' | 'nullable'> = {}) =>
|
||||
Column({ ...options, type: 'uuid', nullable: false, default: () => `${immich_uuid_v7.name}()` });
|
||||
|
||||
export const UpdateIdColumn = () => GeneratedUuidV7Column();
|
||||
|
||||
export const PrimaryGeneratedUuidV7Column = () => GeneratedUuidV7Column({ primary: true });
|
||||
|
||||
export const UpdatedAtTrigger = (name: string) =>
|
||||
BeforeUpdateTrigger({
|
||||
name,
|
||||
scope: 'row',
|
||||
function: updated_at,
|
||||
});
|
||||
|
||||
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
|
||||
// maximum number of parameters is 65535. Any query that tries to bind more than that (e.g. searching
|
||||
// by a list of IDs) requires splitting the query into multiple chunks.
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator';
|
||||
import { Activity } from 'src/database';
|
||||
import { mapUser, UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { ActivityItem } from 'src/types';
|
||||
import { Optional, ValidateUUID } from 'src/validation';
|
||||
|
||||
export enum ReactionType {
|
||||
@@ -68,13 +67,13 @@ export class ActivityCreateDto extends ActivityDto {
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export const mapActivity = (activity: ActivityItem): ActivityResponseDto => {
|
||||
export const mapActivity = (activity: Activity): ActivityResponseDto => {
|
||||
return {
|
||||
id: activity.id,
|
||||
assetId: activity.assetId,
|
||||
createdAt: activity.createdAt,
|
||||
comment: activity.comment,
|
||||
type: activity.isLiked ? ReactionType.LIKE : ReactionType.COMMENT,
|
||||
user: mapUser(activity.user as unknown as UserEntity),
|
||||
user: mapUser(activity.user),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -143,13 +143,11 @@ export class AlbumResponseDto {
|
||||
}
|
||||
|
||||
export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => {
|
||||
const sharedUsers: UserResponseDto[] = [];
|
||||
const albumUsers: AlbumUserResponseDto[] = [];
|
||||
|
||||
if (entity.albumUsers) {
|
||||
for (const albumUser of entity.albumUsers) {
|
||||
const user = mapUser(albumUser.user);
|
||||
sharedUsers.push(user);
|
||||
albumUsers.push({
|
||||
user,
|
||||
role: albumUser.role,
|
||||
@@ -162,7 +160,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt
|
||||
const assets = entity.assets || [];
|
||||
|
||||
const hasSharedLink = entity.sharedLinks?.length > 0;
|
||||
const hasSharedUser = sharedUsers.length > 0;
|
||||
const hasSharedUser = albumUsers.length > 0;
|
||||
|
||||
let startDate = assets.at(0)?.localDateTime;
|
||||
let endDate = assets.at(-1)?.localDateTime;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser } from 'src/database';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database';
|
||||
import { ImmichCookie } from 'src/enum';
|
||||
import { toEmail } from 'src/validation';
|
||||
|
||||
@@ -42,7 +41,7 @@ export class LoginResponseDto {
|
||||
shouldChangePassword!: boolean;
|
||||
}
|
||||
|
||||
export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto {
|
||||
export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto {
|
||||
return {
|
||||
accessToken,
|
||||
userId: entity.id,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { Exif } from 'src/database';
|
||||
|
||||
export class ExifResponseDto {
|
||||
make?: string | null = null;
|
||||
@@ -28,7 +28,7 @@ export class ExifResponseDto {
|
||||
rating?: number | null = null;
|
||||
}
|
||||
|
||||
export function mapExif(entity: ExifEntity): ExifResponseDto {
|
||||
export function mapExif(entity: Exif): ExifResponseDto {
|
||||
return {
|
||||
make: entity.make,
|
||||
model: entity.model,
|
||||
@@ -55,7 +55,7 @@ export function mapExif(entity: ExifEntity): ExifResponseDto {
|
||||
};
|
||||
}
|
||||
|
||||
export function mapSanitizedExif(entity: ExifEntity): ExifResponseDto {
|
||||
export function mapSanitizedExif(entity: Exif): ExifResponseDto {
|
||||
return {
|
||||
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
|
||||
orientation: entity.orientation,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator';
|
||||
import { Memory } from 'src/database';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { MemoryType } from 'src/enum';
|
||||
import { MemoryItem } from 'src/types';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
|
||||
class MemoryBaseDto {
|
||||
@@ -89,7 +89,7 @@ export class MemoryResponseDto {
|
||||
assets!: AssetResponseDto[];
|
||||
}
|
||||
|
||||
export const mapMemory = (entity: MemoryItem, auth: AuthDto): MemoryResponseDto => {
|
||||
export const mapMemory = (entity: Memory, auth: AuthDto): MemoryResponseDto => {
|
||||
return {
|
||||
id: entity.id,
|
||||
createdAt: entity.createdAt,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
|
||||
import { Place } from 'src/database';
|
||||
import { PropertyLifecycle } from 'src/decorators';
|
||||
import { AlbumResponseDto } from 'src/dtos/album.dto';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||
import { AssetOrder, AssetType } from 'src/enum';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
|
||||
@@ -226,15 +226,16 @@ export class PlacesResponseDto {
|
||||
admin2name?: string;
|
||||
}
|
||||
|
||||
export function mapPlaces(place: GeodataPlacesEntity): PlacesResponseDto {
|
||||
export function mapPlaces(place: Place): PlacesResponseDto {
|
||||
return {
|
||||
name: place.name,
|
||||
latitude: place.latitude,
|
||||
longitude: place.longitude,
|
||||
admin1name: place.admin1Name,
|
||||
admin2name: place.admin2Name,
|
||||
admin1name: place.admin1Name ?? undefined,
|
||||
admin2name: place.admin2Name ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export enum SearchSuggestionType {
|
||||
COUNTRY = 'country',
|
||||
STATE = 'state',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SessionItem } from 'src/types';
|
||||
import { Session } from 'src/database';
|
||||
|
||||
export class SessionResponseDto {
|
||||
id!: string;
|
||||
@@ -9,7 +9,7 @@ export class SessionResponseDto {
|
||||
deviceOS!: string;
|
||||
}
|
||||
|
||||
export const mapSession = (entity: SessionItem, currentId?: string): SessionResponseDto => ({
|
||||
export const mapSession = (entity: Session, currentId?: string): SessionResponseDto => ({
|
||||
id: entity.id,
|
||||
createdAt: entity.createdAt.toISOString(),
|
||||
updatedAt: entity.updatedAt.toISOString(),
|
||||
|
||||
@@ -104,9 +104,6 @@ export class SharedLinkResponseDto {
|
||||
|
||||
export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
||||
const linkAssets = sharedLink.assets || [];
|
||||
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);
|
||||
|
||||
const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id);
|
||||
|
||||
return {
|
||||
id: sharedLink.id,
|
||||
@@ -117,7 +114,7 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD
|
||||
type: sharedLink.type,
|
||||
createdAt: sharedLink.createdAt,
|
||||
expiresAt: sharedLink.expiresAt,
|
||||
assets: assets.map((asset) => mapAsset(asset)),
|
||||
assets: linkAssets.map((asset) => mapAsset(asset)),
|
||||
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
allowDownload: sharedLink.allowDownload,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsHexColor, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { TagItem } from 'src/types';
|
||||
import { Tag } from 'src/database';
|
||||
import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class TagCreateDto {
|
||||
@@ -52,7 +51,7 @@ export class TagResponseDto {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function mapTag(entity: TagItem | TagEntity): TagResponseDto {
|
||||
export function mapTag(entity: Tag): TagResponseDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
parentId: entity.parentId ?? undefined,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator';
|
||||
import { UserPreferences } from 'src/entities/user-metadata.entity';
|
||||
import { UserAvatarColor } from 'src/enum';
|
||||
import { UserPreferences } from 'src/types';
|
||||
import { Optional, ValidateBoolean } from 'src/validation';
|
||||
|
||||
class AvatarUpdate {
|
||||
|
||||
@@ -2,9 +2,8 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
||||
import { User, UserAdmin } from 'src/database';
|
||||
import { UserMetadataEntity, UserMetadataItem } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
||||
import { UserMetadataItem } from 'src/types';
|
||||
import { getPreferences } from 'src/utils/preferences';
|
||||
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
|
||||
|
||||
@@ -42,13 +41,13 @@ export class UserLicense {
|
||||
activatedAt!: Date;
|
||||
}
|
||||
|
||||
export const mapUser = (entity: UserEntity | User): UserResponseDto => {
|
||||
export const mapUser = (entity: User | UserAdmin): UserResponseDto => {
|
||||
return {
|
||||
id: entity.id,
|
||||
email: entity.email,
|
||||
name: entity.name,
|
||||
profileImagePath: entity.profileImagePath,
|
||||
avatarColor: getPreferences(entity.email, (entity as UserEntity).metadata || []).avatar.color,
|
||||
avatarColor: getPreferences(entity.email, (entity as UserAdmin).metadata || []).avatar.color,
|
||||
profileChangedAt: entity.profileChangedAt,
|
||||
};
|
||||
};
|
||||
@@ -142,9 +141,10 @@ export class UserAdminResponseDto extends UserResponseDto {
|
||||
license!: UserLicense | null;
|
||||
}
|
||||
|
||||
export function mapUserAdmin(entity: UserEntity | UserAdmin): UserAdminResponseDto {
|
||||
const license = (entity.metadata as UserMetadataItem[])?.find(
|
||||
(item): item is UserMetadataEntity<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
|
||||
export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto {
|
||||
const metadata = entity.metadata || [];
|
||||
const license = metadata.find(
|
||||
(item): item is UserMetadataItem<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
|
||||
)?.value;
|
||||
return {
|
||||
...mapUser(entity),
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
|
||||
export class AlbumUserEntity {
|
||||
albumId!: string;
|
||||
userId!: string;
|
||||
album!: AlbumEntity;
|
||||
user!: UserEntity;
|
||||
role!: AlbumUserRole;
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { AlbumUserEntity } from 'src/entities/album-user.entity';
|
||||
import { AlbumUser, User } from 'src/database';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AssetOrder } from 'src/enum';
|
||||
|
||||
export class AlbumEntity {
|
||||
id!: string;
|
||||
owner!: UserEntity;
|
||||
owner!: User;
|
||||
ownerId!: string;
|
||||
albumName!: string;
|
||||
description!: string;
|
||||
@@ -16,7 +15,7 @@ export class AlbumEntity {
|
||||
deletedAt!: Date | null;
|
||||
albumThumbnailAsset!: AssetEntity | null;
|
||||
albumThumbnailAssetId!: string | null;
|
||||
albumUsers!: AlbumUserEntity[];
|
||||
albumUsers!: AlbumUser[];
|
||||
assets!: AssetEntity[];
|
||||
sharedLinks!: SharedLinkEntity[];
|
||||
isActivityEnabled!: boolean;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetFileType } from 'src/enum';
|
||||
|
||||
export class AssetFileEntity {
|
||||
id!: string;
|
||||
assetId!: string;
|
||||
asset?: AssetEntity;
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
updateId?: string;
|
||||
type!: AssetFileType;
|
||||
path!: string;
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
import { DeduplicateJoinsPlugin, ExpressionBuilder, Kysely, SelectQueryBuilder, sql } from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { AssetFile, Exif, Tag, User } from 'src/database';
|
||||
import { DB } from 'src/db';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
||||
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/repositories/asset.repository';
|
||||
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
|
||||
@@ -21,14 +17,14 @@ export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
|
||||
export class AssetEntity {
|
||||
id!: string;
|
||||
deviceAssetId!: string;
|
||||
owner!: UserEntity;
|
||||
owner!: User;
|
||||
ownerId!: string;
|
||||
libraryId?: string | null;
|
||||
deviceId!: string;
|
||||
type!: AssetType;
|
||||
status!: AssetStatus;
|
||||
originalPath!: string;
|
||||
files!: AssetFileEntity[];
|
||||
files!: AssetFile[];
|
||||
thumbhash!: Buffer | null;
|
||||
encodedVideoPath!: string | null;
|
||||
createdAt!: Date;
|
||||
@@ -49,9 +45,8 @@ export class AssetEntity {
|
||||
livePhotoVideoId!: string | null;
|
||||
originalFileName!: string;
|
||||
sidecarPath!: string | null;
|
||||
exifInfo?: ExifEntity;
|
||||
smartSearch?: SmartSearchEntity;
|
||||
tags!: TagEntity[];
|
||||
exifInfo?: Exif;
|
||||
tags?: Tag[];
|
||||
sharedLinks!: SharedLinkEntity[];
|
||||
albums?: AlbumEntity[];
|
||||
faces!: AssetFaceEntity[];
|
||||
@@ -68,7 +63,9 @@ export type AssetEntityPlaceholder = AssetEntity & {
|
||||
};
|
||||
|
||||
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'));
|
||||
return qb
|
||||
.leftJoin('exif', 'assets.id', 'exif.assetId')
|
||||
.select((eb) => eb.fn.toJson(eb.table('exif')).$castTo<Exif>().as('exifInfo'));
|
||||
}
|
||||
|
||||
export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||
@@ -97,9 +94,9 @@ export function withFiles(eb: ExpressionBuilder<DB, 'assets'>, type?: AssetFileT
|
||||
return jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('asset_files')
|
||||
.selectAll()
|
||||
.selectAll('asset_files')
|
||||
.whereRef('asset_files.assetId', '=', 'assets.id')
|
||||
.$if(!!type, (qb) => qb.where('type', '=', type!)),
|
||||
.$if(!!type, (qb) => qb.where('asset_files.type', '=', type!)),
|
||||
).as('files');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
|
||||
export class ExifEntity {
|
||||
asset?: AssetEntity;
|
||||
assetId!: string;
|
||||
updatedAt?: Date;
|
||||
updateId?: string;
|
||||
description!: string; // or caption
|
||||
exifImageWidth!: number | null;
|
||||
exifImageHeight!: number | null;
|
||||
fileSizeInByte!: number | null;
|
||||
orientation!: string | null;
|
||||
dateTimeOriginal!: Date | null;
|
||||
modifyDate!: Date | null;
|
||||
timeZone!: string | null;
|
||||
latitude!: number | null;
|
||||
longitude!: number | null;
|
||||
projectionType!: string | null;
|
||||
city!: string | null;
|
||||
livePhotoCID!: string | null;
|
||||
autoStackId!: string | null;
|
||||
state!: string | null;
|
||||
country!: string | null;
|
||||
make!: string | null;
|
||||
model!: string | null;
|
||||
lensModel!: string | null;
|
||||
fNumber!: number | null;
|
||||
focalLength!: number | null;
|
||||
iso!: number | null;
|
||||
exposureTime!: string | null;
|
||||
profileDescription!: string | null;
|
||||
colorspace!: string | null;
|
||||
bitsPerSample!: number | null;
|
||||
rating!: number | null;
|
||||
fps?: number | null;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export class GeodataPlacesEntity {
|
||||
id!: number;
|
||||
name!: string;
|
||||
longitude!: number;
|
||||
latitude!: number;
|
||||
countryCode!: string;
|
||||
admin1Code!: string;
|
||||
admin2Code!: string;
|
||||
admin1Name!: string;
|
||||
admin2Name!: string;
|
||||
alternateNames!: string;
|
||||
modificationDate!: Date;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { PathType } from 'src/enum';
|
||||
|
||||
export class MoveEntity {
|
||||
id!: string;
|
||||
entityId!: string;
|
||||
pathType!: PathType;
|
||||
oldPath!: string;
|
||||
newPath!: string;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export class NaturalEarthCountriesTempEntity {
|
||||
id!: number;
|
||||
admin!: string;
|
||||
admin_a3!: string;
|
||||
type!: string;
|
||||
coordinates!: string;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export class PersonEntity {
|
||||
id!: string;
|
||||
@@ -7,7 +6,6 @@ export class PersonEntity {
|
||||
updatedAt!: Date;
|
||||
updateId?: string;
|
||||
ownerId!: string;
|
||||
owner!: UserEntity;
|
||||
name!: string;
|
||||
birthDate!: Date | string | null;
|
||||
thumbnailPath!: string;
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ExpressionBuilder } from 'kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export class SessionEntity {
|
||||
id!: string;
|
||||
token!: string;
|
||||
userId!: string;
|
||||
user!: UserEntity;
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
updateId!: string;
|
||||
deviceType!: string;
|
||||
deviceOS!: string;
|
||||
}
|
||||
|
||||
const userColumns = [
|
||||
'id',
|
||||
'email',
|
||||
'createdAt',
|
||||
'profileImagePath',
|
||||
'isAdmin',
|
||||
'shouldChangePassword',
|
||||
'deletedAt',
|
||||
'oauthId',
|
||||
'updatedAt',
|
||||
'storageLabel',
|
||||
'name',
|
||||
'quotaSizeInBytes',
|
||||
'quotaUsageInBytes',
|
||||
'status',
|
||||
'profileChangedAt',
|
||||
] as const;
|
||||
|
||||
export const withUser = (eb: ExpressionBuilder<DB, 'sessions'>) => {
|
||||
return eb
|
||||
.selectFrom('users')
|
||||
.select(userColumns)
|
||||
.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'),
|
||||
)
|
||||
.whereRef('users.id', '=', 'sessions.userId')
|
||||
.where('users.deletedAt', 'is', null)
|
||||
.as('user');
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
|
||||
export class SharedLinkEntity {
|
||||
@@ -8,7 +7,6 @@ export class SharedLinkEntity {
|
||||
description!: string | null;
|
||||
password!: string | null;
|
||||
userId!: string;
|
||||
user!: UserEntity;
|
||||
key!: Buffer; // use to access the inidividual asset
|
||||
type!: SharedLinkType;
|
||||
createdAt!: Date;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
|
||||
export class SmartSearchEntity {
|
||||
asset?: AssetEntity;
|
||||
assetId!: string;
|
||||
embedding!: string;
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export class StackEntity {
|
||||
id!: string;
|
||||
owner!: UserEntity;
|
||||
ownerId!: string;
|
||||
assets!: AssetEntity[];
|
||||
primaryAsset!: AssetEntity;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export class TagEntity {
|
||||
id!: string;
|
||||
value!: string;
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
updateId?: string;
|
||||
color!: string | null;
|
||||
parentId?: string;
|
||||
parent?: TagEntity;
|
||||
children?: TagEntity[];
|
||||
user?: UserEntity;
|
||||
userId!: string;
|
||||
assets?: AssetEntity[];
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { UserAvatarColor, UserMetadataKey } from 'src/enum';
|
||||
import { DeepPartial } from 'src/types';
|
||||
import { HumanReadableSize } from 'src/utils/bytes';
|
||||
|
||||
export type UserMetadataItem<T extends keyof UserMetadata = UserMetadataKey> = {
|
||||
key: T;
|
||||
value: UserMetadata[T];
|
||||
};
|
||||
|
||||
export class UserMetadataEntity<T extends keyof UserMetadata = UserMetadataKey> implements UserMetadataItem<T> {
|
||||
userId!: string;
|
||||
user?: UserEntity;
|
||||
key!: T;
|
||||
value!: UserMetadata[T];
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
folders: {
|
||||
enabled: boolean;
|
||||
sidebarWeb: boolean;
|
||||
};
|
||||
memories: {
|
||||
enabled: boolean;
|
||||
};
|
||||
people: {
|
||||
enabled: boolean;
|
||||
sidebarWeb: boolean;
|
||||
};
|
||||
ratings: {
|
||||
enabled: boolean;
|
||||
};
|
||||
sharedLinks: {
|
||||
enabled: boolean;
|
||||
sidebarWeb: boolean;
|
||||
};
|
||||
tags: {
|
||||
enabled: boolean;
|
||||
sidebarWeb: boolean;
|
||||
};
|
||||
avatar: {
|
||||
color: UserAvatarColor;
|
||||
};
|
||||
emailNotifications: {
|
||||
enabled: boolean;
|
||||
albumInvite: boolean;
|
||||
albumUpdate: boolean;
|
||||
};
|
||||
download: {
|
||||
archiveSize: number;
|
||||
includeEmbeddedVideos: boolean;
|
||||
};
|
||||
purchase: {
|
||||
showSupportBadge: boolean;
|
||||
hideBuyButtonUntil: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const getDefaultPreferences = (user: { email: string }): UserPreferences => {
|
||||
const values = Object.values(UserAvatarColor);
|
||||
const randomIndex = Math.floor(
|
||||
[...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
|
||||
);
|
||||
|
||||
return {
|
||||
folders: {
|
||||
enabled: false,
|
||||
sidebarWeb: false,
|
||||
},
|
||||
memories: {
|
||||
enabled: true,
|
||||
},
|
||||
people: {
|
||||
enabled: true,
|
||||
sidebarWeb: false,
|
||||
},
|
||||
sharedLinks: {
|
||||
enabled: true,
|
||||
sidebarWeb: false,
|
||||
},
|
||||
ratings: {
|
||||
enabled: false,
|
||||
},
|
||||
tags: {
|
||||
enabled: false,
|
||||
sidebarWeb: false,
|
||||
},
|
||||
avatar: {
|
||||
color: values[randomIndex],
|
||||
},
|
||||
emailNotifications: {
|
||||
enabled: true,
|
||||
albumInvite: true,
|
||||
albumUpdate: true,
|
||||
},
|
||||
download: {
|
||||
archiveSize: HumanReadableSize.GiB * 4,
|
||||
includeEmbeddedVideos: false,
|
||||
},
|
||||
purchase: {
|
||||
showSupportBadge: true,
|
||||
hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export interface UserMetadata extends Record<UserMetadataKey, Record<string, any>> {
|
||||
[UserMetadataKey.PREFERENCES]: DeepPartial<UserPreferences>;
|
||||
[UserMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: string };
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ExpressionBuilder } from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { DB } from 'src/db';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||
import { UserStatus } from 'src/enum';
|
||||
|
||||
export class UserEntity {
|
||||
id!: string;
|
||||
name!: string;
|
||||
isAdmin!: boolean;
|
||||
email!: string;
|
||||
storageLabel!: string | null;
|
||||
password?: string;
|
||||
oauthId!: string;
|
||||
profileImagePath!: string;
|
||||
shouldChangePassword!: boolean;
|
||||
createdAt!: Date;
|
||||
deletedAt!: Date | null;
|
||||
status!: UserStatus;
|
||||
updatedAt!: Date;
|
||||
updateId?: string;
|
||||
assets!: AssetEntity[];
|
||||
quotaSizeInBytes!: number | null;
|
||||
quotaUsageInBytes!: number;
|
||||
metadata!: UserMetadataEntity[];
|
||||
profileChangedAt!: Date;
|
||||
}
|
||||
|
||||
export const withMetadata = (eb: ExpressionBuilder<DB, 'users'>) => {
|
||||
return jsonArrayFrom(
|
||||
eb.selectFrom('user_metadata').selectAll('user_metadata').whereRef('users.id', '=', 'user_metadata.userId'),
|
||||
).as('metadata');
|
||||
};
|
||||
12
server/src/migrations/1743595393000-TableCleanup.ts
Normal file
12
server/src/migrations/1743595393000-TableCleanup.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class TableCleanup1743595393000 implements MigrationInterface {
|
||||
name = 'TableCleanup1743595393000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "system_config"`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "socket_io_attachments"`);
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {}
|
||||
}
|
||||
@@ -3,6 +3,38 @@
|
||||
-- ActivityRepository.search
|
||||
select
|
||||
"activity".*,
|
||||
to_json("user") as "user"
|
||||
from
|
||||
"activity"
|
||||
inner join "users" on "users"."id" = "activity"."userId"
|
||||
and "users"."deletedAt" is null
|
||||
inner join lateral (
|
||||
select
|
||||
"users"."id",
|
||||
"users"."name",
|
||||
"users"."email",
|
||||
"users"."profileImagePath",
|
||||
"users"."profileChangedAt"
|
||||
from
|
||||
(
|
||||
select
|
||||
1
|
||||
) as "dummy"
|
||||
) as "user" on true
|
||||
left join "assets" on "assets"."id" = "activity"."assetId"
|
||||
and "assets"."deletedAt" is null
|
||||
where
|
||||
"activity"."albumId" = $1
|
||||
order by
|
||||
"activity"."createdAt" asc
|
||||
|
||||
-- ActivityRepository.create
|
||||
insert into
|
||||
"activity" ("albumId", "userId")
|
||||
values
|
||||
($1, $2)
|
||||
returning
|
||||
*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
@@ -18,17 +50,13 @@ select
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "activity"."userId"
|
||||
and "users"."deletedAt" is null
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"activity"
|
||||
left join "assets" on "assets"."id" = "activity"."assetId"
|
||||
and "assets"."deletedAt" is null
|
||||
|
||||
-- ActivityRepository.delete
|
||||
delete from "activity"
|
||||
where
|
||||
"activity"."albumId" = $1
|
||||
order by
|
||||
"activity"."createdAt" asc
|
||||
"id" = $1::uuid
|
||||
|
||||
-- ActivityRepository.getStatistics
|
||||
select
|
||||
|
||||
@@ -179,6 +179,63 @@ from
|
||||
where
|
||||
"livePhotoVideoId" = $1::uuid
|
||||
|
||||
-- AssetRepository.getAssetForSearchDuplicatesJob
|
||||
select
|
||||
"id",
|
||||
"type",
|
||||
"ownerId",
|
||||
"duplicateId",
|
||||
"stackId",
|
||||
"isVisible",
|
||||
"smart_search"."embedding",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"asset_files".*
|
||||
from
|
||||
"asset_files"
|
||||
where
|
||||
"asset_files"."assetId" = "assets"."id"
|
||||
and "asset_files"."type" = $1
|
||||
) as agg
|
||||
) as "files"
|
||||
from
|
||||
"assets"
|
||||
left join "smart_search" on "assets"."id" = "smart_search"."assetId"
|
||||
where
|
||||
"assets"."id" = $2::uuid
|
||||
limit
|
||||
$3
|
||||
|
||||
-- AssetRepository.getAssetForSidecarWriteJob
|
||||
select
|
||||
"id",
|
||||
"sidecarPath",
|
||||
"originalPath",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"tags"."value"
|
||||
from
|
||||
"tags"
|
||||
inner join "tag_asset" on "tags"."id" = "tag_asset"."tagsId"
|
||||
where
|
||||
"assets"."id" = "tag_asset"."assetsId"
|
||||
) as agg
|
||||
) as "tags"
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."id" = $1::uuid
|
||||
limit
|
||||
$2
|
||||
|
||||
-- AssetRepository.getById
|
||||
select
|
||||
"assets".*
|
||||
|
||||
@@ -38,41 +38,11 @@ where
|
||||
|
||||
-- SessionRepository.getByUserId
|
||||
select
|
||||
"sessions".*,
|
||||
to_json("user") as "user"
|
||||
"sessions".*
|
||||
from
|
||||
"sessions"
|
||||
inner join lateral (
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt",
|
||||
(
|
||||
select
|
||||
array_agg("user_metadata") as "metadata"
|
||||
from
|
||||
"user_metadata"
|
||||
where
|
||||
"users"."id" = "user_metadata"."userId"
|
||||
) as "metadata"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "sessions"."userId"
|
||||
and "users"."deletedAt" is null
|
||||
) as "user" on true
|
||||
inner join "users" on "users"."id" = "sessions"."userId"
|
||||
and "users"."deletedAt" is null
|
||||
where
|
||||
"sessions"."userId" = $1
|
||||
order by
|
||||
|
||||
@@ -24,7 +24,8 @@ select
|
||||
from
|
||||
(
|
||||
select
|
||||
"user_metadata".*
|
||||
"user_metadata"."key",
|
||||
"user_metadata"."value"
|
||||
from
|
||||
"user_metadata"
|
||||
where
|
||||
@@ -54,7 +55,21 @@ select
|
||||
"shouldChangePassword",
|
||||
"storageLabel",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes"
|
||||
"quotaUsageInBytes",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"user_metadata"."key",
|
||||
"user_metadata"."value"
|
||||
from
|
||||
"user_metadata"
|
||||
where
|
||||
"users"."id" = "user_metadata"."userId"
|
||||
) as agg
|
||||
) as "metadata"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
@@ -87,7 +102,21 @@ select
|
||||
"shouldChangePassword",
|
||||
"storageLabel",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes"
|
||||
"quotaUsageInBytes",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"user_metadata"."key",
|
||||
"user_metadata"."value"
|
||||
from
|
||||
"user_metadata"
|
||||
where
|
||||
"users"."id" = "user_metadata"."userId"
|
||||
) as agg
|
||||
) as "metadata"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
@@ -135,7 +164,21 @@ select
|
||||
"shouldChangePassword",
|
||||
"storageLabel",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes"
|
||||
"quotaUsageInBytes",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"user_metadata"."key",
|
||||
"user_metadata"."value"
|
||||
from
|
||||
"user_metadata"
|
||||
where
|
||||
"users"."id" = "user_metadata"."userId"
|
||||
) as agg
|
||||
) as "metadata"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
@@ -174,7 +217,8 @@ select
|
||||
from
|
||||
(
|
||||
select
|
||||
"user_metadata".*
|
||||
"user_metadata"."key",
|
||||
"user_metadata"."value"
|
||||
from
|
||||
"user_metadata"
|
||||
where
|
||||
@@ -210,7 +254,8 @@ select
|
||||
from
|
||||
(
|
||||
select
|
||||
"user_metadata".*
|
||||
"user_metadata"."key",
|
||||
"user_metadata"."value"
|
||||
from
|
||||
"user_metadata"
|
||||
where
|
||||
@@ -232,15 +277,15 @@ select
|
||||
count(*) filter (
|
||||
where
|
||||
(
|
||||
"assets"."type" = $1
|
||||
and "assets"."isVisible" = $2
|
||||
"assets"."type" = 'IMAGE'
|
||||
and "assets"."isVisible" = true
|
||||
)
|
||||
) as "photos",
|
||||
count(*) filter (
|
||||
where
|
||||
(
|
||||
"assets"."type" = $3
|
||||
and "assets"."isVisible" = $4
|
||||
"assets"."type" = 'VIDEO'
|
||||
and "assets"."isVisible" = true
|
||||
)
|
||||
) as "videos",
|
||||
coalesce(
|
||||
@@ -255,7 +300,7 @@ select
|
||||
where
|
||||
(
|
||||
"assets"."libraryId" is null
|
||||
and "assets"."type" = $5
|
||||
and "assets"."type" = 'IMAGE'
|
||||
)
|
||||
),
|
||||
0
|
||||
@@ -265,7 +310,7 @@ select
|
||||
where
|
||||
(
|
||||
"assets"."libraryId" is null
|
||||
and "assets"."type" = $6
|
||||
and "assets"."type" = 'VIDEO'
|
||||
)
|
||||
),
|
||||
0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Insertable, Kysely } from 'kysely';
|
||||
import { Insertable, Kysely, NotNull, sql } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
@@ -14,16 +14,6 @@ export interface ActivitySearch {
|
||||
isLiked?: boolean;
|
||||
}
|
||||
|
||||
const withUser = (eb: ExpressionBuilder<DB, 'activity'>) => {
|
||||
return jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('users')
|
||||
.select(columns.user)
|
||||
.whereRef('users.id', '=', 'activity.userId')
|
||||
.where('users.deletedAt', 'is', null),
|
||||
).as('user');
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class ActivityRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
@@ -35,7 +25,16 @@ export class ActivityRepository {
|
||||
return this.db
|
||||
.selectFrom('activity')
|
||||
.selectAll('activity')
|
||||
.select(withUser)
|
||||
.innerJoin('users', (join) => join.onRef('users.id', '=', 'activity.userId').on('users.deletedAt', 'is', null))
|
||||
.innerJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom(sql`(select 1)`.as('dummy'))
|
||||
.select(columns.userWithPrefix)
|
||||
.as('user'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson('user').as('user'))
|
||||
.leftJoin('assets', (join) => join.onRef('assets.id', '=', 'activity.assetId').on('assets.deletedAt', 'is', null))
|
||||
.$if(!!userId, (qb) => qb.where('activity.userId', '=', userId!))
|
||||
.$if(assetId === null, (qb) => qb.where('assetId', 'is', null))
|
||||
@@ -46,10 +45,22 @@ export class ActivityRepository {
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ albumId: DummyValue.UUID, userId: DummyValue.UUID }] })
|
||||
async create(activity: Insertable<Activity>) {
|
||||
return this.save(activity);
|
||||
return this.db
|
||||
.insertInto('activity')
|
||||
.values(activity)
|
||||
.returningAll()
|
||||
.returning((eb) =>
|
||||
jsonObjectFrom(eb.selectFrom('users').whereRef('users.id', '=', 'activity.userId').select(columns.user)).as(
|
||||
'user',
|
||||
),
|
||||
)
|
||||
.$narrowType<{ user: NotNull }>()
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async delete(id: string) {
|
||||
await this.db.deleteFrom('activity').where('id', '=', asUuid(id)).execute();
|
||||
}
|
||||
@@ -58,7 +69,7 @@ export class ActivityRepository {
|
||||
async getStatistics({ albumId, assetId }: { albumId: string; assetId?: string }): Promise<number> {
|
||||
const { count } = await this.db
|
||||
.selectFrom('activity')
|
||||
.select((eb) => eb.fn.countAll().as('count'))
|
||||
.select((eb) => eb.fn.countAll<number>().as('count'))
|
||||
.innerJoin('users', (join) => join.onRef('users.id', '=', 'activity.userId').on('users.deletedAt', 'is', null))
|
||||
.leftJoin('assets', 'assets.id', 'activity.assetId')
|
||||
.$if(!!assetId, (qb) => qb.where('activity.assetId', '=', assetId!))
|
||||
@@ -70,17 +81,6 @@ export class ActivityRepository {
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return count as number;
|
||||
}
|
||||
|
||||
private async save(entity: Insertable<Activity>) {
|
||||
const { id } = await this.db.insertInto('activity').values(entity).returning('id').executeTakeFirstOrThrow();
|
||||
|
||||
return this.db
|
||||
.selectFrom('activity')
|
||||
.selectAll('activity')
|
||||
.select(withUser)
|
||||
.where('activity.id', '=', asUuid(id))
|
||||
.executeTakeFirstOrThrow();
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, Selectable, UpdateResult, Updateable, sql } from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { isEmpty, isUndefined, omitBy } from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
|
||||
@@ -469,10 +470,51 @@ export class AssetRepository {
|
||||
async getLivePhotoCount(motionId: string): Promise<number> {
|
||||
const [{ count }] = await this.db
|
||||
.selectFrom('assets')
|
||||
.select((eb) => eb.fn.countAll().as('count'))
|
||||
.select((eb) => eb.fn.countAll<number>().as('count'))
|
||||
.where('livePhotoVideoId', '=', asUuid(motionId))
|
||||
.execute();
|
||||
return count as number;
|
||||
return count;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAssetForSearchDuplicatesJob(id: string) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.where('assets.id', '=', asUuid(id))
|
||||
.leftJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||
.select((eb) => [
|
||||
'id',
|
||||
'type',
|
||||
'ownerId',
|
||||
'duplicateId',
|
||||
'stackId',
|
||||
'isVisible',
|
||||
'smart_search.embedding',
|
||||
withFiles(eb, AssetFileType.PREVIEW),
|
||||
])
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAssetForSidecarWriteJob(id: string) {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.where('assets.id', '=', asUuid(id))
|
||||
.select((eb) => [
|
||||
'id',
|
||||
'sidecarPath',
|
||||
'originalPath',
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('tags')
|
||||
.select(['tags.value'])
|
||||
.innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId')
|
||||
.whereRef('assets.id', '=', 'tag_asset.assetsId'),
|
||||
).as('tags'),
|
||||
])
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
@@ -731,10 +773,10 @@ export class AssetRepository {
|
||||
getStatistics(ownerId: string, { isArchived, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> {
|
||||
return this.db
|
||||
.selectFrom('assets')
|
||||
.select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.AUDIO).as(AssetType.AUDIO))
|
||||
.select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.IMAGE).as(AssetType.IMAGE))
|
||||
.select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.VIDEO).as(AssetType.VIDEO))
|
||||
.select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.OTHER).as(AssetType.OTHER))
|
||||
.select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.AUDIO).as(AssetType.AUDIO))
|
||||
.select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.IMAGE).as(AssetType.IMAGE))
|
||||
.select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.VIDEO).as(AssetType.VIDEO))
|
||||
.select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.OTHER).as(AssetType.OTHER))
|
||||
.where('ownerId', '=', asUuid(ownerId))
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
@@ -744,7 +786,7 @@ export class AssetRepository {
|
||||
.$if(isFavorite !== undefined, (qb) => qb.where('isFavorite', '=', isFavorite!))
|
||||
.$if(!!isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
||||
.where('deletedAt', isTrashed ? 'is not' : 'is', null)
|
||||
.executeTakeFirst() as Promise<AssetStats>;
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
getRandom(userIds: string[], take: number): Promise<AssetEntity[]> {
|
||||
@@ -805,7 +847,7 @@ export class AssetRepository {
|
||||
The line below outputs in YYYY-MM-DD format, but needs a change in the web app to work.
|
||||
.select(sql<string>`"timeBucket"::date::text`.as('timeBucket'))
|
||||
*/
|
||||
.select((eb) => eb.fn.countAll().as('count'))
|
||||
.select((eb) => eb.fn.countAll<number>().as('count'))
|
||||
.groupBy('timeBucket')
|
||||
.orderBy('timeBucket', options.order ?? 'desc')
|
||||
.execute() as any as Promise<TimeBucketItem[]>
|
||||
@@ -1103,10 +1145,10 @@ export class AssetRepository {
|
||||
async getLibraryAssetCount(libraryId: string): Promise<number> {
|
||||
const { count } = await this.db
|
||||
.selectFrom('assets')
|
||||
.select((eb) => eb.fn.countAll().as('count'))
|
||||
.select((eb) => eb.fn.countAll<number>().as('count'))
|
||||
.where('libraryId', '=', asUuid(libraryId))
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return Number(count);
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ const getEnv = (): EnvData => {
|
||||
},
|
||||
bigint: {
|
||||
to: 20,
|
||||
from: [20],
|
||||
from: [20, 1700],
|
||||
parse: (value: string) => Number.parseInt(value),
|
||||
serialize: (value: number) => value.toString(),
|
||||
},
|
||||
|
||||
@@ -197,58 +197,62 @@ export class DatabaseRepository {
|
||||
return dimSize;
|
||||
}
|
||||
|
||||
async runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise<void> {
|
||||
async runMigrations(options?: { transaction?: 'all' | 'none' | 'each'; only?: 'kysely' | 'typeorm' }): Promise<void> {
|
||||
const { database } = this.configRepository.getEnv();
|
||||
const dataSource = new DataSource(database.config.typeorm);
|
||||
if (options?.only !== 'kysely') {
|
||||
const dataSource = new DataSource(database.config.typeorm);
|
||||
|
||||
this.logger.log('Running migrations, this may take a while');
|
||||
this.logger.log('Running migrations, this may take a while');
|
||||
|
||||
this.logger.debug('Running typeorm migrations');
|
||||
this.logger.debug('Running typeorm migrations');
|
||||
|
||||
await dataSource.initialize();
|
||||
await dataSource.runMigrations(options);
|
||||
await dataSource.destroy();
|
||||
await dataSource.initialize();
|
||||
await dataSource.runMigrations(options);
|
||||
await dataSource.destroy();
|
||||
|
||||
this.logger.debug('Finished running typeorm migrations');
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
const migrationFolder = join(__dirname, '..', 'schema/migrations');
|
||||
|
||||
// TODO remove after we have at least one kysely migration
|
||||
if (!existsSync(migrationFolder)) {
|
||||
return;
|
||||
this.logger.debug('Finished running typeorm migrations');
|
||||
}
|
||||
|
||||
this.logger.debug('Running kysely migrations');
|
||||
const migrator = new Migrator({
|
||||
db: this.db,
|
||||
migrationLockTableName: 'kysely_migrations_lock',
|
||||
migrationTableName: 'kysely_migrations',
|
||||
provider: new FileMigrationProvider({
|
||||
fs: { readdir },
|
||||
path: { join },
|
||||
migrationFolder,
|
||||
}),
|
||||
});
|
||||
if (options?.only !== 'typeorm') {
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
const migrationFolder = join(__dirname, '..', 'schema/migrations');
|
||||
|
||||
const { error, results } = await migrator.migrateToLatest();
|
||||
|
||||
for (const result of results ?? []) {
|
||||
if (result.status === 'Success') {
|
||||
this.logger.log(`Migration "${result.migrationName}" succeeded`);
|
||||
// TODO remove after we have at least one kysely migration
|
||||
if (!existsSync(migrationFolder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.status === 'Error') {
|
||||
this.logger.warn(`Migration "${result.migrationName}" failed`);
|
||||
this.logger.debug('Running kysely migrations');
|
||||
const migrator = new Migrator({
|
||||
db: this.db,
|
||||
migrationLockTableName: 'kysely_migrations_lock',
|
||||
migrationTableName: 'kysely_migrations',
|
||||
provider: new FileMigrationProvider({
|
||||
fs: { readdir },
|
||||
path: { join },
|
||||
migrationFolder,
|
||||
}),
|
||||
});
|
||||
|
||||
const { error, results } = await migrator.migrateToLatest();
|
||||
|
||||
for (const result of results ?? []) {
|
||||
if (result.status === 'Success') {
|
||||
this.logger.log(`Migration "${result.migrationName}" succeeded`);
|
||||
}
|
||||
|
||||
if (result.status === 'Error') {
|
||||
this.logger.warn(`Migration "${result.migrationName}" failed`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.logger.error(`Kysely migrations failed: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
if (error) {
|
||||
this.logger.error(`Kysely migrations failed: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.debug('Finished running kysely migrations');
|
||||
this.logger.debug('Finished running kysely migrations');
|
||||
}
|
||||
}
|
||||
|
||||
async withLock<R>(lock: DatabaseLock, callback: () => Promise<R>): Promise<R> {
|
||||
|
||||
@@ -76,13 +76,13 @@ export class LibraryRepository {
|
||||
.leftJoin('exif', 'exif.assetId', 'assets.id')
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.countAll()
|
||||
.countAll<number>()
|
||||
.filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.IMAGE), eb('assets.isVisible', '=', true)]))
|
||||
.as('photos'),
|
||||
)
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.countAll()
|
||||
.countAll<number>()
|
||||
.filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.VIDEO), eb('assets.isVisible', '=', true)]))
|
||||
.as('videos'),
|
||||
)
|
||||
@@ -105,10 +105,10 @@ export class LibraryRepository {
|
||||
}
|
||||
|
||||
return {
|
||||
photos: Number(stats.photos),
|
||||
videos: Number(stats.videos),
|
||||
usage: Number(stats.usage),
|
||||
total: Number(stats.photos) + Number(stats.videos),
|
||||
photos: stats.photos,
|
||||
videos: stats.videos,
|
||||
usage: stats.usage,
|
||||
total: stats.photos + stats.videos,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
|
||||
import { ConsoleLogger, Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { Telemetry } from 'src/decorators';
|
||||
@@ -26,7 +26,7 @@ export class MyConsoleLogger extends ConsoleLogger {
|
||||
private isColorEnabled: boolean;
|
||||
|
||||
constructor(
|
||||
private cls: ClsService,
|
||||
private cls: ClsService | undefined,
|
||||
options?: { color?: boolean; context?: string },
|
||||
) {
|
||||
super(options?.context || MyConsoleLogger.name);
|
||||
@@ -74,7 +74,7 @@ export class MyConsoleLogger extends ConsoleLogger {
|
||||
export class LoggingRepository {
|
||||
private logger: MyConsoleLogger;
|
||||
|
||||
constructor(cls: ClsService, configRepository: ConfigRepository) {
|
||||
constructor(@Inject(ClsService) cls: ClsService | undefined, configRepository: ConfigRepository) {
|
||||
const { noColor } = configRepository.getEnv();
|
||||
this.logger = new MyConsoleLogger(cls, { context: LoggingRepository.name, color: !noColor });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { getName } from 'i18n-iso-countries';
|
||||
import { Expression, Kysely, sql, SqlBool } from 'kysely';
|
||||
import { Expression, Insertable, Kysely, NotNull, sql, SqlBool } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { createReadStream, existsSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
@@ -8,7 +8,6 @@ import readLine from 'node:readline';
|
||||
import { citiesFile } from 'src/constants';
|
||||
import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity';
|
||||
import { SystemMetadataKey } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
@@ -88,6 +87,7 @@ export class MapRepository {
|
||||
.on('exif.longitude', 'is not', null),
|
||||
)
|
||||
.select(['id', 'exif.latitude as lat', 'exif.longitude as lon', 'exif.city', 'exif.state', 'exif.country'])
|
||||
.$narrowType<{ lat: NotNull; lon: NotNull }>()
|
||||
.where('isVisible', '=', true)
|
||||
.$if(isArchived !== undefined, (q) => q.where('isArchived', '=', isArchived!))
|
||||
.$if(isFavorite !== undefined, (q) => q.where('isFavorite', '=', isFavorite!))
|
||||
@@ -115,7 +115,7 @@ export class MapRepository {
|
||||
return eb.or(expression);
|
||||
})
|
||||
.orderBy('fileCreatedAt', 'desc')
|
||||
.execute() as Promise<MapMarker[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
async reverseGeocode(point: GeoPoint): Promise<ReverseGeocodeResult> {
|
||||
@@ -182,11 +182,11 @@ export class MapRepository {
|
||||
return;
|
||||
}
|
||||
|
||||
const entities: Omit<NaturalEarthCountriesTempEntity, 'id'>[] = [];
|
||||
const entities: Insertable<NaturalearthCountries>[] = [];
|
||||
for (const feature of geoJSONData.features) {
|
||||
for (const entry of feature.geometry.coordinates) {
|
||||
const coordinates: number[][][] = feature.geometry.type === 'MultiPolygon' ? entry[0] : entry;
|
||||
const featureRecord: Omit<NaturalEarthCountriesTempEntity, 'id'> = {
|
||||
const featureRecord: Insertable<NaturalearthCountries> = {
|
||||
admin: feature.properties.ADMIN,
|
||||
admin_a3: feature.properties.ADM0_A3,
|
||||
type: feature.properties.TYPE,
|
||||
|
||||
@@ -6,7 +6,7 @@ import fs from 'node:fs/promises';
|
||||
import { Writable } from 'node:stream';
|
||||
import sharp from 'sharp';
|
||||
import { ORIENTATION_TO_SHARP_ROTATION } from 'src/constants';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { Exif } from 'src/database';
|
||||
import { Colorspace, LogLevel } from 'src/enum';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import {
|
||||
@@ -66,7 +66,7 @@ export class MediaRepository {
|
||||
return true;
|
||||
}
|
||||
|
||||
async writeExif(tags: Partial<ExifEntity>, output: string): Promise<boolean> {
|
||||
async writeExif(tags: Partial<Exif>, output: string): Promise<boolean> {
|
||||
try {
|
||||
const tagsToWrite: WriteTags = {
|
||||
ExifImageWidth: tags.exifImageWidth,
|
||||
|
||||
@@ -3,49 +3,38 @@ import { Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB, MoveHistory } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { MoveEntity } from 'src/entities/move.entity';
|
||||
import { AssetPathType, PathType } from 'src/enum';
|
||||
|
||||
export type MoveCreate = Pick<MoveEntity, 'oldPath' | 'newPath' | 'entityId' | 'pathType'> & Partial<MoveEntity>;
|
||||
|
||||
@Injectable()
|
||||
export class MoveRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
create(entity: Insertable<MoveHistory>): Promise<MoveEntity> {
|
||||
return this.db
|
||||
.insertInto('move_history')
|
||||
.values(entity)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow() as Promise<MoveEntity>;
|
||||
create(entity: Insertable<MoveHistory>) {
|
||||
return this.db.insertInto('move_history').values(entity).returningAll().executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
||||
getByEntity(entityId: string, pathType: PathType): Promise<MoveEntity | undefined> {
|
||||
getByEntity(entityId: string, pathType: PathType) {
|
||||
return this.db
|
||||
.selectFrom('move_history')
|
||||
.selectAll()
|
||||
.where('entityId', '=', entityId)
|
||||
.where('pathType', '=', pathType)
|
||||
.executeTakeFirst() as Promise<MoveEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
update(id: string, entity: Updateable<MoveHistory>): Promise<MoveEntity> {
|
||||
update(id: string, entity: Updateable<MoveHistory>) {
|
||||
return this.db
|
||||
.updateTable('move_history')
|
||||
.set(entity)
|
||||
.where('id', '=', id)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow() as unknown as Promise<MoveEntity>;
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
delete(id: string): Promise<MoveEntity> {
|
||||
return this.db
|
||||
.deleteFrom('move_history')
|
||||
.where('id', '=', id)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow() as unknown as Promise<MoveEntity>;
|
||||
delete(id: string) {
|
||||
return this.db.deleteFrom('move_history').where('id', '=', id).returningAll().executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
|
||||
@@ -63,6 +63,18 @@ export class OAuthRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async getProfilePicture(url: string) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch picture: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return {
|
||||
data: await response.arrayBuffer(),
|
||||
contentType: response.headers.get('content-type'),
|
||||
};
|
||||
}
|
||||
|
||||
private async getClient({
|
||||
issuerUrl,
|
||||
clientId,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Insertable, Kysely, Updateable } from 'kysely';
|
||||
import { ExpressionBuilder, Insertable, Kysely, NotNull, Updateable } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns, Partner } from 'src/database';
|
||||
import { columns } from 'src/database';
|
||||
import { DB, Partners } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
|
||||
@@ -44,7 +44,7 @@ export class PartnerRepository {
|
||||
return this.builder()
|
||||
.where('sharedWithId', '=', sharedWithId)
|
||||
.where('sharedById', '=', sharedById)
|
||||
.executeTakeFirst() as Promise<Partner | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }] })
|
||||
@@ -55,7 +55,8 @@ export class PartnerRepository {
|
||||
.returningAll()
|
||||
.returning(withSharedBy)
|
||||
.returning(withSharedWith)
|
||||
.executeTakeFirstOrThrow() as Promise<Partner>;
|
||||
.$narrowType<{ sharedWith: NotNull; sharedBy: NotNull }>()
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }, { inTimeline: true }] })
|
||||
@@ -68,7 +69,8 @@ export class PartnerRepository {
|
||||
.returningAll()
|
||||
.returning(withSharedBy)
|
||||
.returning(withSharedWith)
|
||||
.executeTakeFirstOrThrow() as Promise<Partner>;
|
||||
.$narrowType<{ sharedWith: NotNull; sharedBy: NotNull }>()
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }] })
|
||||
|
||||
@@ -5,7 +5,6 @@ import { randomUUID } from 'node:crypto';
|
||||
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 { AssetStatus, AssetType } from 'src/enum';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { anyUuid, asUuid } from 'src/utils/database';
|
||||
@@ -372,7 +371,7 @@ export class SearchRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]> {
|
||||
searchPlaces(placeName: string) {
|
||||
return this.db
|
||||
.selectFrom('geodata_places')
|
||||
.selectAll()
|
||||
@@ -395,7 +394,7 @@ export class SearchRepository {
|
||||
`,
|
||||
)
|
||||
.limit(20)
|
||||
.execute() as Promise<GeodataPlacesEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
export type SessionSearchOptions = { updatedBefore: Date };
|
||||
@@ -45,9 +44,8 @@ export class SessionRepository {
|
||||
getByUserId(userId: string) {
|
||||
return this.db
|
||||
.selectFrom('sessions')
|
||||
.innerJoinLateral(withUser, (join) => join.onTrue())
|
||||
.innerJoin('users', (join) => join.onRef('users.id', '=', 'sessions.userId').on('users.deletedAt', 'is', null))
|
||||
.selectAll('sessions')
|
||||
.select((eb) => eb.fn.toJson('user').as('user'))
|
||||
.where('sessions.userId', '=', userId)
|
||||
.orderBy('sessions.updatedAt', 'desc')
|
||||
.orderBy('sessions.createdAt', 'desc')
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { DateTime } from 'luxon';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns, UserAdmin } from 'src/database';
|
||||
import { columns } from 'src/database';
|
||||
import { DB, UserMetadata as DbUserMetadata } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { UserMetadata, UserMetadataItem } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity, withMetadata } from 'src/entities/user.entity';
|
||||
import { AssetType, UserStatus } from 'src/enum';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import { UserMetadata, UserMetadataItem } from 'src/types';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
type Upsert = Insertable<DbUserMetadata>;
|
||||
@@ -32,12 +32,21 @@ export interface UserFindOptions {
|
||||
withDeleted?: boolean;
|
||||
}
|
||||
|
||||
const withMetadata = (eb: ExpressionBuilder<DB, 'users'>) => {
|
||||
return jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('user_metadata')
|
||||
.select(['user_metadata.key', 'user_metadata.value'])
|
||||
.whereRef('users.id', '=', 'user_metadata.userId'),
|
||||
).as('metadata');
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BOOLEAN] })
|
||||
get(userId: string, options: UserFindOptions): Promise<UserEntity | undefined> {
|
||||
get(userId: string, options: UserFindOptions) {
|
||||
options = options || {};
|
||||
|
||||
return this.db
|
||||
@@ -46,7 +55,7 @@ export class UserRepository {
|
||||
.select(withMetadata)
|
||||
.where('users.id', '=', userId)
|
||||
.$if(!options.withDeleted, (eb) => eb.where('users.deletedAt', 'is', null))
|
||||
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
getMetadata(userId: string) {
|
||||
@@ -58,13 +67,14 @@ export class UserRepository {
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
getAdmin(): Promise<UserEntity | undefined> {
|
||||
getAdmin() {
|
||||
return this.db
|
||||
.selectFrom('users')
|
||||
.select(columns.userAdmin)
|
||||
.select(withMetadata)
|
||||
.where('users.isAdmin', '=', true)
|
||||
.where('users.deletedAt', 'is', null)
|
||||
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
@@ -80,34 +90,36 @@ export class UserRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.EMAIL] })
|
||||
getByEmail(email: string, withPassword?: boolean): Promise<UserEntity | undefined> {
|
||||
getByEmail(email: string, withPassword?: boolean) {
|
||||
return this.db
|
||||
.selectFrom('users')
|
||||
.select(columns.userAdmin)
|
||||
.select(withMetadata)
|
||||
.$if(!!withPassword, (eb) => eb.select('password'))
|
||||
.where('email', '=', email)
|
||||
.where('users.deletedAt', 'is', null)
|
||||
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
getByStorageLabel(storageLabel: string): Promise<UserEntity | undefined> {
|
||||
getByStorageLabel(storageLabel: string) {
|
||||
return this.db
|
||||
.selectFrom('users')
|
||||
.select(columns.userAdmin)
|
||||
.where('users.storageLabel', '=', storageLabel)
|
||||
.where('users.deletedAt', 'is', null)
|
||||
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
getByOAuthId(oauthId: string): Promise<UserEntity | undefined> {
|
||||
getByOAuthId(oauthId: string) {
|
||||
return this.db
|
||||
.selectFrom('users')
|
||||
.select(columns.userAdmin)
|
||||
.select(withMetadata)
|
||||
.where('users.oauthId', '=', oauthId)
|
||||
.where('users.deletedAt', 'is', null)
|
||||
.executeTakeFirst() as Promise<UserEntity | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DateTime.now().minus({ years: 1 })] })
|
||||
@@ -126,18 +138,19 @@ export class UserRepository {
|
||||
.select(withMetadata)
|
||||
.$if(!withDeleted, (eb) => eb.where('users.deletedAt', 'is', null))
|
||||
.orderBy('createdAt', 'desc')
|
||||
.execute() as Promise<UserAdmin[]>;
|
||||
.execute();
|
||||
}
|
||||
|
||||
async create(dto: Insertable<UserTable>): Promise<UserEntity> {
|
||||
async create(dto: Insertable<UserTable>) {
|
||||
return this.db
|
||||
.insertInto('users')
|
||||
.values(dto)
|
||||
.returning(columns.userAdmin)
|
||||
.executeTakeFirst() as unknown as Promise<UserEntity>;
|
||||
.returning(withMetadata)
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
update(id: string, dto: Updateable<UserTable>): Promise<UserEntity> {
|
||||
update(id: string, dto: Updateable<UserTable>) {
|
||||
return this.db
|
||||
.updateTable('users')
|
||||
.set(dto)
|
||||
@@ -145,17 +158,17 @@ export class UserRepository {
|
||||
.where('users.deletedAt', 'is', null)
|
||||
.returning(columns.userAdmin)
|
||||
.returning(withMetadata)
|
||||
.executeTakeFirst() as unknown as Promise<UserEntity>;
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
restore(id: string): Promise<UserEntity> {
|
||||
restore(id: string) {
|
||||
return this.db
|
||||
.updateTable('users')
|
||||
.set({ status: UserStatus.ACTIVE, deletedAt: null })
|
||||
.where('users.id', '=', asUuid(id))
|
||||
.returning(columns.userAdmin)
|
||||
.returning(withMetadata)
|
||||
.executeTakeFirst() as unknown as Promise<UserEntity>;
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async upsertMetadata<T extends keyof UserMetadata>(id: string, { key, value }: { key: T; value: UserMetadata[T] }) {
|
||||
@@ -175,41 +188,41 @@ export class UserRepository {
|
||||
await this.db.deleteFrom('user_metadata').where('userId', '=', id).where('key', '=', key).execute();
|
||||
}
|
||||
|
||||
delete(user: { id: string }, hard?: boolean): Promise<UserEntity> {
|
||||
delete(user: { id: string }, hard?: boolean) {
|
||||
return hard
|
||||
? (this.db.deleteFrom('users').where('id', '=', user.id).execute() as unknown as Promise<UserEntity>)
|
||||
: (this.db
|
||||
.updateTable('users')
|
||||
.set({ deletedAt: new Date() })
|
||||
.where('id', '=', user.id)
|
||||
.execute() as unknown as Promise<UserEntity>);
|
||||
? this.db.deleteFrom('users').where('id', '=', user.id).execute()
|
||||
: this.db.updateTable('users').set({ deletedAt: new Date() }).where('id', '=', user.id).execute();
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
async getUserStats(): Promise<UserStatsQueryResponse[]> {
|
||||
const stats = (await this.db
|
||||
getUserStats() {
|
||||
return this.db
|
||||
.selectFrom('users')
|
||||
.leftJoin('assets', 'assets.ownerId', 'users.id')
|
||||
.leftJoin('exif', 'exif.assetId', 'assets.id')
|
||||
.select(['users.id as userId', 'users.name as userName', 'users.quotaSizeInBytes as quotaSizeInBytes'])
|
||||
.select((eb) => [
|
||||
eb.fn
|
||||
.countAll()
|
||||
.filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.IMAGE), eb('assets.isVisible', '=', true)]))
|
||||
.countAll<number>()
|
||||
.filterWhere((eb) =>
|
||||
eb.and([eb('assets.type', '=', sql.lit(AssetType.IMAGE)), eb('assets.isVisible', '=', sql.lit(true))]),
|
||||
)
|
||||
.as('photos'),
|
||||
eb.fn
|
||||
.countAll()
|
||||
.filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.VIDEO), eb('assets.isVisible', '=', true)]))
|
||||
.countAll<number>()
|
||||
.filterWhere((eb) =>
|
||||
eb.and([eb('assets.type', '=', sql.lit(AssetType.VIDEO)), eb('assets.isVisible', '=', sql.lit(true))]),
|
||||
)
|
||||
.as('videos'),
|
||||
eb.fn
|
||||
.coalesce(eb.fn.sum('exif.fileSizeInByte').filterWhere('assets.libraryId', 'is', null), eb.lit(0))
|
||||
.coalesce(eb.fn.sum<number>('exif.fileSizeInByte').filterWhere('assets.libraryId', 'is', null), eb.lit(0))
|
||||
.as('usage'),
|
||||
eb.fn
|
||||
.coalesce(
|
||||
eb.fn
|
||||
.sum('exif.fileSizeInByte')
|
||||
.sum<number>('exif.fileSizeInByte')
|
||||
.filterWhere((eb) =>
|
||||
eb.and([eb('assets.libraryId', 'is', null), eb('assets.type', '=', AssetType.IMAGE)]),
|
||||
eb.and([eb('assets.libraryId', 'is', null), eb('assets.type', '=', sql.lit(AssetType.IMAGE))]),
|
||||
),
|
||||
eb.lit(0),
|
||||
)
|
||||
@@ -217,9 +230,9 @@ export class UserRepository {
|
||||
eb.fn
|
||||
.coalesce(
|
||||
eb.fn
|
||||
.sum('exif.fileSizeInByte')
|
||||
.sum<number>('exif.fileSizeInByte')
|
||||
.filterWhere((eb) =>
|
||||
eb.and([eb('assets.libraryId', 'is', null), eb('assets.type', '=', AssetType.VIDEO)]),
|
||||
eb.and([eb('assets.libraryId', 'is', null), eb('assets.type', '=', sql.lit(AssetType.VIDEO))]),
|
||||
),
|
||||
eb.lit(0),
|
||||
)
|
||||
@@ -228,17 +241,7 @@ export class UserRepository {
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.groupBy('users.id')
|
||||
.orderBy('users.createdAt', 'asc')
|
||||
.execute()) as UserStatsQueryResponse[];
|
||||
|
||||
for (const stat of stats) {
|
||||
stat.photos = Number(stat.photos);
|
||||
stat.videos = Number(stat.videos);
|
||||
stat.usage = Number(stat.usage);
|
||||
stat.usagePhotos = Number(stat.usagePhotos);
|
||||
stat.usageVideos = Number(stat.usageVideos);
|
||||
}
|
||||
|
||||
return stats;
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.NUMBER] })
|
||||
|
||||
12
server/src/schema/enums.ts
Normal file
12
server/src/schema/enums.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { AssetStatus, SourceType } from 'src/enum';
|
||||
import { registerEnum } from 'src/sql-tools';
|
||||
|
||||
export const assets_status_enum = registerEnum({
|
||||
name: 'assets_status_enum',
|
||||
values: Object.values(AssetStatus),
|
||||
});
|
||||
|
||||
export const asset_face_source_type = registerEnum({
|
||||
name: 'sourcetype',
|
||||
values: Object.values(SourceType),
|
||||
});
|
||||
116
server/src/schema/functions.ts
Normal file
116
server/src/schema/functions.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { registerFunction } from 'src/sql-tools';
|
||||
|
||||
export const immich_uuid_v7 = registerFunction({
|
||||
name: 'immich_uuid_v7',
|
||||
arguments: ['p_timestamp timestamp with time zone default clock_timestamp()'],
|
||||
returnType: 'uuid',
|
||||
language: 'SQL',
|
||||
behavior: 'volatile',
|
||||
body: `
|
||||
SELECT encode(
|
||||
set_bit(
|
||||
set_bit(
|
||||
overlay(uuid_send(gen_random_uuid())
|
||||
placing substring(int8send(floor(extract(epoch from p_timestamp) * 1000)::bigint) from 3)
|
||||
from 1 for 6
|
||||
),
|
||||
52, 1
|
||||
),
|
||||
53, 1
|
||||
),
|
||||
'hex')::uuid;
|
||||
`,
|
||||
synchronize: false,
|
||||
});
|
||||
|
||||
export const updated_at = registerFunction({
|
||||
name: 'updated_at',
|
||||
returnType: 'TRIGGER',
|
||||
language: 'PLPGSQL',
|
||||
body: `
|
||||
DECLARE
|
||||
clock_timestamp TIMESTAMP := clock_timestamp();
|
||||
BEGIN
|
||||
new."updatedAt" = clock_timestamp;
|
||||
new."updateId" = immich_uuid_v7(clock_timestamp);
|
||||
return new;
|
||||
END;`,
|
||||
synchronize: false,
|
||||
});
|
||||
|
||||
export const f_concat_ws = registerFunction({
|
||||
name: 'f_concat_ws',
|
||||
arguments: ['text', 'text[]'],
|
||||
returnType: 'text',
|
||||
language: 'SQL',
|
||||
parallel: 'safe',
|
||||
behavior: 'immutable',
|
||||
body: `SELECT array_to_string($2, $1)`,
|
||||
synchronize: false,
|
||||
});
|
||||
|
||||
export const f_unaccent = registerFunction({
|
||||
name: 'f_unaccent',
|
||||
arguments: ['text'],
|
||||
returnType: 'text',
|
||||
language: 'SQL',
|
||||
parallel: 'safe',
|
||||
strict: true,
|
||||
behavior: 'immutable',
|
||||
return: `unaccent('unaccent', $1)`,
|
||||
synchronize: false,
|
||||
});
|
||||
|
||||
export const ll_to_earth_public = registerFunction({
|
||||
name: 'll_to_earth_public',
|
||||
arguments: ['latitude double precision', 'longitude double precision'],
|
||||
returnType: 'public.earth',
|
||||
language: 'SQL',
|
||||
parallel: 'safe',
|
||||
strict: true,
|
||||
behavior: 'immutable',
|
||||
body: `SELECT public.cube(public.cube(public.cube(public.earth()*cos(radians(latitude))*cos(radians(longitude))),public.earth()*cos(radians(latitude))*sin(radians(longitude))),public.earth()*sin(radians(latitude)))::public.earth`,
|
||||
synchronize: false,
|
||||
});
|
||||
|
||||
export const users_delete_audit = registerFunction({
|
||||
name: 'users_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
language: 'PLPGSQL',
|
||||
body: `
|
||||
BEGIN
|
||||
INSERT INTO users_audit ("userId")
|
||||
SELECT "id"
|
||||
FROM OLD;
|
||||
RETURN NULL;
|
||||
END`,
|
||||
synchronize: false,
|
||||
});
|
||||
|
||||
export const partners_delete_audit = registerFunction({
|
||||
name: 'partners_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
language: 'PLPGSQL',
|
||||
body: `
|
||||
BEGIN
|
||||
INSERT INTO partners_audit ("sharedById", "sharedWithId")
|
||||
SELECT "sharedById", "sharedWithId"
|
||||
FROM OLD;
|
||||
RETURN NULL;
|
||||
END`,
|
||||
synchronize: false,
|
||||
});
|
||||
|
||||
export const assets_delete_audit = registerFunction({
|
||||
name: 'assets_delete_audit',
|
||||
returnType: 'TRIGGER',
|
||||
language: 'PLPGSQL',
|
||||
body: `
|
||||
BEGIN
|
||||
INSERT INTO assets_audit ("assetId", "ownerId")
|
||||
SELECT "id", "ownerId"
|
||||
FROM OLD;
|
||||
RETURN NULL;
|
||||
END`,
|
||||
synchronize: false,
|
||||
});
|
||||
109
server/src/schema/index.ts
Normal file
109
server/src/schema/index.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { asset_face_source_type, assets_status_enum } from 'src/schema/enums';
|
||||
import {
|
||||
assets_delete_audit,
|
||||
f_concat_ws,
|
||||
f_unaccent,
|
||||
immich_uuid_v7,
|
||||
ll_to_earth_public,
|
||||
partners_delete_audit,
|
||||
updated_at,
|
||||
users_delete_audit,
|
||||
} from 'src/schema/functions';
|
||||
import { ActivityTable } from 'src/schema/tables/activity.table';
|
||||
import { AlbumAssetTable } from 'src/schema/tables/album-asset.table';
|
||||
import { AlbumUserTable } from 'src/schema/tables/album-user.table';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { APIKeyTable } from 'src/schema/tables/api-key.table';
|
||||
import { AssetAuditTable } from 'src/schema/tables/asset-audit.table';
|
||||
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
|
||||
import { AssetFileTable } from 'src/schema/tables/asset-files.table';
|
||||
import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { AuditTable } from 'src/schema/tables/audit.table';
|
||||
import { ExifTable } from 'src/schema/tables/exif.table';
|
||||
import { FaceSearchTable } from 'src/schema/tables/face-search.table';
|
||||
import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table';
|
||||
import { LibraryTable } from 'src/schema/tables/library.table';
|
||||
import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||
import { MemoryAssetTable } from 'src/schema/tables/memory_asset.table';
|
||||
import { MoveTable } from 'src/schema/tables/move.table';
|
||||
import { NaturalEarthCountriesTable } from 'src/schema/tables/natural-earth-countries.table';
|
||||
import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table';
|
||||
import { PartnerTable } from 'src/schema/tables/partner.table';
|
||||
import { PersonTable } from 'src/schema/tables/person.table';
|
||||
import { SessionTable } from 'src/schema/tables/session.table';
|
||||
import { SharedLinkAssetTable } from 'src/schema/tables/shared-link-asset.table';
|
||||
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
||||
import { SmartSearchTable } from 'src/schema/tables/smart-search.table';
|
||||
import { StackTable } from 'src/schema/tables/stack.table';
|
||||
import { SessionSyncCheckpointTable } from 'src/schema/tables/sync-checkpoint.table';
|
||||
import { SystemMetadataTable } from 'src/schema/tables/system-metadata.table';
|
||||
import { TagAssetTable } from 'src/schema/tables/tag-asset.table';
|
||||
import { TagClosureTable } from 'src/schema/tables/tag-closure.table';
|
||||
import { UserAuditTable } from 'src/schema/tables/user-audit.table';
|
||||
import { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import { VersionHistoryTable } from 'src/schema/tables/version-history.table';
|
||||
import { ConfigurationParameter, Database, Extensions } from 'src/sql-tools';
|
||||
|
||||
@Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'vectors', 'plpgsql'])
|
||||
@ConfigurationParameter({ name: 'search_path', value: () => '"$user", public, vectors', scope: 'database' })
|
||||
@ConfigurationParameter({
|
||||
name: 'vectors.pgvector_compatibility',
|
||||
value: () => 'on',
|
||||
scope: 'user',
|
||||
synchronize: false,
|
||||
})
|
||||
@Database({ name: 'immich' })
|
||||
export class ImmichDatabase {
|
||||
tables = [
|
||||
ActivityTable,
|
||||
AlbumAssetTable,
|
||||
AlbumUserTable,
|
||||
AlbumTable,
|
||||
APIKeyTable,
|
||||
AssetAuditTable,
|
||||
AssetFaceTable,
|
||||
AssetJobStatusTable,
|
||||
AssetTable,
|
||||
AssetFileTable,
|
||||
AuditTable,
|
||||
ExifTable,
|
||||
FaceSearchTable,
|
||||
GeodataPlacesTable,
|
||||
LibraryTable,
|
||||
MemoryAssetTable,
|
||||
MemoryTable,
|
||||
MoveTable,
|
||||
NaturalEarthCountriesTable,
|
||||
PartnerAuditTable,
|
||||
PartnerTable,
|
||||
PersonTable,
|
||||
SessionTable,
|
||||
SharedLinkAssetTable,
|
||||
SharedLinkTable,
|
||||
SmartSearchTable,
|
||||
StackTable,
|
||||
SessionSyncCheckpointTable,
|
||||
SystemMetadataTable,
|
||||
TagAssetTable,
|
||||
TagClosureTable,
|
||||
UserAuditTable,
|
||||
UserMetadataTable,
|
||||
UserTable,
|
||||
VersionHistoryTable,
|
||||
];
|
||||
|
||||
functions = [
|
||||
immich_uuid_v7,
|
||||
updated_at,
|
||||
f_concat_ws,
|
||||
f_unaccent,
|
||||
ll_to_earth_public,
|
||||
users_delete_audit,
|
||||
partners_delete_audit,
|
||||
assets_delete_audit,
|
||||
];
|
||||
|
||||
enum = [assets_status_enum, asset_face_source_type];
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
@@ -11,10 +12,10 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
Table,
|
||||
UpdateDateColumn,
|
||||
UpdateIdColumn,
|
||||
} from 'src/sql-tools';
|
||||
|
||||
@Table('activity')
|
||||
@UpdatedAtTrigger('activity_updated_at')
|
||||
@Index({
|
||||
name: 'IDX_activity_like',
|
||||
columns: ['assetId', 'userId', 'albumId'],
|
||||
@@ -35,9 +36,14 @@ export class ActivityTable {
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@ColumnIndex('IDX_activity_update_id')
|
||||
@UpdateIdColumn()
|
||||
updateId!: string;
|
||||
@ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
albumId!: string;
|
||||
|
||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
userId!: string;
|
||||
|
||||
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
|
||||
assetId!: string | null;
|
||||
|
||||
@Column({ type: 'text', default: null })
|
||||
comment!: string | null;
|
||||
@@ -45,12 +51,7 @@ export class ActivityTable {
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isLiked!: boolean;
|
||||
|
||||
@ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
|
||||
assetId!: string | null;
|
||||
|
||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
userId!: string;
|
||||
|
||||
@ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
albumId!: string;
|
||||
@ColumnIndex('IDX_activity_update_id')
|
||||
@UpdateIdColumn()
|
||||
updateId!: string;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user