Compare commits
50 Commits
v1.107.2
...
apeman76/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27fa817ea6 | ||
|
|
4bf2ded729 | ||
|
|
fed7d0464a | ||
|
|
a0f6d7444a | ||
|
|
eb89208abb | ||
|
|
af94f0f979 | ||
|
|
025a54c462 | ||
|
|
334a709cc6 | ||
|
|
5f25e2ce82 | ||
|
|
04d0f575b7 | ||
|
|
e9683b326a | ||
|
|
cb40db9555 | ||
|
|
39221c8d1f | ||
|
|
a5467d60ea | ||
|
|
d582ec02b1 | ||
|
|
59cdbdc492 | ||
|
|
01706ccf5c | ||
|
|
3ab67886b0 | ||
|
|
2b06d4b284 | ||
|
|
34bea0190e | ||
|
|
6c49a4ba34 | ||
|
|
e1f25b44d2 | ||
|
|
f6cafa3290 | ||
|
|
53d4a5268b | ||
|
|
cf88f4b6f8 | ||
|
|
ac8d8d91f7 | ||
|
|
842291124c | ||
|
|
6f5b3c47b0 | ||
|
|
b25642b889 | ||
|
|
7bde19d842 | ||
|
|
eb1ba11d60 | ||
|
|
23b3073687 | ||
|
|
3cd187dced | ||
|
|
6791af8c2c | ||
|
|
e566fbb009 | ||
|
|
e5c92912fc | ||
|
|
f33d5b0a38 | ||
|
|
df10618a7e | ||
|
|
6030349a6f | ||
|
|
6629bf50ae | ||
|
|
e32ce82179 | ||
|
|
10ea894186 | ||
|
|
81d12c0586 | ||
|
|
0b88bef157 | ||
|
|
2b8942026c | ||
|
|
f5937a5a9b | ||
|
|
04f0ac1aad | ||
|
|
4a481acca6 | ||
|
|
de62bd3ba5 | ||
|
|
ab2ea28ed9 |
6
.github/workflows/cli.yml
vendored
6
.github/workflows/cli.yml
vendored
@@ -56,10 +56,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
uses: docker/setup-qemu-action@v3.1.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.3.0
|
||||
uses: docker/setup-buildx-action@v3.4.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.2.0
|
||||
uses: docker/build-push-action@v6.3.0
|
||||
with:
|
||||
file: cli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
@@ -63,10 +63,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
uses: docker/setup-qemu-action@v3.1.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.3.0
|
||||
uses: docker/setup-buildx-action@v3.4.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
# Only push to Docker Hub when making a release
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.2.0
|
||||
uses: docker/build-push-action@v6.3.0
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
file: ${{ matrix.file }}
|
||||
|
||||
@@ -131,4 +131,4 @@ conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
For answers to common questions about this code of conduct, see the
|
||||
FAQ at https://www.contributor-covenant.org/faq. Translations are
|
||||
available at https://www.contributor-covenant.org/translations.
|
||||
available at https://www.contributor-covenant.org/translations.
|
||||
|
||||
203
cli/package-lock.json
generated
203
cli/package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"fast-glob": "^3.3.2",
|
||||
"fastq": "^1.17.1",
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"bin": {
|
||||
@@ -34,11 +35,12 @@
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.2.2",
|
||||
"vitest-fetch-mock": "^0.2.2",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1177,17 +1179,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz",
|
||||
"integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
|
||||
"integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "7.14.1",
|
||||
"@typescript-eslint/type-utils": "7.14.1",
|
||||
"@typescript-eslint/utils": "7.14.1",
|
||||
"@typescript-eslint/visitor-keys": "7.14.1",
|
||||
"@typescript-eslint/scope-manager": "7.15.0",
|
||||
"@typescript-eslint/type-utils": "7.15.0",
|
||||
"@typescript-eslint/utils": "7.15.0",
|
||||
"@typescript-eslint/visitor-keys": "7.15.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1211,16 +1213,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz",
|
||||
"integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
|
||||
"integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.14.1",
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/typescript-estree": "7.14.1",
|
||||
"@typescript-eslint/visitor-keys": "7.14.1",
|
||||
"@typescript-eslint/scope-manager": "7.15.0",
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"@typescript-eslint/typescript-estree": "7.15.0",
|
||||
"@typescript-eslint/visitor-keys": "7.15.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1240,14 +1242,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz",
|
||||
"integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz",
|
||||
"integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/visitor-keys": "7.14.1"
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"@typescript-eslint/visitor-keys": "7.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1258,14 +1260,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz",
|
||||
"integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
|
||||
"integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "7.14.1",
|
||||
"@typescript-eslint/utils": "7.14.1",
|
||||
"@typescript-eslint/typescript-estree": "7.15.0",
|
||||
"@typescript-eslint/utils": "7.15.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
@@ -1286,9 +1288,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz",
|
||||
"integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1300,14 +1302,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz",
|
||||
"integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz",
|
||||
"integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/visitor-keys": "7.14.1",
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"@typescript-eslint/visitor-keys": "7.15.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1329,16 +1331,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz",
|
||||
"integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
|
||||
"integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "7.14.1",
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/typescript-estree": "7.14.1"
|
||||
"@typescript-eslint/scope-manager": "7.15.0",
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"@typescript-eslint/typescript-estree": "7.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1352,13 +1354,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz",
|
||||
"integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz",
|
||||
"integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1853,6 +1855,15 @@
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
|
||||
"integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.12"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@@ -3146,6 +3157,26 @@
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
||||
@@ -3369,10 +3400,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"dev": true
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@@ -3406,9 +3438,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.38",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
||||
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
|
||||
"version": "8.4.39",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
|
||||
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3424,9 +3456,10 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.0",
|
||||
"picocolors": "^1.0.1",
|
||||
"source-map-js": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3471,21 +3504,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-organize-imports": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz",
|
||||
"integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
|
||||
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@volar/vue-language-plugin-pug": "^1.0.4",
|
||||
"@volar/vue-typescript": "^1.0.4",
|
||||
"@vue/language-plugin-pug": "^2.0.24",
|
||||
"prettier": ">=2.0",
|
||||
"typescript": ">=2.9"
|
||||
"typescript": ">=2.9",
|
||||
"vue-tsc": "^2.0.24"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@volar/vue-language-plugin-pug": {
|
||||
"@vue/language-plugin-pug": {
|
||||
"optional": true
|
||||
},
|
||||
"@volar/vue-typescript": {
|
||||
"vue-tsc": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
@@ -4168,6 +4202,12 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
|
||||
@@ -4240,9 +4280,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
|
||||
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -4315,14 +4355,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz",
|
||||
"integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==",
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
|
||||
"integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss": "^8.4.39",
|
||||
"rollup": "^4.13.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4476,6 +4516,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest-fetch-mock": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.2.2.tgz",
|
||||
"integrity": "sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cross-fetch": "^3.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": ">=0.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -31,11 +31,12 @@
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.2.2",
|
||||
"vitest-fetch-mock": "^0.2.2",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -59,6 +60,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"fast-glob": "^3.3.2",
|
||||
"fastq": "^1.17.1",
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"volta": {
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { platform } from 'node:os';
|
||||
import { UploadOptionsDto, getAlbumName } from 'src/commands/asset';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
describe('Unit function tests', () => {
|
||||
import { Action, checkBulkUpload, defaults, Reason } from '@immich/sdk';
|
||||
import createFetchMock from 'vitest-fetch-mock';
|
||||
|
||||
import { checkForDuplicates, getAlbumName, uploadFiles, UploadOptionsDto } from './asset';
|
||||
|
||||
vi.mock('@immich/sdk');
|
||||
|
||||
describe('getAlbumName', () => {
|
||||
it('should return a non-undefined value', () => {
|
||||
if (platform() === 'win32') {
|
||||
if (os.platform() === 'win32') {
|
||||
// This is meaningless for Unix systems.
|
||||
expect(getAlbumName(String.raw`D:\test\Filename.txt`, {} as UploadOptionsDto)).toBe('test');
|
||||
}
|
||||
@@ -17,3 +25,177 @@ describe('Unit function tests', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadFiles', () => {
|
||||
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
|
||||
const testFilePath = path.join(testDir, 'test.png');
|
||||
const testFileData = 'test';
|
||||
const baseUrl = 'http://example.com';
|
||||
const apiKey = 'key';
|
||||
const retry = 3;
|
||||
|
||||
const fetchMocker = createFetchMock(vi);
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a test file
|
||||
fs.writeFileSync(testFilePath, testFileData);
|
||||
|
||||
// Defaults
|
||||
vi.mocked(defaults).baseUrl = baseUrl;
|
||||
vi.mocked(defaults).headers = { 'x-api-key': apiKey };
|
||||
|
||||
fetchMocker.enableMocks();
|
||||
fetchMocker.resetMocks();
|
||||
});
|
||||
|
||||
it('returns new assets when upload file is successful', async () => {
|
||||
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
|
||||
return {
|
||||
status: 200,
|
||||
body: JSON.stringify({ id: 'fc5621b1-86f6-44a1-9905-403e607df9f5', status: 'created' }),
|
||||
};
|
||||
});
|
||||
|
||||
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([
|
||||
{
|
||||
filepath: testFilePath,
|
||||
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns new assets when upload file retry is successful', async () => {
|
||||
let counter = 0;
|
||||
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
|
||||
counter++;
|
||||
if (counter < retry) {
|
||||
throw new Error('Network error');
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: JSON.stringify({ id: 'fc5621b1-86f6-44a1-9905-403e607df9f5', status: 'created' }),
|
||||
};
|
||||
});
|
||||
|
||||
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([
|
||||
{
|
||||
filepath: testFilePath,
|
||||
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns new assets when upload file retry is failed', async () => {
|
||||
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
|
||||
throw new Error('Network error');
|
||||
});
|
||||
|
||||
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkForDuplicates', () => {
|
||||
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
|
||||
const testFilePath = path.join(testDir, 'test.png');
|
||||
const testFileData = 'test';
|
||||
const testFileChecksum = 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'; // SHA1
|
||||
const retry = 3;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a test file
|
||||
fs.writeFileSync(testFilePath, testFileData);
|
||||
});
|
||||
|
||||
it('checks duplicates', async () => {
|
||||
vi.mocked(checkBulkUpload).mockResolvedValue({
|
||||
results: [
|
||||
{
|
||||
action: Action.Accept,
|
||||
id: testFilePath,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await checkForDuplicates([testFilePath], { concurrency: 1 });
|
||||
|
||||
expect(checkBulkUpload).toHaveBeenCalledWith({
|
||||
assetBulkUploadCheckDto: {
|
||||
assets: [
|
||||
{
|
||||
checksum: testFileChecksum,
|
||||
id: testFilePath,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns duplicates when check duplicates is rejected', async () => {
|
||||
vi.mocked(checkBulkUpload).mockResolvedValue({
|
||||
results: [
|
||||
{
|
||||
action: Action.Reject,
|
||||
id: testFilePath,
|
||||
assetId: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
||||
reason: Reason.Duplicate,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
||||
duplicates: [
|
||||
{
|
||||
filepath: testFilePath,
|
||||
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
||||
},
|
||||
],
|
||||
newFiles: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns new assets when check duplicates is accepted', async () => {
|
||||
vi.mocked(checkBulkUpload).mockResolvedValue({
|
||||
results: [
|
||||
{
|
||||
action: Action.Accept,
|
||||
id: testFilePath,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
||||
duplicates: [],
|
||||
newFiles: [testFilePath],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns results when check duplicates retry is successful', async () => {
|
||||
let mocked = vi.mocked(checkBulkUpload);
|
||||
for (let i = 1; i < retry; i++) {
|
||||
mocked = mocked.mockRejectedValueOnce(new Error('Network error'));
|
||||
}
|
||||
mocked.mockResolvedValue({
|
||||
results: [
|
||||
{
|
||||
action: Action.Accept,
|
||||
id: testFilePath,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
||||
duplicates: [],
|
||||
newFiles: [testFilePath],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns results when check duplicates retry is failed', async () => {
|
||||
vi.mocked(checkBulkUpload).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
||||
duplicates: [],
|
||||
newFiles: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import { chunk } from 'lodash-es';
|
||||
import { Stats, createReadStream } from 'node:fs';
|
||||
import { stat, unlink } from 'node:fs/promises';
|
||||
import path, { basename } from 'node:path';
|
||||
import { Queue } from 'src/queue';
|
||||
import { BaseOptions, authenticate, crawl, sha1 } from 'src/utils';
|
||||
|
||||
const s = (count: number) => (count === 1 ? '' : 's');
|
||||
@@ -83,7 +84,7 @@ const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => {
|
||||
return files;
|
||||
};
|
||||
|
||||
const checkForDuplicates = async (files: string[], { concurrency, skipHash }: UploadOptionsDto) => {
|
||||
export const checkForDuplicates = async (files: string[], { concurrency, skipHash }: UploadOptionsDto) => {
|
||||
if (skipHash) {
|
||||
console.log('Skipping hash check, assuming all files are new');
|
||||
return { newFiles: files, duplicates: [] };
|
||||
@@ -99,32 +100,50 @@ const checkForDuplicates = async (files: string[], { concurrency, skipHash }: Up
|
||||
const newFiles: string[] = [];
|
||||
const duplicates: Asset[] = [];
|
||||
|
||||
try {
|
||||
// TODO refactor into a queue
|
||||
for (const items of chunk(files, concurrency)) {
|
||||
const dto = await Promise.all(items.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) })));
|
||||
const { results } = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } });
|
||||
|
||||
for (const { id: filepath, assetId, action } of results as AssetBulkUploadCheckResults) {
|
||||
const queue = new Queue<string[], AssetBulkUploadCheckResults>(
|
||||
async (filepaths: string[]) => {
|
||||
const dto = await Promise.all(
|
||||
filepaths.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) })),
|
||||
);
|
||||
const response = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } });
|
||||
const results = response.results as AssetBulkUploadCheckResults;
|
||||
for (const { id: filepath, assetId, action } of results) {
|
||||
if (action === Action.Accept) {
|
||||
newFiles.push(filepath);
|
||||
} else {
|
||||
// rejects are always duplicates
|
||||
duplicates.push({ id: assetId as string, filepath });
|
||||
}
|
||||
progressBar.increment();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
progressBar.stop();
|
||||
progressBar.increment(filepaths.length);
|
||||
return results;
|
||||
},
|
||||
{ concurrency, retry: 3 },
|
||||
);
|
||||
|
||||
for (const items of chunk(files, concurrency)) {
|
||||
await queue.push(items);
|
||||
}
|
||||
|
||||
await queue.drained();
|
||||
|
||||
progressBar.stop();
|
||||
|
||||
console.log(`Found ${newFiles.length} new files and ${duplicates.length} duplicate${s(duplicates.length)}`);
|
||||
|
||||
// Report failures
|
||||
const failedTasks = queue.tasks.filter((task) => task.status === 'failed');
|
||||
if (failedTasks.length > 0) {
|
||||
console.log(`Failed to verify ${failedTasks.length} file${s(failedTasks.length)}:`);
|
||||
for (const task of failedTasks) {
|
||||
console.log(`- ${task.data} - ${task.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { newFiles, duplicates };
|
||||
};
|
||||
|
||||
const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptionsDto): Promise<Asset[]> => {
|
||||
export const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptionsDto): Promise<Asset[]> => {
|
||||
if (files.length === 0) {
|
||||
console.log('All assets were already uploaded, nothing to do.');
|
||||
return [];
|
||||
@@ -158,37 +177,52 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
|
||||
|
||||
const newAssets: Asset[] = [];
|
||||
|
||||
try {
|
||||
for (const items of chunk(files, concurrency)) {
|
||||
await Promise.all(
|
||||
items.map(async (filepath) => {
|
||||
const stats = statsMap.get(filepath) as Stats;
|
||||
const response = await uploadFile(filepath, stats);
|
||||
const queue = new Queue<string, AssetMediaResponseDto>(
|
||||
async (filepath: string) => {
|
||||
const stats = statsMap.get(filepath);
|
||||
if (!stats) {
|
||||
throw new Error(`Stats not found for ${filepath}`);
|
||||
}
|
||||
|
||||
newAssets.push({ id: response.id, filepath });
|
||||
const response = await uploadFile(filepath, stats);
|
||||
newAssets.push({ id: response.id, filepath });
|
||||
if (response.status === AssetMediaStatus.Duplicate) {
|
||||
duplicateCount++;
|
||||
duplicateSize += stats.size ?? 0;
|
||||
} else {
|
||||
successCount++;
|
||||
successSize += stats.size ?? 0;
|
||||
}
|
||||
|
||||
if (response.status === AssetMediaStatus.Duplicate) {
|
||||
duplicateCount++;
|
||||
duplicateSize += stats.size ?? 0;
|
||||
} else {
|
||||
successCount++;
|
||||
successSize += stats.size ?? 0;
|
||||
}
|
||||
uploadProgress.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) });
|
||||
|
||||
uploadProgress.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) });
|
||||
return response;
|
||||
},
|
||||
{ concurrency, retry: 3 },
|
||||
);
|
||||
|
||||
return response;
|
||||
}),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
uploadProgress.stop();
|
||||
for (const filepath of files) {
|
||||
await queue.push(filepath);
|
||||
}
|
||||
|
||||
await queue.drained();
|
||||
|
||||
uploadProgress.stop();
|
||||
|
||||
console.log(`Successfully uploaded ${successCount} new asset${s(successCount)} (${byteSize(successSize)})`);
|
||||
if (duplicateCount > 0) {
|
||||
console.log(`Skipped ${duplicateCount} duplicate asset${s(duplicateCount)} (${byteSize(duplicateSize)})`);
|
||||
}
|
||||
|
||||
// Report failures
|
||||
const failedTasks = queue.tasks.filter((task) => task.status === 'failed');
|
||||
if (failedTasks.length > 0) {
|
||||
console.log(`Failed to upload ${failedTasks.length} asset${s(failedTasks.length)}:`);
|
||||
for (const task of failedTasks) {
|
||||
console.log(`- ${task.data} - ${task.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
return newAssets;
|
||||
};
|
||||
|
||||
|
||||
131
cli/src/queue.ts
Normal file
131
cli/src/queue.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import * as fastq from 'fastq';
|
||||
import { uniqueId } from 'lodash-es';
|
||||
|
||||
export type Task<T, R> = {
|
||||
readonly id: string;
|
||||
status: 'idle' | 'processing' | 'succeeded' | 'failed';
|
||||
data: T;
|
||||
error: unknown | undefined;
|
||||
count: number;
|
||||
// TODO: Could be useful to adding progress property.
|
||||
// TODO: Could be useful to adding start_at/end_at/duration properties.
|
||||
result: undefined | R;
|
||||
};
|
||||
|
||||
export type QueueOptions = {
|
||||
verbose?: boolean;
|
||||
concurrency?: number;
|
||||
retry?: number;
|
||||
// TODO: Could be useful to adding timeout property for retry.
|
||||
};
|
||||
|
||||
export type ComputedQueueOptions = Required<QueueOptions>;
|
||||
|
||||
export const defaultQueueOptions = {
|
||||
concurrency: 1,
|
||||
retry: 0,
|
||||
verbose: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* An in-memory queue that processes tasks in parallel with a given concurrency.
|
||||
* @see {@link https://www.npmjs.com/package/fastq}
|
||||
* @template T - The type of the worker task data.
|
||||
* @template R - The type of the worker output data.
|
||||
*/
|
||||
export class Queue<T, R> {
|
||||
private readonly queue: fastq.queueAsPromised<string, Task<T, R>>;
|
||||
private readonly store = new Map<string, Task<T, R>>();
|
||||
readonly options: ComputedQueueOptions;
|
||||
readonly worker: (data: T) => Promise<R>;
|
||||
|
||||
/**
|
||||
* Create a new queue.
|
||||
* @param worker - The worker function that processes the task.
|
||||
* @param options - The queue options.
|
||||
*/
|
||||
constructor(worker: (data: T) => Promise<R>, options?: QueueOptions) {
|
||||
this.options = { ...defaultQueueOptions, ...options };
|
||||
this.worker = worker;
|
||||
this.store = new Map<string, Task<T, R>>();
|
||||
this.queue = this.buildQueue();
|
||||
}
|
||||
|
||||
get tasks(): Task<T, R>[] {
|
||||
const tasks: Task<T, R>[] = [];
|
||||
for (const task of this.store.values()) {
|
||||
tasks.push(task);
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
getTask(id: string): Task<T, R> {
|
||||
const task = this.store.get(id);
|
||||
if (!task) {
|
||||
throw new Error(`Task with id ${id} not found`);
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the queue to be empty.
|
||||
* @returns Promise<void> - The returned Promise will be resolved when all tasks in the queue have been processed by a worker.
|
||||
* This promise could be ignored as it will not lead to a `unhandledRejection`.
|
||||
*/
|
||||
async drained(): Promise<void> {
|
||||
await this.queue.drain();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a task at the end of the queue.
|
||||
* @see {@link https://www.npmjs.com/package/fastq}
|
||||
* @param data
|
||||
* @returns Promise<void> - A Promise that will be fulfilled (rejected) when the task is completed successfully (unsuccessfully).
|
||||
* This promise could be ignored as it will not lead to a `unhandledRejection`.
|
||||
*/
|
||||
async push(data: T): Promise<Task<T, R>> {
|
||||
const id = uniqueId();
|
||||
const task: Task<T, R> = { id, status: 'idle', error: undefined, count: 0, data, result: undefined };
|
||||
this.store.set(id, task);
|
||||
return this.queue.push(id);
|
||||
}
|
||||
|
||||
// TODO: Support more function delegation to fastq.
|
||||
|
||||
private buildQueue(): fastq.queueAsPromised<string, Task<T, R>> {
|
||||
return fastq.promise((id: string) => {
|
||||
const task = this.getTask(id);
|
||||
return this.work(task);
|
||||
}, this.options.concurrency);
|
||||
}
|
||||
|
||||
private async work(task: Task<T, R>): Promise<Task<T, R>> {
|
||||
task.count += 1;
|
||||
task.error = undefined;
|
||||
task.status = 'processing';
|
||||
if (this.options.verbose) {
|
||||
console.log('[task] processing:', task);
|
||||
}
|
||||
try {
|
||||
task.result = await this.worker(task.data);
|
||||
task.status = 'succeeded';
|
||||
if (this.options.verbose) {
|
||||
console.log('[task] succeeded:', task);
|
||||
}
|
||||
return task;
|
||||
} catch (error) {
|
||||
task.error = error;
|
||||
task.status = 'failed';
|
||||
if (this.options.verbose) {
|
||||
console.log('[task] failed:', task);
|
||||
}
|
||||
if (this.options.retry > 0 && task.count < this.options.retry) {
|
||||
if (this.options.verbose) {
|
||||
console.log('[task] retry:', task);
|
||||
}
|
||||
return this.work(task);
|
||||
}
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,6 @@ Regardless of filesystem, it is not recommended to use a network share for your
|
||||
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
||||
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
||||
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api |
|
||||
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices |
|
||||
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
||||
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
|
||||
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
||||
|
||||
22
docs/package-lock.json
generated
22
docs/package-lock.json
generated
@@ -12640,9 +12640,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@@ -12753,9 +12754,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.38",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
||||
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
|
||||
"version": "8.4.39",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
|
||||
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -12770,9 +12771,10 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.0",
|
||||
"picocolors": "^1.0.1",
|
||||
"source-map-js": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -16374,9 +16376,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
|
||||
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
@tailwind utilities;
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Snowburst+One&display=swap');
|
||||
|
||||
html,
|
||||
button {
|
||||
@@ -48,7 +47,3 @@ img {
|
||||
div[class^='announcementBar_'] {
|
||||
min-height: 2rem;
|
||||
}
|
||||
|
||||
.navbar__brand .navbar__title {
|
||||
@apply font-immich-title text-2xl font-normal text-immich-primary dark:text-immich-dark-primary;
|
||||
}
|
||||
|
||||
77
docs/src/pages/cursed-knowledge.tsx
Normal file
77
docs/src/pages/cursed-knowledge.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { mdiCalendarToday, mdiLeadPencil, mdiLockOutline, mdiSpeedometerSlow, mdiWeb } from '@mdi/js';
|
||||
import Layout from '@theme/Layout';
|
||||
import React from 'react';
|
||||
import { Item as TimelineItem, Timeline } from '../components/timeline';
|
||||
|
||||
const withLanguage = (date: Date) => (language: string) => date.toLocaleDateString(language);
|
||||
|
||||
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
icon: mdiLeadPencil,
|
||||
iconColor: 'gold',
|
||||
title: 'PostgreSQL NOTIFY is cursed',
|
||||
description:
|
||||
'PostgreSQL does everything in a transaction, including NOTIFY. This means using the socket.io postgres-adapter writes to WAL every 5 seconds.',
|
||||
link: { url: 'https://github.com/immich-app/immich/pull/10801', text: '#10801' },
|
||||
date: new Date(2024, 6, 3),
|
||||
},
|
||||
{
|
||||
icon: mdiWeb,
|
||||
iconColor: 'lightskyblue',
|
||||
title: 'npm scripts are cursed',
|
||||
description:
|
||||
'npm scripts make a http call to the npm registry each time they run, which means they are a terrible way to execute a health check.',
|
||||
link: { url: 'https://github.com/immich-app/immich/issues/10796', text: '#10796' },
|
||||
date: new Date(2024, 6, 3),
|
||||
},
|
||||
{
|
||||
icon: mdiSpeedometerSlow,
|
||||
iconColor: 'brown',
|
||||
title: '50 extra packages are cursed',
|
||||
description:
|
||||
'There is a user in the JavaScript community who goes around adding "backwards compatibility" to projects. They do this by adding 50 extra package dependencies to your project, which are maintained by them.',
|
||||
link: { url: 'https://github.com/immich-app/immich/pull/10690', text: '#10690' },
|
||||
date: new Date(2024, 5, 28),
|
||||
},
|
||||
{
|
||||
icon: mdiLockOutline,
|
||||
iconColor: 'gold',
|
||||
title: 'Long passwords are cursed',
|
||||
description:
|
||||
'The bcrypt implementation only uses the first 72 bytes of a string. Any characters after that are ignored.',
|
||||
// link: GHSA-4p64-9f7h-3432
|
||||
date: new Date(2024, 5, 25),
|
||||
},
|
||||
{
|
||||
icon: mdiCalendarToday,
|
||||
iconColor: 'greenyellow',
|
||||
title: 'JavaScript Date objects are cursed',
|
||||
description: 'JavaScript date objects are 1 indexed for years and days, but 0 indexed for months.',
|
||||
link: { url: 'https://github.com/immich-app/immich/pulls/6787', text: '#6787' },
|
||||
date: new Date(2024, 0, 31),
|
||||
},
|
||||
];
|
||||
|
||||
export default function CursedKnowledgePage(): JSX.Element {
|
||||
return (
|
||||
<Layout title="Cursed Knowledge" description="Things we wish we didn't know">
|
||||
<section className="my-8">
|
||||
<h1 className="md:text-6xl text-center mb-10 text-immich-primary dark:text-immich-dark-primary px-2">
|
||||
Cursed Knowledge
|
||||
</h1>
|
||||
<p className="text-center text-xl px-2">
|
||||
Cursed knowledge we have learned as a result of building Immich that we wish we never knew.
|
||||
</p>
|
||||
<div className="flex justify-around mt-8 w-full max-w-full">
|
||||
<Timeline
|
||||
items={items
|
||||
.sort((a, b) => b.date.getTime() - a.date.getTime())
|
||||
.map((item) => ({ ...item, getDateLabel: withLanguage(item.date) }))}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
@@ -21,9 +21,6 @@ module.exports = {
|
||||
'immich-dark-fg': '#e5e7eb',
|
||||
'immich-dark-gray': '#212121',
|
||||
},
|
||||
fontFamily: {
|
||||
'immich-title': ['Snowburst One', 'cursive'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
||||
133
e2e/package-lock.json
generated
133
e2e/package-lock.json
generated
@@ -29,7 +29,7 @@
|
||||
"pg": "^8.11.3",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
@@ -68,7 +68,7 @@
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
@@ -971,13 +971,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.45.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.0.tgz",
|
||||
"integrity": "sha512-TVYsfMlGAaxeUllNkywbwek67Ncf8FRGn8ZlRdO291OL3NjG9oMbfVhyP82HQF0CZLMrYsvesqoUekxdWuF9Qw==",
|
||||
"version": "1.45.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz",
|
||||
"integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.45.0"
|
||||
"playwright": "1.45.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -1345,17 +1345,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz",
|
||||
"integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
|
||||
"integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "7.14.1",
|
||||
"@typescript-eslint/type-utils": "7.14.1",
|
||||
"@typescript-eslint/utils": "7.14.1",
|
||||
"@typescript-eslint/visitor-keys": "7.14.1",
|
||||
"@typescript-eslint/scope-manager": "7.15.0",
|
||||
"@typescript-eslint/type-utils": "7.15.0",
|
||||
"@typescript-eslint/utils": "7.15.0",
|
||||
"@typescript-eslint/visitor-keys": "7.15.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1379,16 +1379,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz",
|
||||
"integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
|
||||
"integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.14.1",
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/typescript-estree": "7.14.1",
|
||||
"@typescript-eslint/visitor-keys": "7.14.1",
|
||||
"@typescript-eslint/scope-manager": "7.15.0",
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"@typescript-eslint/typescript-estree": "7.15.0",
|
||||
"@typescript-eslint/visitor-keys": "7.15.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1408,14 +1408,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz",
|
||||
"integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz",
|
||||
"integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/visitor-keys": "7.14.1"
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"@typescript-eslint/visitor-keys": "7.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1426,14 +1426,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz",
|
||||
"integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
|
||||
"integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "7.14.1",
|
||||
"@typescript-eslint/utils": "7.14.1",
|
||||
"@typescript-eslint/typescript-estree": "7.15.0",
|
||||
"@typescript-eslint/utils": "7.15.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
@@ -1454,9 +1454,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz",
|
||||
"integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1468,14 +1468,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz",
|
||||
"integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz",
|
||||
"integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/visitor-keys": "7.14.1",
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"@typescript-eslint/visitor-keys": "7.15.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1523,16 +1523,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz",
|
||||
"integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
|
||||
"integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "7.14.1",
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/typescript-estree": "7.14.1"
|
||||
"@typescript-eslint/scope-manager": "7.15.0",
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"@typescript-eslint/typescript-estree": "7.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -1546,13 +1546,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz",
|
||||
"integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz",
|
||||
"integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4263,13 +4263,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.45.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.0.tgz",
|
||||
"integrity": "sha512-4z3ac3plDfYzGB6r0Q3LF8POPR20Z8D0aXcxbJvmfMgSSq1hkcgvFRXJk9rUq5H/MJ0Ktal869hhOdI/zUTeLA==",
|
||||
"version": "1.45.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz",
|
||||
"integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.45.0"
|
||||
"playwright-core": "1.45.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -4282,9 +4282,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.45.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.0.tgz",
|
||||
"integrity": "sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ==",
|
||||
"version": "1.45.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz",
|
||||
"integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -4423,21 +4423,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-organize-imports": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz",
|
||||
"integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
|
||||
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@volar/vue-language-plugin-pug": "^1.0.4",
|
||||
"@volar/vue-typescript": "^1.0.4",
|
||||
"@vue/language-plugin-pug": "^2.0.24",
|
||||
"prettier": ">=2.0",
|
||||
"typescript": ">=2.9"
|
||||
"typescript": ">=2.9",
|
||||
"vue-tsc": "^2.0.24"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@volar/vue-language-plugin-pug": {
|
||||
"@vue/language-plugin-pug": {
|
||||
"optional": true
|
||||
},
|
||||
"@volar/vue-typescript": {
|
||||
"vue-tsc": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
@@ -5271,9 +5272,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
|
||||
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"pg": "^8.11.3",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
|
||||
@@ -9,11 +9,30 @@ describe(`immich-admin`, () => {
|
||||
|
||||
describe('list-users', () => {
|
||||
it('should list the admin user', async () => {
|
||||
const { stdout, stderr, exitCode } = await immichAdmin(['list-users']);
|
||||
const { stdout, stderr, exitCode } = await immichAdmin(['list-users']).promise;
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe('');
|
||||
expect(stdout).toContain("email: 'admin@immich.cloud'");
|
||||
expect(stdout).toContain("name: 'Immich Admin'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset-admin-password', () => {
|
||||
it('should reset admin password', async () => {
|
||||
const { child, promise } = immichAdmin(['reset-admin-password']);
|
||||
|
||||
let data = '';
|
||||
child.stdout.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
if (data.includes('Please choose a new password (optional)')) {
|
||||
child.stdin.end('\n');
|
||||
}
|
||||
});
|
||||
|
||||
const { stderr, stdout, exitCode } = await promise;
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe('');
|
||||
expect(stdout).toContain('The admin password has been updated to:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,13 +64,13 @@ export const tempDir = tmpdir();
|
||||
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
|
||||
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
||||
export const immichCli = (args: string[]) =>
|
||||
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]);
|
||||
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]).promise;
|
||||
export const immichAdmin = (args: string[]) =>
|
||||
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
||||
|
||||
const executeCommand = (command: string, args: string[]) => {
|
||||
let _resolve: (value: CommandResponse) => void;
|
||||
const deferred = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
|
||||
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
|
||||
const child = spawn(command, args, { stdio: 'pipe' });
|
||||
|
||||
let stdout = '';
|
||||
@@ -86,7 +86,7 @@ const executeCommand = (command: string, args: string[]) => {
|
||||
});
|
||||
});
|
||||
|
||||
return deferred;
|
||||
return { promise, child };
|
||||
};
|
||||
|
||||
let client: pg.Client | null = null;
|
||||
@@ -152,10 +152,6 @@ export const utils = {
|
||||
|
||||
const sql: string[] = [];
|
||||
|
||||
if (tables.includes('asset_stack')) {
|
||||
sql.push('UPDATE "assets" SET "stackId" = NULL;');
|
||||
}
|
||||
|
||||
for (const table of tables) {
|
||||
if (table === 'system_metadata') {
|
||||
// prevent reverse geocoder from being re-initialized
|
||||
|
||||
@@ -51,6 +51,13 @@ test.describe('Shared Links', () => {
|
||||
await page.getByText('DOWNLOADING', { exact: true }).waitFor();
|
||||
});
|
||||
|
||||
test('download all from shared link', async ({ page }) => {
|
||||
await page.goto(`/share/${sharedLink.key}`);
|
||||
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
||||
await page.getByRole('button', { name: 'Download' }).click();
|
||||
await page.getByText('DOWNLOADING', { exact: true }).waitFor();
|
||||
});
|
||||
|
||||
test('enter password for a shared link', async ({ page }) => {
|
||||
await page.goto(`/share/${sharedLinkPassword.key}`);
|
||||
await page.getByPlaceholder('Password').fill('test-password');
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146EE1CF9000F007C117D /* Immich-Debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Immich-Debug.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
@@ -118,7 +118,7 @@
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
97C146EE1CF9000F007C117D /* Immich-Debug.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -162,7 +162,7 @@
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
@@ -393,8 +393,8 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.107.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile;
|
||||
PRODUCT_NAME = "Immich-Profile";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -535,8 +535,8 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.107.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.debug;
|
||||
PRODUCT_NAME = "Immich-Debug";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -564,7 +564,7 @@
|
||||
);
|
||||
MARKETING_VERSION = 1.107.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PRODUCT_NAME = Immich;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BuildableName = "Immich.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -31,7 +31,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BuildableName = "Immich.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -56,7 +56,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BuildableName = "Immich.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -73,7 +73,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BuildableName = "Immich.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
||||
@@ -1,124 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>app.alextran.immich.backgroundFetch</string>
|
||||
<string>app.alextran.immich.backgroundProcessing</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true />
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Immich</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
<string>ar</string>
|
||||
<string>ca</string>
|
||||
<string>cs</string>
|
||||
<string>da</string>
|
||||
<string>de</string>
|
||||
<string>es</string>
|
||||
<string>fi</string>
|
||||
<string>fr</string>
|
||||
<string>he</string>
|
||||
<string>hi</string>
|
||||
<string>hu</string>
|
||||
<string>it</string>
|
||||
<string>ja</string>
|
||||
<string>ko</string>
|
||||
<string>lv</string>
|
||||
<string>mn</string>
|
||||
<string>nb</string>
|
||||
<string>nl</string>
|
||||
<string>pl</string>
|
||||
<string>pt</string>
|
||||
<string>ro</string>
|
||||
<string>ru</string>
|
||||
<string>sk</string>
|
||||
<string>sl</string>
|
||||
<string>sr</string>
|
||||
<string>sv</string>
|
||||
<string>th</string>
|
||||
<string>uk</string>
|
||||
<string>vi</string>
|
||||
<string>zh</string>
|
||||
</array>
|
||||
<key>CFBundleName</key>
|
||||
<string>immich_mobile</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.107.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>162</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true />
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false />
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true />
|
||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||
<true />
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true />
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Enable location setting to show position of assets on map</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>We need to manage backup your photos album</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We need to manage backup your photos album</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true />
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false />
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true />
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true />
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>app.alextran.immich.backgroundFetch</string>
|
||||
<string>app.alextran.immich.backgroundProcessing</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
<string>ar</string>
|
||||
<string>ca</string>
|
||||
<string>cs</string>
|
||||
<string>da</string>
|
||||
<string>de</string>
|
||||
<string>es</string>
|
||||
<string>fi</string>
|
||||
<string>fr</string>
|
||||
<string>he</string>
|
||||
<string>hi</string>
|
||||
<string>hu</string>
|
||||
<string>it</string>
|
||||
<string>ja</string>
|
||||
<string>ko</string>
|
||||
<string>lv</string>
|
||||
<string>mn</string>
|
||||
<string>nb</string>
|
||||
<string>nl</string>
|
||||
<string>pl</string>
|
||||
<string>pt</string>
|
||||
<string>ro</string>
|
||||
<string>ru</string>
|
||||
<string>sk</string>
|
||||
<string>sl</string>
|
||||
<string>sr</string>
|
||||
<string>sv</string>
|
||||
<string>th</string>
|
||||
<string>uk</string>
|
||||
<string>vi</string>
|
||||
<string>zh</string>
|
||||
</array>
|
||||
<key>CFBundleName</key>
|
||||
<string>immich_mobile</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.107.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>162</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Enable location setting to show position of assets on map</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>We need to manage backup your photos album</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>We need to manage backup your photos album</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -192,7 +192,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: context.locale,
|
||||
debugShowCheckedModeBanner: false,
|
||||
debugShowCheckedModeBanner: true,
|
||||
home: MaterialApp.router(
|
||||
title: 'Immich',
|
||||
debugShowCheckedModeBanner: false,
|
||||
|
||||
@@ -199,9 +199,12 @@ class SearchInputPage extends HookConsumerWidget {
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: LocationPicker(
|
||||
onSelected: handleOnSelect,
|
||||
filter: filter.value.location,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: LocationPicker(
|
||||
onSelected: handleOnSelect,
|
||||
filter: filter.value.location,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -242,7 +245,7 @@ class SearchInputPage extends HookConsumerWidget {
|
||||
onSearch: search,
|
||||
onClear: handleClear,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: CameraPicker(
|
||||
onSelect: handleOnSelect,
|
||||
filter: filter.value.camera,
|
||||
|
||||
@@ -14,17 +14,6 @@ final partnerServiceProvider = Provider(
|
||||
),
|
||||
);
|
||||
|
||||
enum PartnerDirection {
|
||||
sharedWith("shared-with"),
|
||||
sharedBy("shared-by");
|
||||
|
||||
const PartnerDirection(
|
||||
this._value,
|
||||
);
|
||||
|
||||
final String _value;
|
||||
}
|
||||
|
||||
class PartnerService {
|
||||
final ApiService _apiService;
|
||||
final Isar _db;
|
||||
@@ -34,8 +23,7 @@ class PartnerService {
|
||||
|
||||
Future<List<User>?> getPartners(PartnerDirection direction) async {
|
||||
try {
|
||||
final userDtos =
|
||||
await _apiService.partnersApi.getPartners(direction._value);
|
||||
final userDtos = await _apiService.partnersApi.getPartners(direction);
|
||||
if (userDtos != null) {
|
||||
return userDtos.map((u) => User.fromPartnerDto(u)).toList();
|
||||
}
|
||||
|
||||
@@ -73,9 +73,9 @@ class UserService {
|
||||
Future<List<User>?> getUsersFromServer() async {
|
||||
final List<User>? users = await _getAllUsers();
|
||||
final List<User>? sharedBy =
|
||||
await _partnerService.getPartners(PartnerDirection.sharedBy);
|
||||
await _partnerService.getPartners(PartnerDirection.by);
|
||||
final List<User>? sharedWith =
|
||||
await _partnerService.getPartners(PartnerDirection.sharedWith);
|
||||
await _partnerService.getPartners(PartnerDirection.with_);
|
||||
|
||||
if (users == null || sharedBy == null || sharedWith == null) {
|
||||
_log.warning("Failed to refresh users");
|
||||
|
||||
@@ -194,7 +194,7 @@ class ExifBottomSheet extends HookConsumerWidget {
|
||||
exifInfo.mm != null ||
|
||||
exifInfo.iso != null
|
||||
? Text(
|
||||
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} s ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
|
||||
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
|
||||
style: context.textTheme.bodySmall,
|
||||
)
|
||||
: null,
|
||||
|
||||
@@ -49,7 +49,7 @@ class ExifDetail extends StatelessWidget {
|
||||
exifInfo?.mm != null ||
|
||||
exifInfo?.iso != null
|
||||
? Text(
|
||||
"ƒ/${exifInfo?.fNumber} ${exifInfo?.exposureTime} s ${exifInfo?.focalLength} mm ISO ${exifInfo?.iso ?? ''} ",
|
||||
"ƒ/${exifInfo?.fNumber} ${exifInfo?.exposureTime} ${exifInfo?.focalLength} mm ISO ${exifInfo?.iso ?? ''} ",
|
||||
style: context.textTheme.bodySmall,
|
||||
)
|
||||
: null,
|
||||
|
||||
@@ -38,10 +38,7 @@ class FilterBottomSheetScaffold extends StatelessWidget {
|
||||
style: context.textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: buildChildWidget(),
|
||||
),
|
||||
buildChildWidget(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
|
||||
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@@ -353,6 +353,7 @@ Class | Method | HTTP request | Description
|
||||
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
|
||||
- [OAuthConfigDto](doc//OAuthConfigDto.md)
|
||||
- [OnThisDayDto](doc//OnThisDayDto.md)
|
||||
- [PartnerDirection](doc//PartnerDirection.md)
|
||||
- [PartnerResponseDto](doc//PartnerResponseDto.md)
|
||||
- [PathEntityType](doc//PathEntityType.md)
|
||||
- [PathType](doc//PathType.md)
|
||||
|
||||
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@@ -166,6 +166,7 @@ part 'model/o_auth_authorize_response_dto.dart';
|
||||
part 'model/o_auth_callback_dto.dart';
|
||||
part 'model/o_auth_config_dto.dart';
|
||||
part 'model/on_this_day_dto.dart';
|
||||
part 'model/partner_direction.dart';
|
||||
part 'model/partner_response_dto.dart';
|
||||
part 'model/path_entity_type.dart';
|
||||
part 'model/path_type.dart';
|
||||
|
||||
8
mobile/openapi/lib/api/partners_api.dart
generated
8
mobile/openapi/lib/api/partners_api.dart
generated
@@ -67,8 +67,8 @@ class PartnersApi {
|
||||
/// Performs an HTTP 'GET /partners' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] direction (required):
|
||||
Future<Response> getPartnersWithHttpInfo(String direction,) async {
|
||||
/// * [PartnerDirection] direction (required):
|
||||
Future<Response> getPartnersWithHttpInfo(PartnerDirection direction,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/partners';
|
||||
|
||||
@@ -97,8 +97,8 @@ class PartnersApi {
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] direction (required):
|
||||
Future<List<PartnerResponseDto>?> getPartners(String direction,) async {
|
||||
/// * [PartnerDirection] direction (required):
|
||||
Future<List<PartnerResponseDto>?> getPartners(PartnerDirection direction,) async {
|
||||
final response = await getPartnersWithHttpInfo(direction,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
|
||||
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@@ -390,6 +390,8 @@ class ApiClient {
|
||||
return OAuthConfigDto.fromJson(value);
|
||||
case 'OnThisDayDto':
|
||||
return OnThisDayDto.fromJson(value);
|
||||
case 'PartnerDirection':
|
||||
return PartnerDirectionTypeTransformer().decode(value);
|
||||
case 'PartnerResponseDto':
|
||||
return PartnerResponseDto.fromJson(value);
|
||||
case 'PathEntityType':
|
||||
|
||||
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
@@ -103,6 +103,9 @@ String parameterToString(dynamic value) {
|
||||
if (value is MemoryType) {
|
||||
return MemoryTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is PartnerDirection) {
|
||||
return PartnerDirectionTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is PathEntityType) {
|
||||
return PathEntityTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
85
mobile/openapi/lib/model/partner_direction.dart
generated
Normal file
85
mobile/openapi/lib/model/partner_direction.dart
generated
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class PartnerDirection {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const PartnerDirection._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const by = PartnerDirection._(r'shared-by');
|
||||
static const with_ = PartnerDirection._(r'shared-with');
|
||||
|
||||
/// List of all possible values in this [enum][PartnerDirection].
|
||||
static const values = <PartnerDirection>[
|
||||
by,
|
||||
with_,
|
||||
];
|
||||
|
||||
static PartnerDirection? fromJson(dynamic value) => PartnerDirectionTypeTransformer().decode(value);
|
||||
|
||||
static List<PartnerDirection> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PartnerDirection>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PartnerDirection.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [PartnerDirection] to String,
|
||||
/// and [decode] dynamic data back to [PartnerDirection].
|
||||
class PartnerDirectionTypeTransformer {
|
||||
factory PartnerDirectionTypeTransformer() => _instance ??= const PartnerDirectionTypeTransformer._();
|
||||
|
||||
const PartnerDirectionTypeTransformer._();
|
||||
|
||||
String encode(PartnerDirection data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a PartnerDirection.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
PartnerDirection? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'shared-by': return PartnerDirection.by;
|
||||
case r'shared-with': return PartnerDirection.with_;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [PartnerDirectionTypeTransformer] instance.
|
||||
static PartnerDirectionTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
@@ -3660,11 +3660,7 @@
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"shared-by",
|
||||
"shared-with"
|
||||
],
|
||||
"type": "string"
|
||||
"$ref": "#/components/schemas/PartnerDirection"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -9473,6 +9469,13 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PartnerDirection": {
|
||||
"enum": [
|
||||
"shared-by",
|
||||
"shared-with"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PartnerResponseDto": {
|
||||
"properties": {
|
||||
"avatarColor": {
|
||||
|
||||
6
open-api/typescript-sdk/package-lock.json
generated
6
open-api/typescript-sdk/package-lock.json
generated
@@ -32,9 +32,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
|
||||
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -2128,7 +2128,7 @@ export function unlinkOAuthAccount(opts?: Oazapfts.RequestOpts) {
|
||||
}));
|
||||
}
|
||||
export function getPartners({ direction }: {
|
||||
direction: "shared-by" | "shared-with";
|
||||
direction: PartnerDirection;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
@@ -3131,6 +3131,10 @@ export enum Type2 {
|
||||
export enum MemoryType {
|
||||
OnThisDay = "on_this_day"
|
||||
}
|
||||
export enum PartnerDirection {
|
||||
SharedBy = "shared-by",
|
||||
SharedWith = "shared-with"
|
||||
}
|
||||
export enum PathEntityType {
|
||||
Asset = "asset",
|
||||
Person = "person",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
- ⚠️ Volg altijd het [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan voor je kostbare foto's en video's!
|
||||
|
||||
## Inhoud
|
||||
s
|
||||
|
||||
- [Officiële documentatie](https://immich.app/docs)
|
||||
- [Toekomstplannen](https://github.com/orgs/immich-app/projects/1)
|
||||
- [Demo](#demo)
|
||||
@@ -57,24 +57,15 @@ De belangrijkste documentatie, inclusief installatie handleidingen, zijn te vind
|
||||
|
||||
## Demo
|
||||
|
||||
De demo is te bekijken op https://demo.immich.app.
|
||||
Je kunt de demo [hier](https://demo.immich.app/) bekijken. De demo server is actief op een Free-tier Oracle VM in Amsterdam met een 2.4GHz quad-core ARM64 CPU en 24GB RAM.
|
||||
|
||||
Voor de mobiele app kunt u gebruik maken van `https://demo.immich.app/api` voor de `Server Endpoint URL`
|
||||
Voor de mobiele app kun je gebruik maken van `https://demo.immich.app/api` voor de `Server Endpoint URL`
|
||||
|
||||
```bash title="Demo inloggegevens"
|
||||
De inloggegevens
|
||||
email: demo@immich.app
|
||||
wachtwoord: demo
|
||||
```
|
||||
|
||||
```
|
||||
Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
```
|
||||
|
||||
## Activiteit
|
||||
|
||||
 | Ja | Ja |
|
||||
| Offline-ondersteuning | Ja | Nee |
|
||||
| Alleen-lezen galerij | Ja | Ja |
|
||||
| Gestapelde foto's | Ja | Ja |
|
||||
|
||||
## Contributie-leden
|
||||
## Vertalingen
|
||||
|
||||
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||
</a>
|
||||
Je kunt [hier](https://immich.app/docs/developer/translations) meer over vertalingen lezen.
|
||||
|
||||
## Repository activiteit
|
||||
|
||||

|
||||
|
||||
## Ster geschiedenis
|
||||
|
||||
@@ -123,3 +117,9 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Contributie-leden
|
||||
|
||||
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:20240625@sha256:3899114e00a55cb4138b02e1a5da4d2bd8d401a30c72a706134e0d5567c296eb as dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:20240708@sha256:2a9e3231c34493cb861299d475c84c031e7f04519dbc895bbebb5017d479a3cb as dev
|
||||
|
||||
RUN apt-get install --no-install-recommends -yqq tini
|
||||
WORKDIR /usr/src/app
|
||||
@@ -41,7 +41,7 @@ RUN npm run build
|
||||
|
||||
|
||||
# prod build
|
||||
FROM ghcr.io/immich-app/base-server-prod:20240625@sha256:19d095589b75bee07c4fd72f53a25c527cd6c5b78162a723d2d8db8e7cd51483
|
||||
FROM ghcr.io/immich-app/base-server-prod:20240708@sha256:0af3a5bb036c9a4b6a5a51becaa6e94fe182f6bc97480d57e8f2e6f994bfa453
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
@@ -50,7 +50,7 @@ ENV NODE_ENV=production \
|
||||
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=prod /usr/src/app/dist ./dist
|
||||
COPY --from=prod /usr/src/app/bin ./bin
|
||||
COPY --from=web /usr/src/app/build ./www
|
||||
COPY --from=web /usr/src/app/build /build/www
|
||||
COPY server/resources resources
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
COPY server/start*.sh ./
|
||||
@@ -80,4 +80,4 @@ EXPOSE 3001
|
||||
ENTRYPOINT ["tini", "--", "/bin/bash"]
|
||||
CMD ["start.sh"]
|
||||
|
||||
HEALTHCHECK CMD npm run healthcheck
|
||||
HEALTHCHECK CMD immich-healthcheck
|
||||
|
||||
3
server/bin/immich-healthcheck
Executable file
3
server/bin/immich-healthcheck
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
node /usr/src/app/dist/utils/healthcheck.js
|
||||
3184
server/package-lock.json
generated
3184
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -50,8 +50,8 @@
|
||||
"@opentelemetry/context-async-hooks": "^1.24.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.52.0",
|
||||
"@opentelemetry/sdk-node": "^0.52.0",
|
||||
"@react-email/components": "^0.0.19",
|
||||
"@socket.io/postgres-adapter": "^0.3.1",
|
||||
"@react-email/components": "^0.0.21",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"archiver": "^7.0.0",
|
||||
"async-lock": "^1.4.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
@@ -121,7 +121,7 @@
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.0.2",
|
||||
"prettier-plugin-organize-imports": "^3.2.3",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"rimraf": "^5.0.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"sql-formatter": "^15.0.0",
|
||||
|
||||
@@ -384,7 +384,7 @@ export const immichAppConfig: ConfigModuleOptions = {
|
||||
}),
|
||||
};
|
||||
|
||||
function parseRedisConfig(): RedisOptions {
|
||||
export function parseRedisConfig(): RedisOptions {
|
||||
const redisUrl = process.env.REDIS_URL;
|
||||
if (redisUrl && redisUrl.startsWith('ioredis://')) {
|
||||
try {
|
||||
|
||||
@@ -27,13 +27,28 @@ export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www';
|
||||
const HOST_SERVER_PORT = process.env.IMMICH_PORT || '2283';
|
||||
export const DEFAULT_EXTERNAL_DOMAIN = 'http://localhost:' + HOST_SERVER_PORT;
|
||||
|
||||
const GEODATA_ROOT_PATH = process.env.IMMICH_REVERSE_GEOCODING_ROOT || '/usr/src/resources';
|
||||
|
||||
export const citiesFile = 'cities500.txt';
|
||||
export const geodataDatePath = join(GEODATA_ROOT_PATH, 'geodata-date.txt');
|
||||
export const geodataAdmin1Path = join(GEODATA_ROOT_PATH, 'admin1CodesASCII.txt');
|
||||
export const geodataAdmin2Path = join(GEODATA_ROOT_PATH, 'admin2Codes.txt');
|
||||
export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile);
|
||||
|
||||
const buildFolder = process.env.IMMICH_BUILD_DATA || '/build';
|
||||
|
||||
const folders = {
|
||||
geodata: join(buildFolder, 'geodata'),
|
||||
web: join(buildFolder, 'www'),
|
||||
};
|
||||
|
||||
export const resourcePaths = {
|
||||
lockFile: join(buildFolder, 'build-lock.json'),
|
||||
geodata: {
|
||||
dateFile: join(folders.geodata, 'geodata-date.txt'),
|
||||
admin1: join(folders.geodata, 'admin1CodesASCII.txt'),
|
||||
admin2: join(folders.geodata, 'admin2Codes.txt'),
|
||||
cities500: join(folders.geodata, citiesFile),
|
||||
},
|
||||
web: {
|
||||
root: folders.web,
|
||||
indexHtml: join(folders.web, 'index.html'),
|
||||
},
|
||||
};
|
||||
|
||||
export const MOBILE_REDIRECT = 'app.immich:/';
|
||||
export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
|
||||
import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
|
||||
import { PartnerDirection } from 'src/interfaces/partner.interface';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { PartnerService } from 'src/services/partner.service';
|
||||
@@ -16,8 +16,8 @@ export class PartnerController {
|
||||
@ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true })
|
||||
@Authenticated()
|
||||
// TODO: remove 'direction' and convert to full query dto
|
||||
getPartners(@Auth() auth: AuthDto, @Query('direction') direction: PartnerDirection): Promise<PartnerResponseDto[]> {
|
||||
return this.service.getAll(auth, direction);
|
||||
getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise<PartnerResponseDto[]> {
|
||||
return this.service.search(auth, dto);
|
||||
}
|
||||
|
||||
@Post(':id')
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty } from 'class-validator';
|
||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { PartnerDirection } from 'src/interfaces/partner.interface';
|
||||
|
||||
export class UpdatePartnerDto {
|
||||
@IsNotEmpty()
|
||||
inTimeline!: boolean;
|
||||
}
|
||||
|
||||
export class PartnerSearchDto {
|
||||
@IsEnum(PartnerDirection)
|
||||
@ApiProperty({ enum: PartnerDirection, enumName: 'PartnerDirection' })
|
||||
direction!: PartnerDirection;
|
||||
}
|
||||
|
||||
export class PartnerResponseDto extends UserResponseDto {
|
||||
inTimeline?: boolean;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from '
|
||||
import { UserAvatarColor, UserMetadataEntity, UserMetadataKey } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity, UserStatus } from 'src/entities/user.entity';
|
||||
import { getPreferences } from 'src/utils/preferences';
|
||||
import { Optional, toEmail, toSanitized, ValidateBoolean } from 'src/validation';
|
||||
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
|
||||
|
||||
export class UserUpdateMeDto {
|
||||
@Optional()
|
||||
@@ -140,7 +140,7 @@ export class UserAdminResponseDto extends UserResponseDto {
|
||||
}
|
||||
|
||||
export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto {
|
||||
const license = entity.metadata.find(
|
||||
const license = entity.metadata?.find(
|
||||
(item): item is UserMetadataEntity<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
|
||||
)?.value;
|
||||
return {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
import { AssetStackEntity } from 'src/entities/asset-stack.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { SmartInfoEntity } from 'src/entities/smart-info.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 {
|
||||
@@ -164,9 +164,9 @@ export class AssetEntity {
|
||||
@Column({ nullable: true })
|
||||
stackId?: string | null;
|
||||
|
||||
@ManyToOne(() => AssetStackEntity, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' })
|
||||
@ManyToOne(() => StackEntity, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' })
|
||||
@JoinColumn()
|
||||
stack?: AssetStackEntity | null;
|
||||
stack?: StackEntity | null;
|
||||
|
||||
@OneToOne(() => AssetJobStatusEntity, (jobStatus) => jobStatus.asset, { nullable: true })
|
||||
jobStatus?: AssetJobStatusEntity;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { APIKeyEntity } from 'src/entities/api-key.entity';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
import { AssetStackEntity } from 'src/entities/asset-stack.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AuditEntity } from 'src/entities/audit.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
@@ -19,6 +18,7 @@ import { SessionEntity } from 'src/entities/session.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
||||
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
import { SystemMetadataEntity } from 'src/entities/system-metadata.entity';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||
@@ -30,7 +30,6 @@ export const entities = [
|
||||
AlbumUserEntity,
|
||||
APIKeyEntity,
|
||||
AssetEntity,
|
||||
AssetStackEntity,
|
||||
AssetFaceEntity,
|
||||
AssetJobStatusEntity,
|
||||
AuditEntity,
|
||||
@@ -44,6 +43,7 @@ export const entities = [
|
||||
SharedLinkEntity,
|
||||
SmartInfoEntity,
|
||||
SmartSearchEntity,
|
||||
StackEntity,
|
||||
SystemMetadataEntity,
|
||||
TagEntity,
|
||||
UserEntity,
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { Column, Entity, JoinColumn, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity('asset_stack')
|
||||
export class AssetStackEntity {
|
||||
export class StackEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
owner!: UserEntity;
|
||||
|
||||
@Column()
|
||||
ownerId!: string;
|
||||
|
||||
@OneToMany(() => AssetEntity, (asset) => asset.stack)
|
||||
assets!: AssetEntity[];
|
||||
|
||||
@@ -14,24 +14,11 @@ export interface AlbumInfoOptions {
|
||||
withAssets: boolean;
|
||||
}
|
||||
|
||||
export interface AlbumAsset {
|
||||
albumId: string;
|
||||
assetId: string;
|
||||
}
|
||||
|
||||
export interface AlbumAssets {
|
||||
albumId: string;
|
||||
assetIds: string[];
|
||||
}
|
||||
|
||||
export interface IAlbumRepository extends IBulkAsset {
|
||||
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>;
|
||||
getByIds(ids: string[]): Promise<AlbumEntity[]>;
|
||||
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
||||
getAssetIds(albumId: string, assetIds?: string[]): Promise<Set<string>>;
|
||||
hasAsset(asset: AlbumAsset): Promise<boolean>;
|
||||
removeAsset(assetId: string): Promise<void>;
|
||||
removeAssetIds(albumId: string, assetIds: string[]): Promise<void>;
|
||||
getMetadataForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
||||
getInvalidThumbnail(): Promise<string[]>;
|
||||
getOwned(ownerId: string): Promise<AlbumEntity[]>;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { AssetStackEntity } from 'src/entities/asset-stack.entity';
|
||||
|
||||
export const IAssetStackRepository = 'IAssetStackRepository';
|
||||
|
||||
export interface IAssetStackRepository {
|
||||
create(assetStack: Partial<AssetStackEntity>): Promise<AssetStackEntity>;
|
||||
update(asset: Pick<AssetStackEntity, 'id'> & Partial<AssetStackEntity>): Promise<AssetStackEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
getById(id: string): Promise<AssetStackEntity | null>;
|
||||
deleteAll(userId: string): Promise<void>;
|
||||
}
|
||||
@@ -4,17 +4,36 @@ import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.d
|
||||
|
||||
export const IEventRepository = 'IEventRepository';
|
||||
|
||||
export type SystemConfigUpdateEvent = { newConfig: SystemConfig; oldConfig: SystemConfig };
|
||||
export type AlbumUpdateEvent = {
|
||||
id: string;
|
||||
/** user id */
|
||||
updatedBy: string;
|
||||
};
|
||||
export type AlbumInviteEvent = { id: string; userId: string };
|
||||
export type UserSignupEvent = { notify: boolean; id: string; tempPassword?: string };
|
||||
|
||||
type MaybePromise<T> = Promise<T> | T;
|
||||
type Handler<T = undefined> = (data: T) => MaybePromise<void>;
|
||||
|
||||
const noop = () => {};
|
||||
const dummyHandlers = {
|
||||
onBootstrapEvent: noop as (app: 'api' | 'microservices') => MaybePromise<void>,
|
||||
// app events
|
||||
onBootstrapEvent: noop as Handler<'api' | 'microservices'>,
|
||||
onShutdownEvent: noop as () => MaybePromise<void>,
|
||||
onConfigUpdateEvent: noop as (update: SystemConfigUpdate) => MaybePromise<void>,
|
||||
onConfigValidateEvent: noop as (update: SystemConfigUpdate) => MaybePromise<void>,
|
||||
|
||||
// config events
|
||||
onConfigUpdateEvent: noop as Handler<SystemConfigUpdateEvent>,
|
||||
onConfigValidateEvent: noop as Handler<SystemConfigUpdateEvent>,
|
||||
|
||||
// album events
|
||||
onAlbumUpdateEvent: noop as Handler<AlbumUpdateEvent>,
|
||||
onAlbumInviteEvent: noop as Handler<AlbumInviteEvent>,
|
||||
|
||||
// user events
|
||||
onUserSignupEvent: noop as Handler<UserSignupEvent>,
|
||||
};
|
||||
|
||||
export type SystemConfigUpdate = { newConfig: SystemConfig; oldConfig: SystemConfig };
|
||||
export type EventHandlers = typeof dummyHandlers;
|
||||
export type EmitEvent = keyof EventHandlers;
|
||||
export type EmitEventHandler<T extends EmitEvent> = (...args: Parameters<EventHandlers[T]>) => MaybePromise<void>;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { MemoryEntity } from 'src/entities/memory.entity';
|
||||
import { IBulkAsset } from 'src/utils/asset.util';
|
||||
|
||||
export const IMemoryRepository = 'IMemoryRepository';
|
||||
|
||||
export interface IMemoryRepository {
|
||||
export interface IMemoryRepository extends IBulkAsset {
|
||||
search(ownerId: string): Promise<MemoryEntity[]>;
|
||||
get(id: string): Promise<MemoryEntity | null>;
|
||||
create(memory: Partial<MemoryEntity>): Promise<MemoryEntity>;
|
||||
update(memory: Partial<MemoryEntity>): Promise<MemoryEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
getAssetIds(id: string, assetIds: string[]): Promise<Set<string>>;
|
||||
addAssetIds(id: string, assetIds: string[]): Promise<void>;
|
||||
removeAssetIds(id: string, assetIds: string[]): Promise<void>;
|
||||
}
|
||||
|
||||
10
server/src/interfaces/stack.interface.ts
Normal file
10
server/src/interfaces/stack.interface.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
|
||||
export const IStackRepository = 'IStackRepository';
|
||||
|
||||
export interface IStackRepository {
|
||||
create(stack: Partial<StackEntity> & { ownerId: string }): Promise<StackEntity>;
|
||||
update(stack: Pick<StackEntity, 'id'> & Partial<StackEntity>): Promise<StackEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
getById(id: string): Promise<StackEntity | null>;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { INestApplicationContext } from '@nestjs/common';
|
||||
import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||
import { createAdapter } from '@socket.io/postgres-adapter';
|
||||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import { Redis } from 'ioredis';
|
||||
import { ServerOptions } from 'socket.io';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { PostgresDriver } from 'typeorm/driver/postgres/PostgresDriver.js';
|
||||
import { parseRedisConfig } from 'src/config';
|
||||
|
||||
export class WebSocketAdapter extends IoAdapter {
|
||||
constructor(private app: INestApplicationContext) {
|
||||
@@ -12,8 +12,9 @@ export class WebSocketAdapter extends IoAdapter {
|
||||
|
||||
createIOServer(port: number, options?: ServerOptions): any {
|
||||
const server = super.createIOServer(port, options);
|
||||
const pool = (this.app.get(DataSource).driver as PostgresDriver).master;
|
||||
server.adapter(createAdapter(pool));
|
||||
const pubClient = new Redis(parseRedisConfig());
|
||||
const subClient = pubClient.duplicate();
|
||||
server.adapter(createAdapter(pubClient, subClient));
|
||||
return server;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,25 +9,50 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface {
|
||||
await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
|
||||
}
|
||||
|
||||
const hasEmbeddings = async (tableName: string): Promise<boolean> => {
|
||||
const columns = await queryRunner.query(
|
||||
`SELECT column_name as name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = '${tableName}'`);
|
||||
return columns.some((column: { name: string }) => column.name === 'embedding');
|
||||
}
|
||||
|
||||
const hasAssetEmbeddings = await hasEmbeddings('smart_search');
|
||||
if (!hasAssetEmbeddings) {
|
||||
await queryRunner.query(`TRUNCATE smart_search`);
|
||||
await queryRunner.query(`ALTER TABLE smart_search ADD COLUMN IF NOT EXISTS embedding vector(512) NOT NULL`);
|
||||
}
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE face_search (
|
||||
"faceId" uuid PRIMARY KEY REFERENCES asset_faces(id) ON DELETE CASCADE,
|
||||
embedding vector(512) NOT NULL )`);
|
||||
CREATE TABLE face_search (
|
||||
"faceId" uuid PRIMARY KEY REFERENCES asset_faces(id) ON DELETE CASCADE,
|
||||
embedding vector(512) NOT NULL )`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET STORAGE EXTERNAL`);
|
||||
await queryRunner.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET STORAGE EXTERNAL`);
|
||||
|
||||
await queryRunner.query(`
|
||||
INSERT INTO face_search("faceId", embedding)
|
||||
SELECT id, embedding
|
||||
FROM asset_faces faces`);
|
||||
const hasFaceEmbeddings = await hasEmbeddings('asset_faces')
|
||||
if (hasFaceEmbeddings) {
|
||||
await queryRunner.query(`
|
||||
INSERT INTO face_search("faceId", embedding)
|
||||
SELECT id, embedding
|
||||
FROM asset_faces faces`);
|
||||
}
|
||||
|
||||
await queryRunner.query(`ALTER TABLE asset_faces DROP COLUMN "embedding"`);
|
||||
await queryRunner.query(`ALTER TABLE asset_faces DROP COLUMN IF EXISTS embedding`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE real[]`);
|
||||
await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE vector(512)`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX face_index ON face_search
|
||||
USING hnsw (embedding vector_cosine_ops)
|
||||
WITH (ef_construction = 300, m = 16)`);
|
||||
CREATE INDEX IF NOT EXISTS clip_index ON smart_search
|
||||
USING hnsw (embedding vector_cosine_ops)
|
||||
WITH (ef_construction = 300, m = 16)`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX face_index ON face_search
|
||||
USING hnsw (embedding vector_cosine_ops)
|
||||
WITH (ef_construction = 300, m = 16)`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
@@ -40,15 +65,15 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface {
|
||||
await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET STORAGE DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET STORAGE DEFAULT`);
|
||||
await queryRunner.query(`
|
||||
UPDATE asset_faces
|
||||
SET embedding = fs.embedding
|
||||
FROM face_search fs
|
||||
WHERE id = fs."faceId"`);
|
||||
UPDATE asset_faces
|
||||
SET embedding = fs.embedding
|
||||
FROM face_search fs
|
||||
WHERE id = fs."faceId"`);
|
||||
await queryRunner.query(`DROP TABLE face_search`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX face_index ON asset_faces
|
||||
USING hnsw (embedding vector_cosine_ops)
|
||||
WITH (ef_construction = 300, m = 16)`);
|
||||
CREATE INDEX face_index ON asset_faces
|
||||
USING hnsw (embedding vector_cosine_ops)
|
||||
WITH (ef_construction = 300, m = 16)`);
|
||||
}
|
||||
}
|
||||
|
||||
22
server/src/migrations/1720207981949-AddStackOwner.ts
Normal file
22
server/src/migrations/1720207981949-AddStackOwner.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddStackOwner1720207981949 implements MigrationInterface {
|
||||
name = 'AddStackOwner1720207981949'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "asset_stack" ADD "ownerId" uuid`);
|
||||
await queryRunner.query(`
|
||||
UPDATE "asset_stack" stack
|
||||
SET "ownerId" = asset."ownerId"
|
||||
FROM "assets" asset
|
||||
WHERE stack."primaryAssetId" = asset."id"
|
||||
`)
|
||||
await queryRunner.query('ALTER TABLE "asset_stack" ALTER COLUMN "ownerId" SET NOT NULL')
|
||||
await queryRunner.query(`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_c05079e542fd74de3b5ecb5c1c8" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "asset_stack" DROP CONSTRAINT "FK_c05079e542fd74de3b5ecb5c1c8"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_stack" DROP COLUMN "ownerId"`);
|
||||
}
|
||||
}
|
||||
@@ -585,45 +585,6 @@ WHERE
|
||||
"albums_assets"."albumsId" = $1
|
||||
AND "albums_assets"."assetsId" IN ($2)
|
||||
|
||||
-- AlbumRepository.getAssetIds (no assets)
|
||||
SELECT
|
||||
"albums_assets"."assetsId" AS "assetId"
|
||||
FROM
|
||||
"albums_assets_assets" "albums_assets"
|
||||
WHERE
|
||||
"albums_assets"."albumsId" = $1
|
||||
|
||||
-- AlbumRepository.hasAsset
|
||||
SELECT
|
||||
1 AS "row_exists"
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
1 AS dummy_column
|
||||
) "dummy_table"
|
||||
WHERE
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "albums_assets_assets" "AlbumEntity_AlbumEntity__AlbumEntity_assets" ON "AlbumEntity_AlbumEntity__AlbumEntity_assets"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "assets" "AlbumEntity__AlbumEntity_assets" ON "AlbumEntity__AlbumEntity_assets"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_assets"."assetsId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_assets"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
(
|
||||
(
|
||||
("AlbumEntity"."id" = $1)
|
||||
AND ((("AlbumEntity__AlbumEntity_assets"."id" = $2)))
|
||||
)
|
||||
)
|
||||
AND ("AlbumEntity"."deletedAt" IS NULL)
|
||||
)
|
||||
LIMIT
|
||||
1
|
||||
|
||||
-- AlbumRepository.addAssetIds
|
||||
INSERT INTO
|
||||
"albums_assets_assets" ("albumsId", "assetsId")
|
||||
|
||||
@@ -205,6 +205,7 @@ SELECT
|
||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId",
|
||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden",
|
||||
"AssetEntity__AssetEntity_stack"."id" AS "AssetEntity__AssetEntity_stack_id",
|
||||
"AssetEntity__AssetEntity_stack"."ownerId" AS "AssetEntity__AssetEntity_stack_ownerId",
|
||||
"AssetEntity__AssetEntity_stack"."primaryAssetId" AS "AssetEntity__AssetEntity_stack_primaryAssetId",
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."id" AS "bd93d5747511a4dad4923546c51365bf1a803774_id",
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."deviceAssetId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceAssetId",
|
||||
@@ -629,6 +630,7 @@ SELECT
|
||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||
"exifInfo"."fps" AS "exifInfo_fps",
|
||||
"stack"."id" AS "stack_id",
|
||||
"stack"."ownerId" AS "stack_ownerId",
|
||||
"stack"."primaryAssetId" AS "stack_primaryAssetId",
|
||||
"stackedAssets"."id" AS "stackedAssets_id",
|
||||
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
|
||||
@@ -769,6 +771,7 @@ SELECT
|
||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||
"exifInfo"."fps" AS "exifInfo_fps",
|
||||
"stack"."id" AS "stack_id",
|
||||
"stack"."ownerId" AS "stack_ownerId",
|
||||
"stack"."primaryAssetId" AS "stack_primaryAssetId",
|
||||
"stackedAssets"."id" AS "stackedAssets_id",
|
||||
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
|
||||
@@ -885,6 +888,7 @@ SELECT
|
||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||
"exifInfo"."fps" AS "exifInfo_fps",
|
||||
"stack"."id" AS "stack_id",
|
||||
"stack"."ownerId" AS "stack_ownerId",
|
||||
"stack"."primaryAssetId" AS "stack_primaryAssetId",
|
||||
"stackedAssets"."id" AS "stackedAssets_id",
|
||||
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
|
||||
@@ -1051,6 +1055,7 @@ SELECT
|
||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||
"exifInfo"."fps" AS "exifInfo_fps",
|
||||
"stack"."id" AS "stack_id",
|
||||
"stack"."ownerId" AS "stack_ownerId",
|
||||
"stack"."primaryAssetId" AS "stack_primaryAssetId"
|
||||
FROM
|
||||
"assets" "asset"
|
||||
@@ -1126,6 +1131,7 @@ SELECT
|
||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||
"exifInfo"."fps" AS "exifInfo_fps",
|
||||
"stack"."id" AS "stack_id",
|
||||
"stack"."ownerId" AS "stack_ownerId",
|
||||
"stack"."primaryAssetId" AS "stack_primaryAssetId"
|
||||
FROM
|
||||
"assets" "asset"
|
||||
|
||||
@@ -37,6 +37,7 @@ FROM
|
||||
"asset"."stackId" AS "asset_stackId",
|
||||
"asset"."duplicateId" AS "asset_duplicateId",
|
||||
"stack"."id" AS "stack_id",
|
||||
"stack"."ownerId" AS "stack_ownerId",
|
||||
"stack"."primaryAssetId" AS "stack_primaryAssetId",
|
||||
"stackedAssets"."id" AS "stackedAssets_id",
|
||||
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
|
||||
@@ -133,6 +134,7 @@ SELECT
|
||||
"asset"."stackId" AS "asset_stackId",
|
||||
"asset"."duplicateId" AS "asset_duplicateId",
|
||||
"stack"."id" AS "stack_id",
|
||||
"stack"."ownerId" AS "stack_ownerId",
|
||||
"stack"."primaryAssetId" AS "stack_primaryAssetId",
|
||||
"stackedAssets"."id" AS "stackedAssets_id",
|
||||
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
import _ from 'lodash';
|
||||
import { Chunked, ChunkedArray, DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AlbumAsset, AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
import { setUnion } from 'src/utils/set';
|
||||
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
|
||||
|
||||
const withoutDeletedUsers = <T extends AlbumEntity | null>(album: T) => {
|
||||
@@ -215,6 +213,10 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
@Chunked({ paramIndex: 1 })
|
||||
async removeAssetIds(albumId: string, assetIds: string[]): Promise<void> {
|
||||
if (assetIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
@@ -233,42 +235,22 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
* @param assetIds Optional list of asset IDs to filter on.
|
||||
* @returns Set of Asset IDs for the given album ID.
|
||||
*/
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }, { name: 'no assets', params: [DummyValue.UUID] })
|
||||
async getAssetIds(albumId: string, assetIds?: string[]): Promise<Set<string>> {
|
||||
const query = this.dataSource
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async getAssetIds(albumId: string, assetIds: string[]): Promise<Set<string>> {
|
||||
if (assetIds.length === 0) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const results = await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.select('albums_assets.assetsId', 'assetId')
|
||||
.from('albums_assets_assets', 'albums_assets')
|
||||
.where('"albums_assets"."albumsId" = :albumId', { albumId });
|
||||
.where('"albums_assets"."albumsId" = :albumId', { albumId })
|
||||
.andWhere('"albums_assets"."assetsId" IN (:...assetIds)', { assetIds })
|
||||
.getRawMany<{ assetId: string }>();
|
||||
|
||||
if (!assetIds?.length) {
|
||||
const result = await query.getRawMany();
|
||||
return new Set(result.map((row) => row['assetId']));
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
_.chunk(assetIds, DATABASE_PARAMETER_CHUNK_SIZE).map((idChunk) =>
|
||||
query
|
||||
.andWhere('"albums_assets"."assetsId" IN (:...assetIds)', { assetIds: idChunk })
|
||||
.getRawMany()
|
||||
.then((result) => new Set(result.map((row) => row['assetId']))),
|
||||
),
|
||||
).then((results) => setUnion(...results));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ albumId: DummyValue.UUID, assetId: DummyValue.UUID }] })
|
||||
hasAsset(asset: AlbumAsset): Promise<boolean> {
|
||||
return this.repository.exist({
|
||||
where: {
|
||||
id: asset.albumId,
|
||||
assets: {
|
||||
id: asset.assetId,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
return new Set(results.map(({ assetId }) => assetId));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { AssetStackEntity } from 'src/entities/asset-stack.entity';
|
||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class AssetStackRepository implements IAssetStackRepository {
|
||||
constructor(@InjectRepository(AssetStackEntity) private repository: Repository<AssetStackEntity>) {}
|
||||
|
||||
create(entity: Partial<AssetStackEntity>) {
|
||||
return this.save(entity);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
|
||||
update(entity: Partial<AssetStackEntity>) {
|
||||
return this.save(entity);
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<AssetStackEntity | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: {
|
||||
primaryAsset: true,
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAll(userId: string): Promise<void> {
|
||||
// TODO add owner to stack entity
|
||||
const stacks = await this.repository.find({ where: { primaryAsset: { ownerId: userId } } });
|
||||
const stackIds = new Set(stacks.map((stack) => stack.id));
|
||||
await this.repository.delete({ id: In([...stackIds]) });
|
||||
}
|
||||
|
||||
private async save(entity: Partial<AssetStackEntity>) {
|
||||
const { id } = await this.repository.save(entity);
|
||||
return this.repository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: {
|
||||
primaryAsset: true,
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { IActivityRepository } from 'src/interfaces/activity.interface';
|
||||
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
@@ -26,6 +25,7 @@ import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||
import { IStackRepository } from 'src/interfaces/stack.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||
@@ -35,7 +35,6 @@ import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
||||
import { AssetStackRepository } from 'src/repositories/asset-stack.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { AuditRepository } from 'src/repositories/audit.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
@@ -58,6 +57,7 @@ import { SearchRepository } from 'src/repositories/search.repository';
|
||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||
import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||
import { StackRepository } from 'src/repositories/stack.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { TagRepository } from 'src/repositories/tag.repository';
|
||||
@@ -69,7 +69,6 @@ export const repositories = [
|
||||
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
||||
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
|
||||
{ provide: IAssetRepository, useClass: AssetRepository },
|
||||
{ provide: IAssetStackRepository, useClass: AssetStackRepository },
|
||||
{ provide: IAuditRepository, useClass: AuditRepository },
|
||||
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
||||
@@ -92,6 +91,7 @@ export const repositories = [
|
||||
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
|
||||
{ provide: ISessionRepository, useClass: SessionRepository },
|
||||
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
||||
{ provide: IStackRepository, useClass: StackRepository },
|
||||
{ provide: IStorageRepository, useClass: StorageRepository },
|
||||
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
||||
{ provide: ITagRepository, useClass: TagRepository },
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getName } from 'i18n-iso-countries';
|
||||
import { createReadStream, existsSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import readLine from 'node:readline';
|
||||
import { citiesFile, geodataAdmin1Path, geodataAdmin2Path, geodataCities500Path, geodataDatePath } from 'src/constants';
|
||||
import { citiesFile, resourcePaths } from 'src/constants';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||
@@ -37,7 +37,7 @@ export class MapRepository implements IMapRepository {
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.logger.log('Initializing metadata repository');
|
||||
const geodataDate = await readFile(geodataDatePath, 'utf8');
|
||||
const geodataDate = await readFile(resourcePaths.geodata.dateFile, 'utf8');
|
||||
|
||||
// TODO move to service init
|
||||
const geocodingMetadata = await this.metadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE);
|
||||
@@ -150,8 +150,8 @@ export class MapRepository implements IMapRepository {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
|
||||
const admin1 = await this.loadAdmin(geodataAdmin1Path);
|
||||
const admin2 = await this.loadAdmin(geodataAdmin2Path);
|
||||
const admin1 = await this.loadAdmin(resourcePaths.geodata.admin1);
|
||||
const admin2 = await this.loadAdmin(resourcePaths.geodata.admin2);
|
||||
|
||||
try {
|
||||
await queryRunner.startTransaction();
|
||||
@@ -221,7 +221,7 @@ export class MapRepository implements IMapRepository {
|
||||
admin1Name: admin1Map.get(`${lineSplit[8]}.${lineSplit[10]}`),
|
||||
admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`),
|
||||
}),
|
||||
geodataCities500Path,
|
||||
resourcePaths.geodata.cities500,
|
||||
{ entityFilter: (lineSplit) => lineSplit[7] != 'PPLX' },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,9 +61,9 @@ export class MemoryRepository implements IMemoryRepository {
|
||||
.from('memories_assets_assets', 'memories_assets')
|
||||
.where('"memories_assets"."memoriesId" = :memoryId', { memoryId: id })
|
||||
.andWhere('memories_assets.assetsId IN (:...assetIds)', { assetIds })
|
||||
.getRawMany();
|
||||
.getRawMany<{ assetId: string }>();
|
||||
|
||||
return new Set(results.map((row) => row['assetId']));
|
||||
return new Set(results.map(({ assetId }) => assetId));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||
|
||||
@@ -4,6 +4,7 @@ import { exec as execCallback } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import sharp from 'sharp';
|
||||
import { resourcePaths } from 'src/constants';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
@@ -61,9 +62,9 @@ export class ServerInfoRepository implements IServerInfoRepository {
|
||||
maybeFirstLine('convert --version'),
|
||||
]);
|
||||
|
||||
const lockfile = await readFile('build-lock.json')
|
||||
const lockfile = await readFile(resourcePaths.lockFile)
|
||||
.then((buffer) => JSON.parse(buffer.toString()))
|
||||
.catch(() => this.logger.warn('Failed to read build-lock.json'));
|
||||
.catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`));
|
||||
|
||||
return {
|
||||
nodejs: nodejsOutput || process.env.NODE_VERSION || '',
|
||||
|
||||
49
server/src/repositories/stack.repository.ts
Normal file
49
server/src/repositories/stack.repository.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
import { IStackRepository } from 'src/interfaces/stack.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class StackRepository implements IStackRepository {
|
||||
constructor(@InjectRepository(StackEntity) private repository: Repository<StackEntity>) {}
|
||||
|
||||
create(entity: Partial<StackEntity>) {
|
||||
return this.save(entity);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.repository.delete(id);
|
||||
}
|
||||
|
||||
update(entity: Partial<StackEntity>) {
|
||||
return this.save(entity);
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<StackEntity | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: {
|
||||
primaryAsset: true,
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async save(entity: Partial<StackEntity>) {
|
||||
const { id } = await this.repository.save(entity);
|
||||
return this.repository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: {
|
||||
primaryAsset: true,
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -202,7 +202,7 @@ describe(StorageRepository.name, () => {
|
||||
.filter((entry) => entry[1])
|
||||
.map(([file]) => file);
|
||||
|
||||
expect(actual.sort()).toEqual(expected.sort());
|
||||
expect(actual.toSorted()).toEqual(expected.toSorted());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AlbumUserRole } from 'src/entities/album-user.entity';
|
||||
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AlbumService } from 'src/services/album.service';
|
||||
import { albumStub } from 'test/fixtures/album.stub';
|
||||
@@ -15,7 +15,7 @@ import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositorie
|
||||
import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock';
|
||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
@@ -24,19 +24,19 @@ describe(AlbumService.name, () => {
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let albumUserMock: Mocked<IAlbumUserRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
accessMock = newAccessRepositoryMock();
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
assetMock = newAssetRepositoryMock();
|
||||
eventMock = newEventRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
albumUserMock = newAlbumUserRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
|
||||
sut = new AlbumService(accessMock, albumMock, assetMock, userMock, albumUserMock, jobMock);
|
||||
sut = new AlbumService(accessMock, albumMock, assetMock, eventMock, userMock, albumUserMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
@@ -270,7 +270,7 @@ describe(AlbumService.name, () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-4']));
|
||||
albumMock.getById.mockResolvedValue(albumStub.oneAsset);
|
||||
albumMock.update.mockResolvedValue(albumStub.oneAsset);
|
||||
albumMock.hasAsset.mockResolvedValue(false);
|
||||
albumMock.getAssetIds.mockResolvedValue(new Set());
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, albumStub.oneAsset.id, {
|
||||
@@ -278,7 +278,7 @@ describe(AlbumService.name, () => {
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(albumMock.hasAsset).toHaveBeenCalledWith({ albumId: 'album-4', assetId: 'not-in-album' });
|
||||
expect(albumMock.getAssetIds).toHaveBeenCalledWith('album-4', ['not-in-album']);
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -381,14 +381,10 @@ describe(AlbumService.name, () => {
|
||||
userId: authStub.user2.user.id,
|
||||
albumId: albumStub.sharedWithAdmin.id,
|
||||
});
|
||||
expect(jobMock.queue.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
name: JobName.NOTIFY_ALBUM_INVITE,
|
||||
data: { id: albumStub.sharedWithAdmin.id, recipientId: authStub.user2.user.id },
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumInviteEvent', {
|
||||
id: albumStub.sharedWithAdmin.id,
|
||||
userId: userStub.user2.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -573,14 +569,10 @@ describe(AlbumService.name, () => {
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||
expect(jobMock.queue.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
name: JobName.NOTIFY_ALBUM_UPDATE,
|
||||
data: { id: 'album-123', senderId: authStub.admin.user.id },
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdateEvent', {
|
||||
id: 'album-123',
|
||||
updatedBy: authStub.admin.user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not set the thumbnail if the album has one already', async () => {
|
||||
@@ -621,14 +613,10 @@ describe(AlbumService.name, () => {
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||
expect(jobMock.queue.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
name: JobName.NOTIFY_ALBUM_UPDATE,
|
||||
data: { id: 'album-123', senderId: authStub.user1.user.id },
|
||||
},
|
||||
],
|
||||
]);
|
||||
expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdateEvent', {
|
||||
id: 'album-123',
|
||||
updatedBy: authStub.user1.user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow a shared user with viewer access to add assets', async () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||
|
||||
@@ -32,9 +32,9 @@ export class AlbumService {
|
||||
@Inject(IAccessRepository) private accessRepository: IAccessRepository,
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
@@ -146,8 +146,8 @@ export class AlbumService {
|
||||
const album = await this.findOrFail(id, { withAssets: true });
|
||||
|
||||
if (dto.albumThumbnailAssetId) {
|
||||
const valid = await this.albumRepository.hasAsset({ albumId: id, assetId: dto.albumThumbnailAssetId });
|
||||
if (!valid) {
|
||||
const results = await this.albumRepository.getAssetIds(id, [dto.albumThumbnailAssetId]);
|
||||
if (results.size === 0) {
|
||||
throw new BadRequestException('Invalid album thumbnail');
|
||||
}
|
||||
}
|
||||
@@ -188,12 +188,9 @@ export class AlbumService {
|
||||
updatedAt: new Date(),
|
||||
albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId,
|
||||
});
|
||||
}
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.NOTIFY_ALBUM_UPDATE,
|
||||
data: { id, senderId: auth.user.id },
|
||||
});
|
||||
await this.eventRepository.emit('onAlbumUpdateEvent', { id, updatedBy: auth.user.id });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -240,11 +237,7 @@ export class AlbumService {
|
||||
}
|
||||
|
||||
await this.albumUserRepository.create({ userId: userId, albumId: id, role });
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.NOTIFY_ALBUM_INVITE,
|
||||
data: { id: album.id, recipientId: user.id },
|
||||
});
|
||||
await this.eventRepository.emit('onAlbumInviteEvent', { id, userId });
|
||||
}
|
||||
|
||||
return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets);
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Cron, CronExpression, Interval } from '@nestjs/schedule';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { ONE_HOUR, WEB_ROOT } from 'src/constants';
|
||||
import { ONE_HOUR, resourcePaths } from 'src/constants';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
@@ -56,9 +55,9 @@ export class ApiService {
|
||||
ssr(excludePaths: string[]) {
|
||||
let index = '';
|
||||
try {
|
||||
index = readFileSync(join(WEB_ROOT, 'index.html')).toString();
|
||||
index = readFileSync(resourcePaths.web.indexHtml).toString();
|
||||
} catch {
|
||||
this.logger.warn('Unable to open `www/index.html, skipping SSR.');
|
||||
this.logger.warn(`Unable to open ${resourcePaths.web.indexHtml}, skipping SSR.`);
|
||||
}
|
||||
|
||||
return async (request: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
@@ -255,13 +255,11 @@ describe(AssetMediaService.name, () => {
|
||||
}
|
||||
|
||||
it('should be sorted (valid)', () => {
|
||||
// TODO: use toSorted in NodeJS 20.
|
||||
expect(valid).toEqual([...valid].sort());
|
||||
expect(valid).toEqual(valid.toSorted());
|
||||
});
|
||||
|
||||
it('should be sorted (invalid)', () => {
|
||||
// TODO: use toSorted in NodeJS 20.
|
||||
expect(invalid).toEqual([...invalid].sort());
|
||||
expect(invalid).toEqual(invalid.toSorted());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,27 +2,27 @@ import { BadRequestException } from '@nestjs/common';
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
|
||||
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { IStackRepository } from 'src/interfaces/stack.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import { assetStackStub, assetStub } from 'test/fixtures/asset.stub';
|
||||
import { assetStub, stackStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { faceStub } from 'test/fixtures/face.stub';
|
||||
import { partnerStub } from 'test/fixtures/partner.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newAssetStackRepositoryMock } from 'test/repositories/asset-stack.repository.mock';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
||||
import { newStackRepositoryMock } from 'test/repositories/stack.repository.mock';
|
||||
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
@@ -47,9 +47,9 @@ describe(AssetService.name, () => {
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let stackMock: Mocked<IStackRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let partnerMock: Mocked<IPartnerRepository>;
|
||||
let assetStackMock: Mocked<IAssetStackRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
|
||||
it('should work', () => {
|
||||
@@ -70,7 +70,7 @@ describe(AssetService.name, () => {
|
||||
userMock = newUserRepositoryMock();
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
partnerMock = newPartnerRepositoryMock();
|
||||
assetStackMock = newAssetStackRepositoryMock();
|
||||
stackMock = newStackRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new AssetService(
|
||||
@@ -81,7 +81,7 @@ describe(AssetService.name, () => {
|
||||
userMock,
|
||||
eventMock,
|
||||
partnerMock,
|
||||
assetStackMock,
|
||||
stackMock,
|
||||
loggerMock,
|
||||
);
|
||||
|
||||
@@ -274,10 +274,10 @@ describe(AssetService.name, () => {
|
||||
{
|
||||
id: 'child-1',
|
||||
stackId: 'stack-1',
|
||||
stack: assetStackStub('stack-1', [{ id: 'parent' } as AssetEntity, { id: 'child-1' } as AssetEntity]),
|
||||
stack: stackStub('stack-1', [{ id: 'parent' } as AssetEntity, { id: 'child-1' } as AssetEntity]),
|
||||
} as AssetEntity,
|
||||
]);
|
||||
assetStackMock.getById.mockResolvedValue(assetStackStub('stack-1', [{ id: 'parent' } as AssetEntity]));
|
||||
stackMock.getById.mockResolvedValue(stackStub('stack-1', [{ id: 'parent' } as AssetEntity]));
|
||||
|
||||
await sut.updateAll(authStub.user1, {
|
||||
ids: ['child-1'],
|
||||
@@ -287,13 +287,13 @@ describe(AssetService.name, () => {
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['parent']), {
|
||||
updatedAt: expect.any(Date),
|
||||
});
|
||||
expect(assetStackMock.delete).toHaveBeenCalledWith('stack-1');
|
||||
expect(stackMock.delete).toHaveBeenCalledWith('stack-1');
|
||||
});
|
||||
|
||||
it('update parentId for new children', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['child-1', 'child-2']));
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent']));
|
||||
const stack = assetStackStub('stack-1', [
|
||||
const stack = stackStub('stack-1', [
|
||||
{ id: 'parent' } as AssetEntity,
|
||||
{ id: 'child-1' } as AssetEntity,
|
||||
{ id: 'child-2' } as AssetEntity,
|
||||
@@ -308,8 +308,8 @@ describe(AssetService.name, () => {
|
||||
ids: ['child-1', 'child-2'],
|
||||
});
|
||||
|
||||
expect(assetStackMock.update).toHaveBeenCalledWith({
|
||||
...assetStackStub('stack-1', [
|
||||
expect(stackMock.update).toHaveBeenCalledWith({
|
||||
...stackStub('stack-1', [
|
||||
{ id: 'child-1' } as AssetEntity,
|
||||
{ id: 'child-2' } as AssetEntity,
|
||||
{ id: 'parent' } as AssetEntity,
|
||||
@@ -337,19 +337,20 @@ describe(AssetService.name, () => {
|
||||
{
|
||||
id: 'child-1',
|
||||
stackId: 'stack-1',
|
||||
stack: assetStackStub('stack-1', [{ id: 'child-1' } as AssetEntity, { id: 'child-2' } as AssetEntity]),
|
||||
stack: stackStub('stack-1', [{ id: 'child-1' } as AssetEntity, { id: 'child-2' } as AssetEntity]),
|
||||
} as AssetEntity,
|
||||
]);
|
||||
assetStackMock.getById.mockResolvedValue(assetStackStub('stack-1', [{ id: 'parent' } as AssetEntity]));
|
||||
stackMock.getById.mockResolvedValue(stackStub('stack-1', [{ id: 'parent' } as AssetEntity]));
|
||||
|
||||
await sut.updateAll(authStub.user1, {
|
||||
ids: ['child-1'],
|
||||
stackParentId: 'parent',
|
||||
});
|
||||
|
||||
expect(assetStackMock.delete).toHaveBeenCalledWith('stack-1');
|
||||
expect(assetStackMock.create).toHaveBeenCalledWith({
|
||||
expect(stackMock.delete).toHaveBeenCalledWith('stack-1');
|
||||
expect(stackMock.create).toHaveBeenCalledWith({
|
||||
assets: [{ id: 'child-1' }, { id: 'parent' }, { id: 'child-1' }, { id: 'child-2' }],
|
||||
ownerId: 'user-id',
|
||||
primaryAssetId: 'parent',
|
||||
});
|
||||
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'parent', 'child-1', 'child-2'], {
|
||||
@@ -437,7 +438,7 @@ describe(AssetService.name, () => {
|
||||
|
||||
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
|
||||
|
||||
expect(assetStackMock.update).toHaveBeenCalledWith({
|
||||
expect(stackMock.update).toHaveBeenCalledWith({
|
||||
id: 'stack-1',
|
||||
primaryAssetId: 'stack-child-asset-1',
|
||||
});
|
||||
@@ -552,7 +553,7 @@ describe(AssetService.name, () => {
|
||||
newParentId: 'new',
|
||||
});
|
||||
|
||||
expect(assetStackMock.update).toBeCalledWith({ id: 'stack-1', primaryAssetId: 'new' });
|
||||
expect(stackMock.update).toBeCalledWith({ id: 'stack-1', primaryAssetId: 'new' });
|
||||
expect(assetMock.updateAll).toBeCalledWith([assetStub.image.id, 'new', assetStub.image.id], {
|
||||
updatedAt: expect.any(Date),
|
||||
});
|
||||
|
||||
@@ -23,7 +23,6 @@ import { MemoryLaneDto } from 'src/dtos/search.dto';
|
||||
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
} from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { IStackRepository } from 'src/interfaces/stack.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { getMyPartnerIds } from 'src/utils/asset.util';
|
||||
@@ -54,7 +54,7 @@ export class AssetService {
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||
@Inject(IAssetStackRepository) private assetStackRepository: IAssetStackRepository,
|
||||
@Inject(IStackRepository) private stackRepository: IStackRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(AssetService.name);
|
||||
@@ -211,14 +211,15 @@ export class AssetService {
|
||||
ids.push(...assetsWithChildren.flatMap((child) => child.stack!.assets.map((gChild) => gChild.id)));
|
||||
|
||||
if (stack) {
|
||||
await this.assetStackRepository.update({
|
||||
await this.stackRepository.update({
|
||||
id: stack.id,
|
||||
primaryAssetId: primaryAsset.id,
|
||||
assets: ids.map((id) => ({ id }) as AssetEntity),
|
||||
});
|
||||
} else {
|
||||
stack = await this.assetStackRepository.create({
|
||||
stack = await this.stackRepository.create({
|
||||
primaryAssetId: primaryAsset.id,
|
||||
ownerId: primaryAsset.ownerId,
|
||||
assets: ids.map((id) => ({ id }) as AssetEntity),
|
||||
});
|
||||
}
|
||||
@@ -233,13 +234,11 @@ export class AssetService {
|
||||
}
|
||||
|
||||
await this.assetRepository.updateAll(ids, options);
|
||||
const stackIdsToDelete = await Promise.all(
|
||||
stackIdsToCheckForDelete.map((id) => this.assetStackRepository.getById(id)),
|
||||
);
|
||||
const stackIdsToDelete = await Promise.all(stackIdsToCheckForDelete.map((id) => this.stackRepository.getById(id)));
|
||||
const stacksToDelete = stackIdsToDelete
|
||||
.flatMap((stack) => (stack ? [stack] : []))
|
||||
.filter((stack) => stack.assets.length < 2);
|
||||
await Promise.all(stacksToDelete.map((as) => this.assetStackRepository.delete(as.id)));
|
||||
await Promise.all(stacksToDelete.map((as) => this.stackRepository.delete(as.id)));
|
||||
this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, ids);
|
||||
}
|
||||
|
||||
@@ -289,12 +288,12 @@ export class AssetService {
|
||||
const stackAssetIds = asset.stack.assets.map((a) => a.id);
|
||||
if (stackAssetIds.length > 2) {
|
||||
const newPrimaryAssetId = stackAssetIds.find((a) => a !== id)!;
|
||||
await this.assetStackRepository.update({
|
||||
await this.stackRepository.update({
|
||||
id: asset.stack.id,
|
||||
primaryAssetId: newPrimaryAssetId,
|
||||
});
|
||||
} else {
|
||||
await this.assetStackRepository.delete(asset.stack.id);
|
||||
await this.stackRepository.delete(asset.stack.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,7 +364,7 @@ export class AssetService {
|
||||
// Get all children of old parent
|
||||
childIds.push(oldParent.id, ...(oldParent.stack?.assets.map((a) => a.id) ?? []));
|
||||
}
|
||||
await this.assetStackRepository.update({
|
||||
await this.stackRepository.update({
|
||||
id: oldParent.stackId,
|
||||
primaryAssetId: newParentId,
|
||||
});
|
||||
|
||||
@@ -150,6 +150,7 @@ describe(DatabaseService.name, () => {
|
||||
for (const version of ['0.2.1', '0.2.0', '0.2.9']) {
|
||||
it(`should update the pgvecto.rs extension to ${version}`, async () => {
|
||||
databaseMock.getAvailableExtensionVersion.mockResolvedValue(version);
|
||||
databaseMock.getExtensionVersion.mockResolvedValueOnce(void 0);
|
||||
databaseMock.getExtensionVersion.mockResolvedValue(version);
|
||||
|
||||
await expect(sut.onBootstrapEvent()).resolves.toBeUndefined();
|
||||
@@ -166,6 +167,7 @@ describe(DatabaseService.name, () => {
|
||||
it(`should update the pgvectors extension to ${version}`, async () => {
|
||||
process.env.DB_VECTOR_EXTENSION = 'pgvector';
|
||||
databaseMock.getAvailableExtensionVersion.mockResolvedValue(version);
|
||||
databaseMock.getExtensionVersion.mockResolvedValueOnce(void 0);
|
||||
databaseMock.getExtensionVersion.mockResolvedValue(version);
|
||||
|
||||
await expect(sut.onBootstrapEvent()).resolves.toBeUndefined();
|
||||
@@ -190,10 +192,10 @@ describe(DatabaseService.name, () => {
|
||||
});
|
||||
}
|
||||
|
||||
for (const version of ['0.4.0', '1.0.0']) {
|
||||
for (const version of ['0.4.0', '0.7.1', '0.7.2', '1.0.0']) {
|
||||
it(`should not upgrade pgvector to ${version}`, async () => {
|
||||
process.env.DB_VECTOR_EXTENSION = 'pgvector';
|
||||
databaseMock.getExtensionVersion.mockResolvedValue('0.5.0');
|
||||
databaseMock.getExtensionVersion.mockResolvedValue('0.7.2');
|
||||
databaseMock.getAvailableExtensionVersion.mockResolvedValue(version);
|
||||
|
||||
await expect(sut.onBootstrapEvent()).resolves.toBeUndefined();
|
||||
|
||||
@@ -98,8 +98,10 @@ export class DatabaseService implements OnEvents {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const initialVersion = await this.databaseRepository.getExtensionVersion(extension);
|
||||
const availableVersion = await this.databaseRepository.getAvailableExtensionVersion(extension);
|
||||
if (availableVersion && semver.satisfies(availableVersion, extensionRange)) {
|
||||
const isAvailable = availableVersion && semver.satisfies(availableVersion, extensionRange);
|
||||
if (isAvailable && (!initialVersion || semver.gt(availableVersion, initialVersion))) {
|
||||
try {
|
||||
this.logger.log(`Updating ${name} extension to ${availableVersion}`);
|
||||
const { restartRequired } = await this.databaseRepository.updateVectorExtension(extension, availableVersion);
|
||||
|
||||
@@ -22,7 +22,7 @@ import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { OnEvents, SystemConfigUpdate } from 'src/interfaces/event.interface';
|
||||
import { OnEvents, SystemConfigUpdateEvent } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
IBaseJob,
|
||||
IEntityJob,
|
||||
@@ -102,7 +102,7 @@ export class LibraryService implements OnEvents {
|
||||
});
|
||||
}
|
||||
|
||||
onConfigValidateEvent({ newConfig }: SystemConfigUpdate) {
|
||||
onConfigValidateEvent({ newConfig }: SystemConfigUpdateEvent) {
|
||||
const { scan } = newConfig.library;
|
||||
if (!validateCronExpression(scan.cronExpression)) {
|
||||
throw new Error(`Invalid cron expression ${scan.cronExpression}`);
|
||||
|
||||
@@ -1573,6 +1573,7 @@ describe(MediaService.name, () => {
|
||||
'-hwaccel qsv',
|
||||
'-hwaccel_output_format qsv',
|
||||
'-async_depth 4',
|
||||
'-noautorotate',
|
||||
'-threads 1',
|
||||
]),
|
||||
outputOptions: expect.arrayContaining([
|
||||
@@ -1838,7 +1839,12 @@ describe(MediaService.name, () => {
|
||||
'/original/path.ext',
|
||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||
{
|
||||
inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
|
||||
inputOptions: expect.arrayContaining([
|
||||
'-hwaccel rkmpp',
|
||||
'-hwaccel_output_format drm_prime',
|
||||
'-afbc rga',
|
||||
'-noautorotate',
|
||||
]),
|
||||
outputOptions: expect.arrayContaining([
|
||||
`-c:v h264_rkmpp`,
|
||||
'-c:a copy',
|
||||
|
||||
@@ -5,7 +5,13 @@ import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { OnEvents, SystemConfigUpdate } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
AlbumInviteEvent,
|
||||
AlbumUpdateEvent,
|
||||
OnEvents,
|
||||
SystemConfigUpdateEvent,
|
||||
UserSignupEvent,
|
||||
} from 'src/interfaces/event.interface';
|
||||
import {
|
||||
IEmailJob,
|
||||
IJobRepository,
|
||||
@@ -38,7 +44,7 @@ export class NotificationService implements OnEvents {
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
|
||||
}
|
||||
|
||||
async onConfigValidateEvent({ newConfig }: SystemConfigUpdate) {
|
||||
async onConfigValidateEvent({ newConfig }: SystemConfigUpdateEvent) {
|
||||
try {
|
||||
if (newConfig.notifications.smtp.enabled) {
|
||||
await this.notificationRepository.verifySmtp(newConfig.notifications.smtp.transport);
|
||||
@@ -49,6 +55,20 @@ export class NotificationService implements OnEvents {
|
||||
}
|
||||
}
|
||||
|
||||
async onUserSignupEvent({ notify, id, tempPassword }: UserSignupEvent) {
|
||||
if (notify) {
|
||||
await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id, tempPassword } });
|
||||
}
|
||||
}
|
||||
|
||||
async onAlbumUpdateEvent({ id, updatedBy }: AlbumUpdateEvent) {
|
||||
await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_UPDATE, data: { id, senderId: updatedBy } });
|
||||
}
|
||||
|
||||
async onAlbumInviteEvent({ id, userId }: AlbumInviteEvent) {
|
||||
await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id, recipientId: userId } });
|
||||
}
|
||||
|
||||
async sendTestEmail(id: string, dto: SystemConfigSmtpDto) {
|
||||
const user = await this.userRepository.get(id, { withDeleted: false });
|
||||
if (!user) {
|
||||
|
||||
@@ -21,16 +21,16 @@ describe(PartnerService.name, () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
describe('search', () => {
|
||||
it("should return a list of partners with whom I've shared my library", async () => {
|
||||
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
|
||||
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedBy)).resolves.toBeDefined();
|
||||
await expect(sut.search(authStub.user1, { direction: PartnerDirection.SharedBy })).resolves.toBeDefined();
|
||||
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||
});
|
||||
|
||||
it('should return a list of partners who have shared their libraries with me', async () => {
|
||||
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
|
||||
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedWith)).resolves.toBeDefined();
|
||||
await expect(sut.search(authStub.user1, { direction: PartnerDirection.SharedWith })).resolves.toBeDefined();
|
||||
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { AccessCore, Permission } from 'src/cores/access.core';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
|
||||
import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
|
||||
import { mapUser } from 'src/dtos/user.dto';
|
||||
import { PartnerEntity } from 'src/entities/partner.entity';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
@@ -38,7 +38,7 @@ export class PartnerService {
|
||||
await this.repository.remove(partner);
|
||||
}
|
||||
|
||||
async getAll(auth: AuthDto, direction: PartnerDirection): Promise<PartnerResponseDto[]> {
|
||||
async search(auth: AuthDto, { direction }: PartnerSearchDto): Promise<PartnerResponseDto[]> {
|
||||
const partners = await this.repository.getAll(auth.user.id);
|
||||
const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId';
|
||||
return partners
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { OnEvents, SystemConfigUpdate } from 'src/interfaces/event.interface';
|
||||
import { OnEvents, SystemConfigUpdateEvent } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
IBaseJob,
|
||||
IEntityJob,
|
||||
@@ -50,7 +50,7 @@ export class SmartInfoService implements OnEvents {
|
||||
await this.jobRepository.resume(QueueName.SMART_SEARCH);
|
||||
}
|
||||
|
||||
async onConfigUpdateEvent({ oldConfig, newConfig }: SystemConfigUpdate) {
|
||||
async onConfigUpdateEvent({ oldConfig, newConfig }: SystemConfigUpdateEvent) {
|
||||
if (oldConfig.machineLearning.clip.modelName !== newConfig.machineLearning.clip.modelName) {
|
||||
await this.repository.init(newConfig.machineLearning.clip.modelName);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { OnEvents, SystemConfigUpdate } from 'src/interfaces/event.interface';
|
||||
import { OnEvents, SystemConfigUpdateEvent } from 'src/interfaces/event.interface';
|
||||
import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
@@ -87,7 +87,7 @@ export class StorageTemplateService implements OnEvents {
|
||||
);
|
||||
}
|
||||
|
||||
onConfigValidateEvent({ newConfig }: SystemConfigUpdate) {
|
||||
onConfigValidateEvent({ newConfig }: SystemConfigUpdateEvent) {
|
||||
try {
|
||||
const { compiled } = this.compile(newConfig.storageTemplate.template);
|
||||
this.render(compiled, {
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
IEventRepository,
|
||||
OnEvents,
|
||||
ServerEvent,
|
||||
SystemConfigUpdate,
|
||||
SystemConfigUpdateEvent,
|
||||
} from 'src/interfaces/event.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
@@ -42,11 +42,7 @@ export class SystemConfigService implements OnEvents {
|
||||
@EventHandlerOptions({ priority: -100 })
|
||||
async onBootstrapEvent() {
|
||||
const config = await this.core.getConfig({ withCache: false });
|
||||
this.config$.next(config);
|
||||
}
|
||||
|
||||
get config$() {
|
||||
return this.core.config$;
|
||||
this.core.config$.next(config);
|
||||
}
|
||||
|
||||
async getConfig(): Promise<SystemConfigDto> {
|
||||
@@ -58,7 +54,7 @@ export class SystemConfigService implements OnEvents {
|
||||
return mapConfig(defaults);
|
||||
}
|
||||
|
||||
onConfigValidateEvent({ newConfig, oldConfig }: SystemConfigUpdate) {
|
||||
onConfigValidateEvent({ newConfig, oldConfig }: SystemConfigUpdateEvent) {
|
||||
if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) {
|
||||
throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.');
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { UserStatus } from 'src/entities/user.entity';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
@@ -11,6 +12,7 @@ import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
@@ -18,21 +20,22 @@ import { Mocked, describe } from 'vitest';
|
||||
|
||||
describe(UserAdminService.name, () => {
|
||||
let sut: UserAdminService;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
let cryptoRepositoryMock: Mocked<ICryptoRepository>;
|
||||
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let eventMock: Mocked<IEventRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
let userMock: Mocked<IUserRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
albumMock = newAlbumRepositoryMock();
|
||||
cryptoRepositoryMock = newCryptoRepositoryMock();
|
||||
cryptoMock = newCryptoRepositoryMock();
|
||||
eventMock = newEventRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new UserAdminService(albumMock, cryptoRepositoryMock, jobMock, userMock, loggerMock);
|
||||
sut = new UserAdminService(albumMock, cryptoMock, eventMock, jobMock, userMock, loggerMock);
|
||||
|
||||
userMock.get.mockImplementation((userId) =>
|
||||
Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null),
|
||||
|
||||
@@ -15,6 +15,7 @@ import { UserMetadataKey } from 'src/entities/user-metadata.entity';
|
||||
import { UserStatus } from 'src/entities/user.entity';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
|
||||
@@ -27,6 +28,7 @@ export class UserAdminService {
|
||||
constructor(
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@@ -44,10 +46,12 @@ export class UserAdminService {
|
||||
const { notify, ...rest } = dto;
|
||||
const user = await this.userCore.createUser(rest);
|
||||
|
||||
const tempPassword = user.shouldChangePassword ? rest.password : undefined;
|
||||
if (notify) {
|
||||
await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id: user.id, tempPassword } });
|
||||
}
|
||||
await this.eventRepository.emit('onUserSignupEvent', {
|
||||
notify: !!notify,
|
||||
id: user.id,
|
||||
tempPassword: user.shouldChangePassword ? rest.password : undefined,
|
||||
});
|
||||
|
||||
return mapUserAdmin(user);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { BadRequestException, InternalServerErrorException, NotFoundException }
|
||||
import { UserMetadataKey } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
@@ -15,7 +14,6 @@ import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||
import { newAssetStackRepositoryMock } from 'test/repositories/asset-stack.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||
@@ -37,7 +35,6 @@ describe(UserService.name, () => {
|
||||
|
||||
let albumMock: Mocked<IAlbumRepository>;
|
||||
let jobMock: Mocked<IJobRepository>;
|
||||
let stackMock: Mocked<IAssetStackRepository>;
|
||||
let storageMock: Mocked<IStorageRepository>;
|
||||
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
@@ -47,21 +44,11 @@ describe(UserService.name, () => {
|
||||
systemMock = newSystemMetadataRepositoryMock();
|
||||
cryptoRepositoryMock = newCryptoRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
stackMock = newAssetStackRepositoryMock();
|
||||
storageMock = newStorageRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
|
||||
sut = new UserService(
|
||||
albumMock,
|
||||
cryptoRepositoryMock,
|
||||
jobMock,
|
||||
stackMock,
|
||||
storageMock,
|
||||
systemMock,
|
||||
userMock,
|
||||
loggerMock,
|
||||
);
|
||||
sut = new UserService(albumMock, cryptoRepositoryMock, jobMock, storageMock, systemMock, userMock, loggerMock);
|
||||
|
||||
userMock.get.mockImplementation((userId) =>
|
||||
Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null),
|
||||
|
||||
@@ -6,13 +6,12 @@ import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||
import { mapPreferences, UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
||||
import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto';
|
||||
import { mapUser, mapUserAdmin, UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto';
|
||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { UserMetadataEntity, UserMetadataKey } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
@@ -30,7 +29,6 @@ export class UserService {
|
||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IAssetStackRepository) private stackRepository: IAssetStackRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@@ -213,7 +211,6 @@ export class UserService {
|
||||
}
|
||||
|
||||
this.logger.warn(`Removing user from database: ${user.id}`);
|
||||
await this.stackRepository.deleteAll(user.id);
|
||||
await this.albumRepository.deleteAll(user.id);
|
||||
await this.userRepository.delete(user, true);
|
||||
|
||||
|
||||
@@ -732,7 +732,13 @@ export class QsvHwDecodeConfig extends QsvSwDecodeConfig {
|
||||
throw new Error('No QSV device found');
|
||||
}
|
||||
|
||||
const options = ['-hwaccel qsv', '-hwaccel_output_format qsv', '-async_depth 4', ...this.getInputThreadOptions()];
|
||||
const options = [
|
||||
'-hwaccel qsv',
|
||||
'-hwaccel_output_format qsv',
|
||||
'-async_depth 4',
|
||||
'-noautorotate',
|
||||
...this.getInputThreadOptions(),
|
||||
];
|
||||
const hwDevice = this.getPreferredHardwareDevice();
|
||||
if (hwDevice) {
|
||||
options.push(`-qsv_device ${hwDevice}`);
|
||||
@@ -910,7 +916,7 @@ export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig {
|
||||
throw new Error('No RKMPP device found');
|
||||
}
|
||||
|
||||
return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'];
|
||||
return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga', '-noautorotate'];
|
||||
}
|
||||
|
||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||
|
||||
@@ -145,7 +145,7 @@ describe('mimeTypes', () => {
|
||||
|
||||
it('should be a sorted list', () => {
|
||||
const keys = Object.keys(mimeTypes.video);
|
||||
expect(keys).toEqual([...keys].sort());
|
||||
expect(keys).toEqual(keys.toSorted());
|
||||
});
|
||||
|
||||
it('should contain only video mime types', () => {
|
||||
@@ -171,7 +171,7 @@ describe('mimeTypes', () => {
|
||||
|
||||
it('should be a sorted list', () => {
|
||||
const keys = Object.keys(mimeTypes.sidecar);
|
||||
expect(keys).toEqual([...keys].sort());
|
||||
expect(keys).toEqual(keys.toSorted());
|
||||
});
|
||||
|
||||
it('should contain only xml mime types', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import cookieParser from 'cookie-parser';
|
||||
import { existsSync } from 'node:fs';
|
||||
import sirv from 'sirv';
|
||||
import { ApiModule } from 'src/app.module';
|
||||
import { envName, excludePaths, isDev, serverVersion, WEB_ROOT } from 'src/constants';
|
||||
import { envName, excludePaths, isDev, resourcePaths, serverVersion } from 'src/constants';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||
import { ApiService } from 'src/services/api.service';
|
||||
@@ -38,11 +38,11 @@ async function bootstrap() {
|
||||
useSwagger(app);
|
||||
|
||||
app.setGlobalPrefix('api', { exclude: excludePaths });
|
||||
if (existsSync(WEB_ROOT)) {
|
||||
if (existsSync(resourcePaths.web.root)) {
|
||||
// copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46
|
||||
// provides serving of precompressed assets and caching of immutable assets
|
||||
app.use(
|
||||
sirv(WEB_ROOT, {
|
||||
sirv(resourcePaths.web.root, {
|
||||
etag: true,
|
||||
gzip: true,
|
||||
brotli: true,
|
||||
|
||||
8
server/test/fixtures/asset.stub.ts
vendored
8
server/test/fixtures/asset.stub.ts
vendored
@@ -1,15 +1,17 @@
|
||||
import { AssetStackEntity } from 'src/entities/asset-stack.entity';
|
||||
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { fileStub } from 'test/fixtures/file.stub';
|
||||
import { libraryStub } from 'test/fixtures/library.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
|
||||
export const assetStackStub = (stackId: string, assets: AssetEntity[]): AssetStackEntity => {
|
||||
export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity => {
|
||||
return {
|
||||
id: stackId,
|
||||
assets: assets,
|
||||
owner: assets[0].owner,
|
||||
ownerId: assets[0].ownerId,
|
||||
primaryAsset: assets[0],
|
||||
primaryAssetId: assets[0].id,
|
||||
};
|
||||
@@ -161,7 +163,7 @@ export const assetStub = {
|
||||
exifImageHeight: 1000,
|
||||
exifImageWidth: 1000,
|
||||
} as ExifEntity,
|
||||
stack: assetStackStub('stack-1', [
|
||||
stack: stackStub('stack-1', [
|
||||
{ id: 'primary-asset-id' } as AssetEntity,
|
||||
{ id: 'stack-child-asset-1' } as AssetEntity,
|
||||
{ id: 'stack-child-asset-2' } as AssetEntity,
|
||||
|
||||
@@ -19,7 +19,6 @@ export const newAlbumRepositoryMock = (): Mocked<IAlbumRepository> => {
|
||||
removeAsset: vitest.fn(),
|
||||
removeAssetIds: vitest.fn(),
|
||||
getAssetIds: vitest.fn(),
|
||||
hasAsset: vitest.fn(),
|
||||
create: vitest.fn(),
|
||||
update: vitest.fn(),
|
||||
delete: vitest.fn(),
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newAssetStackRepositoryMock = (): Mocked<IAssetStackRepository> => {
|
||||
return {
|
||||
create: vitest.fn(),
|
||||
update: vitest.fn(),
|
||||
delete: vitest.fn(),
|
||||
getById: vitest.fn(),
|
||||
deleteAll: vitest.fn(),
|
||||
};
|
||||
};
|
||||
11
server/test/repositories/stack.repository.mock.ts
Normal file
11
server/test/repositories/stack.repository.mock.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { IStackRepository } from 'src/interfaces/stack.interface';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
|
||||
export const newStackRepositoryMock = (): Mocked<IStackRepository> => {
|
||||
return {
|
||||
create: vitest.fn(),
|
||||
update: vitest.fn(),
|
||||
delete: vitest.fn(),
|
||||
getById: vitest.fn(),
|
||||
};
|
||||
};
|
||||
@@ -10,6 +10,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2022",
|
||||
"moduleResolution": "node16",
|
||||
"lib": ["dom", "es2023"],
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"incremental": true,
|
||||
|
||||
141
web/package-lock.json
generated
141
web/package-lock.json
generated
@@ -55,7 +55,7 @@
|
||||
"factory.ts": "^1.4.1",
|
||||
"postcss": "^8.4.35",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"prettier-plugin-sort-json": "^4.0.0",
|
||||
"prettier-plugin-svelte": "^3.2.1",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
@@ -1921,9 +1921,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.17.tgz",
|
||||
"integrity": "sha512-wiADwq7VreR3ctOyxilAZOfPz3Jiy2IIp2C8gfafhTdQaVuGIHllfqQm8dXZKADymKr3uShxzgLZFT+a+CM4kA==",
|
||||
"version": "2.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.18.tgz",
|
||||
"integrity": "sha512-+g06hvpVAnH7b4CDjhnTDgFWBKBiQJpuSmQeGYOuzbO3SC3tdYjRNlDCrafvDtKbGiT2uxY5Dn9qdEUGVZdWOQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -2369,17 +2369,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz",
|
||||
"integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
|
||||
"integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "7.14.1",
|
||||
"@typescript-eslint/type-utils": "7.14.1",
|
||||
"@typescript-eslint/utils": "7.14.1",
|
||||
"@typescript-eslint/visitor-keys": "7.14.1",
|
||||
"@typescript-eslint/scope-manager": "7.15.0",
|
||||
"@typescript-eslint/type-utils": "7.15.0",
|
||||
"@typescript-eslint/utils": "7.15.0",
|
||||
"@typescript-eslint/visitor-keys": "7.15.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -2403,16 +2403,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz",
|
||||
"integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
|
||||
"integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.14.1",
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/typescript-estree": "7.14.1",
|
||||
"@typescript-eslint/visitor-keys": "7.14.1",
|
||||
"@typescript-eslint/scope-manager": "7.15.0",
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"@typescript-eslint/typescript-estree": "7.15.0",
|
||||
"@typescript-eslint/visitor-keys": "7.15.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2432,14 +2432,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz",
|
||||
"integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz",
|
||||
"integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/visitor-keys": "7.14.1"
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"@typescript-eslint/visitor-keys": "7.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -2450,14 +2450,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz",
|
||||
"integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
|
||||
"integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "7.14.1",
|
||||
"@typescript-eslint/utils": "7.14.1",
|
||||
"@typescript-eslint/typescript-estree": "7.15.0",
|
||||
"@typescript-eslint/utils": "7.15.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
@@ -2478,9 +2478,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz",
|
||||
"integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz",
|
||||
"integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2492,14 +2492,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz",
|
||||
"integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz",
|
||||
"integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/visitor-keys": "7.14.1",
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"@typescript-eslint/visitor-keys": "7.15.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -2560,16 +2560,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz",
|
||||
"integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
|
||||
"integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "7.14.1",
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/typescript-estree": "7.14.1"
|
||||
"@typescript-eslint/scope-manager": "7.15.0",
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"@typescript-eslint/typescript-estree": "7.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
@@ -2583,13 +2583,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "7.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz",
|
||||
"integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==",
|
||||
"version": "7.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz",
|
||||
"integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.14.1",
|
||||
"@typescript-eslint/types": "7.15.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6359,10 +6359,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"dev": true
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@@ -6424,9 +6425,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.38",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
||||
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
|
||||
"version": "8.4.39",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
|
||||
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -6442,9 +6443,10 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.0",
|
||||
"picocolors": "^1.0.1",
|
||||
"source-map-js": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6629,21 +6631,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-organize-imports": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz",
|
||||
"integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
|
||||
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@volar/vue-language-plugin-pug": "^1.0.4",
|
||||
"@volar/vue-typescript": "^1.0.4",
|
||||
"@vue/language-plugin-pug": "^2.0.24",
|
||||
"prettier": ">=2.0",
|
||||
"typescript": ">=2.9"
|
||||
"typescript": ">=2.9",
|
||||
"vue-tsc": "^2.0.24"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@volar/vue-language-plugin-pug": {
|
||||
"@vue/language-plugin-pug": {
|
||||
"optional": true
|
||||
},
|
||||
"@volar/vue-typescript": {
|
||||
"vue-tsc": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
@@ -8625,9 +8628,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
|
||||
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -8762,14 +8765,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz",
|
||||
"integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==",
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
|
||||
"integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss": "^8.4.39",
|
||||
"rollup": "^4.13.0"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"factory.ts": "^1.4.1",
|
||||
"postcss": "^8.4.35",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"prettier-plugin-sort-json": "^4.0.0",
|
||||
"prettier-plugin-svelte": "^3.2.1",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
|
||||
@@ -40,11 +40,6 @@
|
||||
ascent-override: 100%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Snowburst One';
|
||||
src: url('$lib/assets/fonts/SnowburstOne-Regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: 'Overpass', sans-serif;
|
||||
/* Used by layouts to ensure proper spacing between navbar and content */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user