From 079aa13edbc5edc4055466d88abdb26c8f265e98 Mon Sep 17 00:00:00 2001
From: Jason Rasmussen
Date: Mon, 14 Aug 2023 21:22:33 -0400
Subject: [PATCH 01/14] fix(web): use server api media types (#3687)
---
web/src/lib/utils/file-uploader.ts | 62 +++++++-----------------------
1 file changed, 13 insertions(+), 49 deletions(-)
diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts
index fad841c221..533ab372ef 100644
--- a/web/src/lib/utils/file-uploader.ts
+++ b/web/src/lib/utils/file-uploader.ts
@@ -1,62 +1,25 @@
import { uploadAssetsStore } from '$lib/stores/upload';
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
-import type { AssetFileUploadResponseDto } from '@api';
+import { api, AssetFileUploadResponseDto } from '@api';
import axios from 'axios';
import { notificationController, NotificationType } from './../components/shared-components/notification/notification';
-const extensions = [
- '.3fr',
- '.3gp',
- '.ari',
- '.arw',
- '.avi',
- '.avif',
- '.cap',
- '.cin',
- '.cr2',
- '.cr3',
- '.crw',
- '.dcr',
- '.dng',
- '.erf',
- '.fff',
- '.flv',
- '.gif',
- '.heic',
- '.heif',
- '.iiq',
- '.jpeg',
- '.jpg',
- '.k25',
- '.kdc',
- '.mkv',
- '.mov',
- '.mp2t',
- '.mp4',
- '.mpeg',
- '.mrw',
- '.nef',
- '.orf',
- '.ori',
- '.pef',
- '.png',
- '.raf',
- '.raw',
- '.rwl',
- '.sr2',
- '.srf',
- '.srw',
- '.tiff',
- '.webm',
- '.webp',
- '.wmv',
- '.x3f',
-];
+let _extensions: string[];
+
+const getExtensions = async () => {
+ if (!_extensions) {
+ const { data } = await api.serverInfoApi.getSupportedMediaTypes();
+ _extensions = [...data.image, ...data.video];
+ }
+ return _extensions;
+};
export const openFileUploadDialog = async (
albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined,
) => {
+ const extensions = await getExtensions();
+
return new Promise<(string | undefined)[]>((resolve, reject) => {
try {
const fileSelector = document.createElement('input');
@@ -87,6 +50,7 @@ export const fileUploadHandler = async (
albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined,
) => {
+ const extensions = await getExtensions();
const iterable = {
files: files.filter((file) => extensions.some((ext) => file.name.toLowerCase().endsWith(ext)))[Symbol.iterator](),
From f1b8a7ab5474fa3364fe3a5835374b85232d1dd0 Mon Sep 17 00:00:00 2001
From: Russell Tan
Date: Mon, 14 Aug 2023 18:37:17 -0700
Subject: [PATCH 02/14] fix(server): Does not assign lat/lon if they are at 0,0
#2991 (#3669)
* fix(server): Does not assign lat/lon if they are at 0,0 #2991
* Adds migration file to fix null island rows
* Removed down migration
* Leave empty down function
---
.../migrations/1692057328660-fixGPSNullIsland.ts | 13 +++++++++++++
.../processors/metadata-extraction.processor.ts | 12 ++++++++++--
.../microservices/utils/exif/coordinates.spec.ts | 12 ++++++++++++
server/src/microservices/utils/exif/coordinates.ts | 11 +++++++++--
4 files changed, 44 insertions(+), 4 deletions(-)
create mode 100644 server/src/infra/migrations/1692057328660-fixGPSNullIsland.ts
diff --git a/server/src/infra/migrations/1692057328660-fixGPSNullIsland.ts b/server/src/infra/migrations/1692057328660-fixGPSNullIsland.ts
new file mode 100644
index 0000000000..74dc40a474
--- /dev/null
+++ b/server/src/infra/migrations/1692057328660-fixGPSNullIsland.ts
@@ -0,0 +1,13 @@
+import { MigrationInterface, QueryRunner } from "typeorm"
+
+export class FixGPSNullIsland1692057328660 implements MigrationInterface {
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`UPDATE "exif" SET latitude = NULL, longitude = NULL WHERE latitude = 0 AND longitude = 0;`);
+ }
+
+ public async down(): Promise {
+ // Setting lat,lon to 0 not necessary
+ }
+
+}
diff --git a/server/src/microservices/processors/metadata-extraction.processor.ts b/server/src/microservices/processors/metadata-extraction.processor.ts
index a37ee6a23a..a5f3ed4dd3 100644
--- a/server/src/microservices/processors/metadata-extraction.processor.ts
+++ b/server/src/microservices/processors/metadata-extraction.processor.ts
@@ -308,8 +308,16 @@ export class MetadataExtractionProcessor {
const latitude = getExifProperty('GPSLatitude');
const longitude = getExifProperty('GPSLongitude');
- newExif.latitude = latitude !== null ? parseLatitude(latitude) : null;
- newExif.longitude = longitude !== null ? parseLongitude(longitude) : null;
+ const lat = parseLatitude(latitude);
+ const lon = parseLongitude(longitude);
+
+ if (lat === 0 && lon === 0) {
+ this.logger.warn(`Latitude & Longitude were on Null Island (${lat},${lon}), not assigning coordinates`);
+ } else {
+ newExif.latitude = lat;
+ newExif.longitude = lon;
+ }
+
if (getExifProperty('MotionPhoto')) {
// Seen on more recent Pixel phones: starting as early as Pixel 4a, possibly earlier.
const rawDirectory = getExifProperty('Directory');
diff --git a/server/src/microservices/utils/exif/coordinates.spec.ts b/server/src/microservices/utils/exif/coordinates.spec.ts
index 975d1e9969..223a7671ec 100644
--- a/server/src/microservices/utils/exif/coordinates.spec.ts
+++ b/server/src/microservices/utils/exif/coordinates.spec.ts
@@ -23,6 +23,12 @@ describe('parsing latitude from string input', () => {
});
});
+describe('parsing latitude from null input', () => {
+ it('returns null for null input', () => {
+ expect(parseLatitude(null)).toBeNull();
+ });
+});
+
describe('parsing longitude from string input', () => {
it('returns null for invalid inputs', () => {
expect(parseLongitude('')).toBeNull();
@@ -44,3 +50,9 @@ describe('parsing longitude from string input', () => {
expect(parseLongitude('-0.0')).toBeCloseTo(-0.0);
});
});
+
+describe('parsing longitude from null input', () => {
+ it('returns null for null input', () => {
+ expect(parseLongitude(null)).toBeNull();
+ });
+});
diff --git a/server/src/microservices/utils/exif/coordinates.ts b/server/src/microservices/utils/exif/coordinates.ts
index 3a07b0c173..03aeb17f09 100644
--- a/server/src/microservices/utils/exif/coordinates.ts
+++ b/server/src/microservices/utils/exif/coordinates.ts
@@ -1,6 +1,9 @@
import { isNumberInRange } from '../numbers';
-export function parseLatitude(input: string | number): number | null {
+export function parseLatitude(input: string | number | null): number | null {
+ if (input === null) {
+ return null;
+ }
const latitude = typeof input === 'string' ? Number.parseFloat(input) : input;
if (isNumberInRange(latitude, -90, 90)) {
@@ -9,7 +12,11 @@ export function parseLatitude(input: string | number): number | null {
return null;
}
-export function parseLongitude(input: string | number): number | null {
+export function parseLongitude(input: string | number | null): number | null {
+ if (input === null) {
+ return null;
+ }
+
const longitude = typeof input === 'string' ? Number.parseFloat(input) : input;
if (isNumberInRange(longitude, -180, 180)) {
From 7ca6f80ed26aa78ed21a3e1cf3fff02f2223a594 Mon Sep 17 00:00:00 2001
From: Jason Rasmussen
Date: Mon, 14 Aug 2023 23:14:52 -0400
Subject: [PATCH 03/14] fix(server): display insta-360 (#3688)
---
server/src/domain/domain.constant.ts | 2 ++
web/src/lib/components/asset-viewer/asset-viewer.svelte | 4 +++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/server/src/domain/domain.constant.ts b/server/src/domain/domain.constant.ts
index a1cb0a7dfe..b06720613c 100644
--- a/server/src/domain/domain.constant.ts
+++ b/server/src/domain/domain.constant.ts
@@ -48,6 +48,7 @@ const image: Record = {
'.heic': ['image/heic'],
'.heif': ['image/heif'],
'.iiq': ['image/iiq', 'image/x-phaseone-iiq'],
+ '.insp': ['image/jpeg'],
'.jpeg': ['image/jpeg'],
'.jpg': ['image/jpeg'],
'.jxl': ['image/jxl'],
@@ -79,6 +80,7 @@ const video: Record = {
'.3gp': ['video/3gpp'],
'.avi': ['video/avi', 'video/msvideo', 'video/vnd.avi', 'video/x-msvideo'],
'.flv': ['video/x-flv'],
+ '.insv': ['video/mp4'],
'.m2ts': ['video/mp2t'],
'.mkv': ['video/x-matroska'],
'.mov': ['video/quicktime'],
diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte
index e65b45e808..5df79b1590 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte
@@ -297,7 +297,9 @@
on:close={closeViewer}
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
/>
- {:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR}
+ {:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || asset.originalPath
+ .toLowerCase()
+ .endsWith('.insp')}
{:else}
From a3b6095b614ae5123cf7ce3fbead8c234da7d56e Mon Sep 17 00:00:00 2001
From: Alex The Bot
Date: Tue, 15 Aug 2023 12:45:48 +0000
Subject: [PATCH 04/14] Version v1.73.0
---
cli/src/api/open-api/api.ts | 2 +-
cli/src/api/open-api/base.ts | 2 +-
cli/src/api/open-api/common.ts | 2 +-
cli/src/api/open-api/configuration.ts | 2 +-
cli/src/api/open-api/index.ts | 2 +-
machine-learning/pyproject.toml | 2 +-
mobile/android/fastlane/Fastfile | 4 ++--
mobile/ios/fastlane/Fastfile | 2 +-
mobile/openapi/README.md | 2 +-
mobile/pubspec.yaml | 2 +-
server/immich-openapi-specs.json | 2 +-
server/package-lock.json | 4 ++--
server/package.json | 2 +-
web/src/api/open-api/api.ts | 2 +-
web/src/api/open-api/base.ts | 2 +-
web/src/api/open-api/common.ts | 2 +-
web/src/api/open-api/configuration.ts | 2 +-
web/src/api/open-api/index.ts | 2 +-
18 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts
index e8662175a4..956f19ba7b 100644
--- a/cli/src/api/open-api/api.ts
+++ b/cli/src/api/open-api/api.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.72.2
+ * The version of the OpenAPI document: 1.73.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
diff --git a/cli/src/api/open-api/base.ts b/cli/src/api/open-api/base.ts
index d636bfdd9a..7109cba71e 100644
--- a/cli/src/api/open-api/base.ts
+++ b/cli/src/api/open-api/base.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.72.2
+ * The version of the OpenAPI document: 1.73.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
diff --git a/cli/src/api/open-api/common.ts b/cli/src/api/open-api/common.ts
index 6b75041a70..66890914ad 100644
--- a/cli/src/api/open-api/common.ts
+++ b/cli/src/api/open-api/common.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.72.2
+ * The version of the OpenAPI document: 1.73.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
diff --git a/cli/src/api/open-api/configuration.ts b/cli/src/api/open-api/configuration.ts
index 275f99f72a..21fd71538b 100644
--- a/cli/src/api/open-api/configuration.ts
+++ b/cli/src/api/open-api/configuration.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.72.2
+ * The version of the OpenAPI document: 1.73.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
diff --git a/cli/src/api/open-api/index.ts b/cli/src/api/open-api/index.ts
index 7047458436..937e9a24ad 100644
--- a/cli/src/api/open-api/index.ts
+++ b/cli/src/api/open-api/index.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.72.2
+ * The version of the OpenAPI document: 1.73.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml
index 4547dcb052..b2fa1b28e3 100644
--- a/machine-learning/pyproject.toml
+++ b/machine-learning/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
-version = "1.72.2"
+version = "1.73.0"
description = ""
authors = ["Hau Tran "]
readme = "README.md"
diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile
index 0de6cb8aa7..e9be5f6548 100644
--- a/mobile/android/fastlane/Fastfile
+++ b/mobile/android/fastlane/Fastfile
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
- "android.injected.version.code" => 95,
- "android.injected.version.name" => "1.72.2",
+ "android.injected.version.code" => 96,
+ "android.injected.version.name" => "1.73.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile
index 56bf4ba34e..7707eadb9d 100644
--- a/mobile/ios/fastlane/Fastfile
+++ b/mobile/ios/fastlane/Fastfile
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
- version_number: "1.72.2"
+ version_number: "1.73.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 2942d1d854..1e20582fae 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
-- API version: 1.72.2
+- API version: 1.73.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index 5c75bfb7e9..dc90a5373c 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
-version: 1.72.2+95
+version: 1.73.0+96
isar_version: &isar_version 3.1.0+1
environment:
diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json
index a60fcbd544..59543353fb 100644
--- a/server/immich-openapi-specs.json
+++ b/server/immich-openapi-specs.json
@@ -4590,7 +4590,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
- "version": "1.72.2",
+ "version": "1.73.0",
"contact": {}
},
"tags": [],
diff --git a/server/package-lock.json b/server/package-lock.json
index fa3fbf12a6..e6e2de6aba 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "immich",
- "version": "1.72.2",
+ "version": "1.73.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
- "version": "1.72.2",
+ "version": "1.73.0",
"license": "UNLICENSED",
"dependencies": {
"@babel/runtime": "^7.20.13",
diff --git a/server/package.json b/server/package.json
index 2baf087bf6..cdfb87d59a 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "immich",
- "version": "1.72.2",
+ "version": "1.73.0",
"description": "",
"author": "",
"private": true,
diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts
index e8662175a4..956f19ba7b 100644
--- a/web/src/api/open-api/api.ts
+++ b/web/src/api/open-api/api.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.72.2
+ * The version of the OpenAPI document: 1.73.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
diff --git a/web/src/api/open-api/base.ts b/web/src/api/open-api/base.ts
index d636bfdd9a..7109cba71e 100644
--- a/web/src/api/open-api/base.ts
+++ b/web/src/api/open-api/base.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.72.2
+ * The version of the OpenAPI document: 1.73.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
diff --git a/web/src/api/open-api/common.ts b/web/src/api/open-api/common.ts
index 6b75041a70..66890914ad 100644
--- a/web/src/api/open-api/common.ts
+++ b/web/src/api/open-api/common.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.72.2
+ * The version of the OpenAPI document: 1.73.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
diff --git a/web/src/api/open-api/configuration.ts b/web/src/api/open-api/configuration.ts
index 275f99f72a..21fd71538b 100644
--- a/web/src/api/open-api/configuration.ts
+++ b/web/src/api/open-api/configuration.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.72.2
+ * The version of the OpenAPI document: 1.73.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
diff --git a/web/src/api/open-api/index.ts b/web/src/api/open-api/index.ts
index 7047458436..937e9a24ad 100644
--- a/web/src/api/open-api/index.ts
+++ b/web/src/api/open-api/index.ts
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
- * The version of the OpenAPI document: 1.72.2
+ * The version of the OpenAPI document: 1.73.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
From a75f368d5b323d978e9a83a9723495bccf1d8402 Mon Sep 17 00:00:00 2001
From: Alex
Date: Tue, 15 Aug 2023 09:42:28 -0500
Subject: [PATCH 05/14] chore: post update
---
mobile/android/fastlane/report.xml | 6 +++---
mobile/ios/Podfile.lock | 2 +-
mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++---
mobile/ios/Runner/Info.plist | 4 ++--
mobile/ios/fastlane/report.xml | 12 ++++++------
5 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml
index 45b5cef2d8..d268a40aee 100644
--- a/mobile/android/fastlane/report.xml
+++ b/mobile/android/fastlane/report.xml
@@ -5,17 +5,17 @@
-
+
-
+
-
+
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index 7b7cbd0d57..4902388c6d 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -163,4 +163,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
-COCOAPODS: 1.11.3
+COCOAPODS: 1.12.1
diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj
index 9f463abbe7..8bb1e31927 100644
--- a/mobile/ios/Runner.xcodeproj/project.pbxproj
+++ b/mobile/ios/Runner.xcodeproj/project.pbxproj
@@ -379,7 +379,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 110;
+ CURRENT_PROJECT_VERSION = 113;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -515,7 +515,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 110;
+ CURRENT_PROJECT_VERSION = 113;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 110;
+ CURRENT_PROJECT_VERSION = 113;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index 041dcd2ebd..ae8c18518e 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -59,11 +59,11 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.70.0
+ 1.73.0
CFBundleSignature
????
CFBundleVersion
- 110
+ 113
FLTEnableImpeller
ITSAppUsesNonExemptEncryption
diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml
index f1ee5f8278..3f00787c15 100644
--- a/mobile/ios/fastlane/report.xml
+++ b/mobile/ios/fastlane/report.xml
@@ -5,32 +5,32 @@
-
+
-
+
-
+
-
+
-
+
-
+
From efc7fdb669002ab8ed6363141e1ea0400cd2355b Mon Sep 17 00:00:00 2001
From: Jason Rasmussen
Date: Tue, 15 Aug 2023 11:49:32 -0400
Subject: [PATCH 06/14] fix(web,server): use POST request to get download info
(#3694)
* fix(web,server): use POST request to get download info
* chore: open api
---
cli/src/api/open-api/api.ts | 107 ++++++------
mobile/openapi/.openapi-generator/FILES | 3 +
mobile/openapi/README.md | 5 +-
mobile/openapi/doc/AssetApi.md | 20 +--
mobile/openapi/doc/DownloadInfoDto.md | 18 +++
mobile/openapi/lib/api.dart | 1 +
mobile/openapi/lib/api/asset_api.dart | 48 ++----
mobile/openapi/lib/api_client.dart | 2 +
.../openapi/lib/model/download_info_dto.dart | 150 +++++++++++++++++
mobile/openapi/test/asset_api_test.dart | 2 +-
.../openapi/test/download_info_dto_test.dart | 42 +++++
server/immich-openapi-specs.json | 153 +++++++++---------
server/src/domain/asset/asset.service.ts | 6 +-
server/src/domain/asset/dto/download.dto.ts | 3 +-
.../immich/controllers/asset.controller.ts | 8 +-
web/src/api/open-api/api.ts | 107 ++++++------
web/src/lib/utils/asset-utils.ts | 10 +-
17 files changed, 426 insertions(+), 259 deletions(-)
create mode 100644 mobile/openapi/doc/DownloadInfoDto.md
create mode 100644 mobile/openapi/lib/model/download_info_dto.dart
create mode 100644 mobile/openapi/test/download_info_dto_test.dart
diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts
index 956f19ba7b..8ade1d5fde 100644
--- a/cli/src/api/open-api/api.ts
+++ b/cli/src/api/open-api/api.ts
@@ -1132,6 +1132,37 @@ export interface DownloadArchiveInfo {
*/
'size': number;
}
+/**
+ *
+ * @export
+ * @interface DownloadInfoDto
+ */
+export interface DownloadInfoDto {
+ /**
+ *
+ * @type {string}
+ * @memberof DownloadInfoDto
+ */
+ 'albumId'?: string;
+ /**
+ *
+ * @type {number}
+ * @memberof DownloadInfoDto
+ */
+ 'archiveSize'?: number;
+ /**
+ *
+ * @type {Array}
+ * @memberof DownloadInfoDto
+ */
+ 'assetIds'?: Array;
+ /**
+ *
+ * @type {string}
+ * @memberof DownloadInfoDto
+ */
+ 'userId'?: string;
+}
/**
*
* @export
@@ -4880,7 +4911,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
downloadArchive: async (assetIdsDto: AssetIdsDto, key?: string, options: AxiosRequestConfig = {}): Promise => {
// verify required parameter 'assetIdsDto' is not null or undefined
assertParamExists('downloadArchive', 'assetIdsDto', assetIdsDto)
- const localVarPath = `/asset/download`;
+ const localVarPath = `/asset/download/archive`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@@ -5379,16 +5410,15 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
},
/**
*
- * @param {Array} [assetIds]
- * @param {string} [albumId]
- * @param {string} [userId]
- * @param {number} [archiveSize]
+ * @param {DownloadInfoDto} downloadInfoDto
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
- getDownloadInfo: async (assetIds?: Array, albumId?: string, userId?: string, archiveSize?: number, key?: string, options: AxiosRequestConfig = {}): Promise => {
- const localVarPath = `/asset/download`;
+ getDownloadInfo: async (downloadInfoDto: DownloadInfoDto, key?: string, options: AxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'downloadInfoDto' is not null or undefined
+ assertParamExists('getDownloadInfo', 'downloadInfoDto', downloadInfoDto)
+ const localVarPath = `/asset/download/info`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@@ -5396,7 +5426,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
baseOptions = configuration.baseOptions;
}
- const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+ const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
@@ -5409,31 +5439,18 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
- if (assetIds) {
- localVarQueryParameter['assetIds'] = assetIds;
- }
-
- if (albumId !== undefined) {
- localVarQueryParameter['albumId'] = albumId;
- }
-
- if (userId !== undefined) {
- localVarQueryParameter['userId'] = userId;
- }
-
- if (archiveSize !== undefined) {
- localVarQueryParameter['archiveSize'] = archiveSize;
- }
-
if (key !== undefined) {
localVarQueryParameter['key'] = key;
}
+ localVarHeaderParameter['Content-Type'] = 'application/json';
+
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+ localVarRequestOptions.data = serializeDataIfNeeded(downloadInfoDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
@@ -6141,16 +6158,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
},
/**
*
- * @param {Array} [assetIds]
- * @param {string} [albumId]
- * @param {string} [userId]
- * @param {number} [archiveSize]
+ * @param {DownloadInfoDto} downloadInfoDto
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
- async getDownloadInfo(assetIds?: Array, albumId?: string, userId?: string, archiveSize?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
- const localVarAxiosArgs = await localVarAxiosParamCreator.getDownloadInfo(assetIds, albumId, userId, archiveSize, key, options);
+ async getDownloadInfo(downloadInfoDto: DownloadInfoDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.getDownloadInfo(downloadInfoDto, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@@ -6406,8 +6420,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
- getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest = {}, options?: AxiosRequestConfig): AxiosPromise {
- return localVarFp.getDownloadInfo(requestParameters.assetIds, requestParameters.albumId, requestParameters.userId, requestParameters.archiveSize, requestParameters.key, options).then((request) => request(axios, basePath));
+ getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest, options?: AxiosRequestConfig): AxiosPromise {
+ return localVarFp.getDownloadInfo(requestParameters.downloadInfoDto, requestParameters.key, options).then((request) => request(axios, basePath));
},
/**
*
@@ -6788,31 +6802,10 @@ export interface AssetApiGetByTimeBucketRequest {
export interface AssetApiGetDownloadInfoRequest {
/**
*
- * @type {Array}
+ * @type {DownloadInfoDto}
* @memberof AssetApiGetDownloadInfo
*/
- readonly assetIds?: Array
-
- /**
- *
- * @type {string}
- * @memberof AssetApiGetDownloadInfo
- */
- readonly albumId?: string
-
- /**
- *
- * @type {string}
- * @memberof AssetApiGetDownloadInfo
- */
- readonly userId?: string
-
- /**
- *
- * @type {number}
- * @memberof AssetApiGetDownloadInfo
- */
- readonly archiveSize?: number
+ readonly downloadInfoDto: DownloadInfoDto
/**
*
@@ -7281,8 +7274,8 @@ export class AssetApi extends BaseAPI {
* @throws {RequiredError}
* @memberof AssetApi
*/
- public getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest = {}, options?: AxiosRequestConfig) {
- return AssetApiFp(this.configuration).getDownloadInfo(requestParameters.assetIds, requestParameters.albumId, requestParameters.userId, requestParameters.archiveSize, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
+ public getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest, options?: AxiosRequestConfig) {
+ return AssetApiFp(this.configuration).getDownloadInfo(requestParameters.downloadInfoDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
}
/**
diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES
index ea0993be8c..bf485ef08b 100644
--- a/mobile/openapi/.openapi-generator/FILES
+++ b/mobile/openapi/.openapi-generator/FILES
@@ -45,6 +45,7 @@ doc/DeleteAssetDto.md
doc/DeleteAssetResponseDto.md
doc/DeleteAssetStatus.md
doc/DownloadArchiveInfo.md
+doc/DownloadInfoDto.md
doc/DownloadResponseDto.md
doc/ExifResponseDto.md
doc/ImportAssetDto.md
@@ -186,6 +187,7 @@ lib/model/delete_asset_dto.dart
lib/model/delete_asset_response_dto.dart
lib/model/delete_asset_status.dart
lib/model/download_archive_info.dart
+lib/model/download_info_dto.dart
lib/model/download_response_dto.dart
lib/model/exif_response_dto.dart
lib/model/import_asset_dto.dart
@@ -298,6 +300,7 @@ test/delete_asset_dto_test.dart
test/delete_asset_response_dto_test.dart
test/delete_asset_status_test.dart
test/download_archive_info_test.dart
+test/download_info_dto_test.dart
test/download_response_dto_test.dart
test/exif_response_dto_test.dart
test/import_asset_dto_test.dart
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 1e20582fae..9167613b25 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -91,7 +91,7 @@ Class | Method | HTTP request | Description
*AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check |
*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |
*AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset |
-*AssetApi* | [**downloadArchive**](doc//AssetApi.md#downloadarchive) | **POST** /asset/download |
+*AssetApi* | [**downloadArchive**](doc//AssetApi.md#downloadarchive) | **POST** /asset/download/archive |
*AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **POST** /asset/download/{id} |
*AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset |
*AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} |
@@ -101,7 +101,7 @@ Class | Method | HTTP request | Description
*AssetApi* | [**getByTimeBucket**](doc//AssetApi.md#getbytimebucket) | **GET** /asset/time-bucket |
*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
*AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
-*AssetApi* | [**getDownloadInfo**](doc//AssetApi.md#getdownloadinfo) | **GET** /asset/download |
+*AssetApi* | [**getDownloadInfo**](doc//AssetApi.md#getdownloadinfo) | **POST** /asset/download/info |
*AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
*AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
*AssetApi* | [**getTimeBuckets**](doc//AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets |
@@ -216,6 +216,7 @@ Class | Method | HTTP request | Description
- [DeleteAssetResponseDto](doc//DeleteAssetResponseDto.md)
- [DeleteAssetStatus](doc//DeleteAssetStatus.md)
- [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
+ - [DownloadInfoDto](doc//DownloadInfoDto.md)
- [DownloadResponseDto](doc//DownloadResponseDto.md)
- [ExifResponseDto](doc//ExifResponseDto.md)
- [ImportAssetDto](doc//ImportAssetDto.md)
diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md
index 2999f2de2a..5c1c7bb4bf 100644
--- a/mobile/openapi/doc/AssetApi.md
+++ b/mobile/openapi/doc/AssetApi.md
@@ -13,7 +13,7 @@ Method | HTTP request | Description
[**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check |
[**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist |
[**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset |
-[**downloadArchive**](AssetApi.md#downloadarchive) | **POST** /asset/download |
+[**downloadArchive**](AssetApi.md#downloadarchive) | **POST** /asset/download/archive |
[**downloadFile**](AssetApi.md#downloadfile) | **POST** /asset/download/{id} |
[**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset |
[**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} |
@@ -23,7 +23,7 @@ Method | HTTP request | Description
[**getByTimeBucket**](AssetApi.md#getbytimebucket) | **GET** /asset/time-bucket |
[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
-[**getDownloadInfo**](AssetApi.md#getdownloadinfo) | **GET** /asset/download |
+[**getDownloadInfo**](AssetApi.md#getdownloadinfo) | **POST** /asset/download/info |
[**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
[**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
[**getTimeBuckets**](AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets |
@@ -842,7 +842,7 @@ This endpoint does not need any parameter.
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getDownloadInfo**
-> DownloadResponseDto getDownloadInfo(assetIds, albumId, userId, archiveSize, key)
+> DownloadResponseDto getDownloadInfo(downloadInfoDto, key)
@@ -865,14 +865,11 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
-final assetIds = []; // List |
-final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
-final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
-final archiveSize = 8.14; // num |
+final downloadInfoDto = DownloadInfoDto(); // DownloadInfoDto |
final key = key_example; // String |
try {
- final result = api_instance.getDownloadInfo(assetIds, albumId, userId, archiveSize, key);
+ final result = api_instance.getDownloadInfo(downloadInfoDto, key);
print(result);
} catch (e) {
print('Exception when calling AssetApi->getDownloadInfo: $e\n');
@@ -883,10 +880,7 @@ try {
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
- **assetIds** | [**List**](String.md)| | [optional] [default to const []]
- **albumId** | **String**| | [optional]
- **userId** | **String**| | [optional]
- **archiveSize** | **num**| | [optional]
+ **downloadInfoDto** | [**DownloadInfoDto**](DownloadInfoDto.md)| |
**key** | **String**| | [optional]
### Return type
@@ -899,7 +893,7 @@ Name | Type | Description | Notes
### HTTP request headers
- - **Content-Type**: Not defined
+ - **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
diff --git a/mobile/openapi/doc/DownloadInfoDto.md b/mobile/openapi/doc/DownloadInfoDto.md
new file mode 100644
index 0000000000..14d5d1e712
--- /dev/null
+++ b/mobile/openapi/doc/DownloadInfoDto.md
@@ -0,0 +1,18 @@
+# openapi.model.DownloadInfoDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**albumId** | **String** | | [optional]
+**archiveSize** | **int** | | [optional]
+**assetIds** | **List** | | [optional] [default to const []]
+**userId** | **String** | | [optional]
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 644244a103..a38d8784e3 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -81,6 +81,7 @@ part 'model/delete_asset_dto.dart';
part 'model/delete_asset_response_dto.dart';
part 'model/delete_asset_status.dart';
part 'model/download_archive_info.dart';
+part 'model/download_info_dto.dart';
part 'model/download_response_dto.dart';
part 'model/exif_response_dto.dart';
part 'model/import_asset_dto.dart';
diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart
index 6f4c391bf8..ba9f1d5a1f 100644
--- a/mobile/openapi/lib/api/asset_api.dart
+++ b/mobile/openapi/lib/api/asset_api.dart
@@ -230,7 +230,7 @@ class AssetApi {
return null;
}
- /// Performs an HTTP 'POST /asset/download' operation and returns the [Response].
+ /// Performs an HTTP 'POST /asset/download/archive' operation and returns the [Response].
/// Parameters:
///
/// * [AssetIdsDto] assetIdsDto (required):
@@ -238,7 +238,7 @@ class AssetApi {
/// * [String] key:
Future downloadArchiveWithHttpInfo(AssetIdsDto assetIdsDto, { String? key, }) async {
// ignore: prefer_const_declarations
- final path = r'/asset/download';
+ final path = r'/asset/download/archive';
// ignore: prefer_final_locals
Object? postBody = assetIdsDto;
@@ -853,51 +853,33 @@ class AssetApi {
return null;
}
- /// Performs an HTTP 'GET /asset/download' operation and returns the [Response].
+ /// Performs an HTTP 'POST /asset/download/info' operation and returns the [Response].
/// Parameters:
///
- /// * [List] assetIds:
- ///
- /// * [String] albumId:
- ///
- /// * [String] userId:
- ///
- /// * [num] archiveSize:
+ /// * [DownloadInfoDto] downloadInfoDto (required):
///
/// * [String] key:
- Future getDownloadInfoWithHttpInfo({ List? assetIds, String? albumId, String? userId, num? archiveSize, String? key, }) async {
+ Future getDownloadInfoWithHttpInfo(DownloadInfoDto downloadInfoDto, { String? key, }) async {
// ignore: prefer_const_declarations
- final path = r'/asset/download';
+ final path = r'/asset/download/info';
// ignore: prefer_final_locals
- Object? postBody;
+ Object? postBody = downloadInfoDto;
final queryParams = [];
final headerParams = {};
final formParams = {};
- if (assetIds != null) {
- queryParams.addAll(_queryParams('multi', 'assetIds', assetIds));
- }
- if (albumId != null) {
- queryParams.addAll(_queryParams('', 'albumId', albumId));
- }
- if (userId != null) {
- queryParams.addAll(_queryParams('', 'userId', userId));
- }
- if (archiveSize != null) {
- queryParams.addAll(_queryParams('', 'archiveSize', archiveSize));
- }
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
- const contentTypes = [];
+ const contentTypes = ['application/json'];
return apiClient.invokeAPI(
path,
- 'GET',
+ 'POST',
queryParams,
postBody,
headerParams,
@@ -908,17 +890,11 @@ class AssetApi {
/// Parameters:
///
- /// * [List] assetIds:
- ///
- /// * [String] albumId:
- ///
- /// * [String] userId:
- ///
- /// * [num] archiveSize:
+ /// * [DownloadInfoDto] downloadInfoDto (required):
///
/// * [String] key:
- Future getDownloadInfo({ List? assetIds, String? albumId, String? userId, num? archiveSize, String? key, }) async {
- final response = await getDownloadInfoWithHttpInfo( assetIds: assetIds, albumId: albumId, userId: userId, archiveSize: archiveSize, key: key, );
+ Future getDownloadInfo(DownloadInfoDto downloadInfoDto, { String? key, }) async {
+ final response = await getDownloadInfoWithHttpInfo(downloadInfoDto, key: key, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index fd1252cc78..eb76c12c58 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -257,6 +257,8 @@ class ApiClient {
return DeleteAssetStatusTypeTransformer().decode(value);
case 'DownloadArchiveInfo':
return DownloadArchiveInfo.fromJson(value);
+ case 'DownloadInfoDto':
+ return DownloadInfoDto.fromJson(value);
case 'DownloadResponseDto':
return DownloadResponseDto.fromJson(value);
case 'ExifResponseDto':
diff --git a/mobile/openapi/lib/model/download_info_dto.dart b/mobile/openapi/lib/model/download_info_dto.dart
new file mode 100644
index 0000000000..fa3a06eeac
--- /dev/null
+++ b/mobile/openapi/lib/model/download_info_dto.dart
@@ -0,0 +1,150 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// 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 DownloadInfoDto {
+ /// Returns a new [DownloadInfoDto] instance.
+ DownloadInfoDto({
+ this.albumId,
+ this.archiveSize,
+ this.assetIds = const [],
+ this.userId,
+ });
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ String? albumId;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ int? archiveSize;
+
+ List assetIds;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ String? userId;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is DownloadInfoDto &&
+ other.albumId == albumId &&
+ other.archiveSize == archiveSize &&
+ other.assetIds == assetIds &&
+ other.userId == userId;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (albumId == null ? 0 : albumId!.hashCode) +
+ (archiveSize == null ? 0 : archiveSize!.hashCode) +
+ (assetIds.hashCode) +
+ (userId == null ? 0 : userId!.hashCode);
+
+ @override
+ String toString() => 'DownloadInfoDto[albumId=$albumId, archiveSize=$archiveSize, assetIds=$assetIds, userId=$userId]';
+
+ Map toJson() {
+ final json = {};
+ if (this.albumId != null) {
+ json[r'albumId'] = this.albumId;
+ } else {
+ // json[r'albumId'] = null;
+ }
+ if (this.archiveSize != null) {
+ json[r'archiveSize'] = this.archiveSize;
+ } else {
+ // json[r'archiveSize'] = null;
+ }
+ json[r'assetIds'] = this.assetIds;
+ if (this.userId != null) {
+ json[r'userId'] = this.userId;
+ } else {
+ // json[r'userId'] = null;
+ }
+ return json;
+ }
+
+ /// Returns a new [DownloadInfoDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static DownloadInfoDto? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ return DownloadInfoDto(
+ albumId: mapValueOfType(json, r'albumId'),
+ archiveSize: mapValueOfType(json, r'archiveSize'),
+ assetIds: json[r'assetIds'] is List
+ ? (json[r'assetIds'] as List).cast()
+ : const [],
+ userId: mapValueOfType(json, r'userId'),
+ );
+ }
+ return null;
+ }
+
+ static List listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = DownloadInfoDto.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = DownloadInfoDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of DownloadInfoDto-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ // ignore: parameter_assignments
+ json = json.cast();
+ for (final entry in json.entries) {
+ map[entry.key] = DownloadInfoDto.listFromJson(entry.value, growable: growable,);
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ };
+}
+
diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart
index 588902a14c..ebb472c4a0 100644
--- a/mobile/openapi/test/asset_api_test.dart
+++ b/mobile/openapi/test/asset_api_test.dart
@@ -97,7 +97,7 @@ void main() {
// TODO
});
- //Future getDownloadInfo({ List assetIds, String albumId, String userId, num archiveSize, String key }) async
+ //Future getDownloadInfo(DownloadInfoDto downloadInfoDto, { String key }) async
test('test getDownloadInfo', () async {
// TODO
});
diff --git a/mobile/openapi/test/download_info_dto_test.dart b/mobile/openapi/test/download_info_dto_test.dart
new file mode 100644
index 0000000000..5efd4e11eb
--- /dev/null
+++ b/mobile/openapi/test/download_info_dto_test.dart
@@ -0,0 +1,42 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// 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
+
+import 'package:openapi/api.dart';
+import 'package:test/test.dart';
+
+// tests for DownloadInfoDto
+void main() {
+ // final instance = DownloadInfoDto();
+
+ group('test DownloadInfoDto', () {
+ // String albumId
+ test('to test the property `albumId`', () async {
+ // TODO
+ });
+
+ // int archiveSize
+ test('to test the property `archiveSize`', () async {
+ // TODO
+ });
+
+ // List assetIds (default value: const [])
+ test('to test the property `assetIds`', () async {
+ // TODO
+ });
+
+ // String userId
+ test('to test the property `userId`', () async {
+ // TODO
+ });
+
+
+ });
+
+}
diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json
index 59543353fb..f193eb25e4 100644
--- a/server/immich-openapi-specs.json
+++ b/server/immich-openapi-specs.json
@@ -1026,84 +1026,7 @@
]
}
},
- "/asset/download": {
- "get": {
- "operationId": "getDownloadInfo",
- "parameters": [
- {
- "name": "assetIds",
- "required": false,
- "in": "query",
- "schema": {
- "format": "uuid",
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- {
- "name": "albumId",
- "required": false,
- "in": "query",
- "schema": {
- "format": "uuid",
- "type": "string"
- }
- },
- {
- "name": "userId",
- "required": false,
- "in": "query",
- "schema": {
- "format": "uuid",
- "type": "string"
- }
- },
- {
- "name": "archiveSize",
- "required": false,
- "in": "query",
- "schema": {
- "type": "number"
- }
- },
- {
- "name": "key",
- "required": false,
- "in": "query",
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/DownloadResponseDto"
- }
- }
- },
- "description": ""
- }
- },
- "security": [
- {
- "bearer": []
- },
- {
- "cookie": []
- },
- {
- "api_key": []
- }
- ],
- "tags": [
- "Asset"
- ]
- },
+ "/asset/download/archive": {
"post": {
"operationId": "downloadArchive",
"parameters": [
@@ -1155,6 +1078,57 @@
]
}
},
+ "/asset/download/info": {
+ "post": {
+ "operationId": "getDownloadInfo",
+ "parameters": [
+ {
+ "name": "key",
+ "required": false,
+ "in": "query",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DownloadInfoDto"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DownloadResponseDto"
+ }
+ }
+ },
+ "description": ""
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ },
+ {
+ "cookie": []
+ },
+ {
+ "api_key": []
+ }
+ ],
+ "tags": [
+ "Asset"
+ ]
+ }
+ },
"/asset/download/{id}": {
"post": {
"operationId": "downloadFile",
@@ -5549,6 +5523,29 @@
],
"type": "object"
},
+ "DownloadInfoDto": {
+ "properties": {
+ "albumId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "archiveSize": {
+ "type": "integer"
+ },
+ "assetIds": {
+ "items": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "userId": {
+ "format": "uuid",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
"DownloadResponseDto": {
"properties": {
"archives": {
diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts
index fdd6df6efd..ac655b8077 100644
--- a/server/src/domain/asset/asset.service.ts
+++ b/server/src/domain/asset/asset.service.ts
@@ -13,7 +13,7 @@ import { IAssetRepository } from './asset.repository';
import {
AssetIdsDto,
DownloadArchiveInfo,
- DownloadDto,
+ DownloadInfoDto,
DownloadResponseDto,
MemoryLaneDto,
TimeBucketAssetDto,
@@ -176,7 +176,7 @@ export class AssetService {
return this.storageRepository.createReadStream(asset.originalPath, mimeTypes.lookup(asset.originalPath));
}
- async getDownloadInfo(authUser: AuthUserDto, dto: DownloadDto): Promise {
+ async getDownloadInfo(authUser: AuthUserDto, dto: DownloadInfoDto): Promise {
const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4;
const archives: DownloadArchiveInfo[] = [];
let archive: DownloadArchiveInfo = { size: 0, assetIds: [] };
@@ -234,7 +234,7 @@ export class AssetService {
return { stream: zip.stream };
}
- private async getDownloadAssets(authUser: AuthUserDto, dto: DownloadDto): Promise> {
+ private async getDownloadAssets(authUser: AuthUserDto, dto: DownloadInfoDto): Promise> {
const PAGINATION_SIZE = 2500;
if (dto.assetIds) {
diff --git a/server/src/domain/asset/dto/download.dto.ts b/server/src/domain/asset/dto/download.dto.ts
index c2cf85685a..604a8ea5fb 100644
--- a/server/src/domain/asset/dto/download.dto.ts
+++ b/server/src/domain/asset/dto/download.dto.ts
@@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';
import { IsInt, IsOptional, IsPositive } from 'class-validator';
import { ValidateUUID } from '../../domain.util';
-export class DownloadDto {
+export class DownloadInfoDto {
@ValidateUUID({ each: true, optional: true })
assetIds?: string[];
@@ -15,6 +15,7 @@ export class DownloadDto {
@IsInt()
@IsPositive()
@IsOptional()
+ @ApiProperty({ type: 'integer' })
archiveSize?: number;
}
diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/immich/controllers/asset.controller.ts
index ba3de02cc5..b55cbb870d 100644
--- a/server/src/immich/controllers/asset.controller.ts
+++ b/server/src/immich/controllers/asset.controller.ts
@@ -5,7 +5,7 @@ import {
AssetStatsDto,
AssetStatsResponseDto,
AuthUserDto,
- DownloadDto,
+ DownloadInfoDto,
DownloadResponseDto,
MapMarkerResponseDto,
MemoryLaneDto,
@@ -39,13 +39,13 @@ export class AssetController {
}
@SharedLinkRoute()
- @Get('download')
- getDownloadInfo(@AuthUser() authUser: AuthUserDto, @Query() dto: DownloadDto): Promise {
+ @Post('download/info')
+ getDownloadInfo(@AuthUser() authUser: AuthUserDto, @Body() dto: DownloadInfoDto): Promise {
return this.service.getDownloadInfo(authUser, dto);
}
@SharedLinkRoute()
- @Post('download')
+ @Post('download/archive')
@HttpCode(HttpStatus.OK)
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
downloadArchive(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetIdsDto): Promise {
diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts
index 956f19ba7b..8ade1d5fde 100644
--- a/web/src/api/open-api/api.ts
+++ b/web/src/api/open-api/api.ts
@@ -1132,6 +1132,37 @@ export interface DownloadArchiveInfo {
*/
'size': number;
}
+/**
+ *
+ * @export
+ * @interface DownloadInfoDto
+ */
+export interface DownloadInfoDto {
+ /**
+ *
+ * @type {string}
+ * @memberof DownloadInfoDto
+ */
+ 'albumId'?: string;
+ /**
+ *
+ * @type {number}
+ * @memberof DownloadInfoDto
+ */
+ 'archiveSize'?: number;
+ /**
+ *
+ * @type {Array}
+ * @memberof DownloadInfoDto
+ */
+ 'assetIds'?: Array;
+ /**
+ *
+ * @type {string}
+ * @memberof DownloadInfoDto
+ */
+ 'userId'?: string;
+}
/**
*
* @export
@@ -4880,7 +4911,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
downloadArchive: async (assetIdsDto: AssetIdsDto, key?: string, options: AxiosRequestConfig = {}): Promise => {
// verify required parameter 'assetIdsDto' is not null or undefined
assertParamExists('downloadArchive', 'assetIdsDto', assetIdsDto)
- const localVarPath = `/asset/download`;
+ const localVarPath = `/asset/download/archive`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@@ -5379,16 +5410,15 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
},
/**
*
- * @param {Array} [assetIds]
- * @param {string} [albumId]
- * @param {string} [userId]
- * @param {number} [archiveSize]
+ * @param {DownloadInfoDto} downloadInfoDto
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
- getDownloadInfo: async (assetIds?: Array, albumId?: string, userId?: string, archiveSize?: number, key?: string, options: AxiosRequestConfig = {}): Promise => {
- const localVarPath = `/asset/download`;
+ getDownloadInfo: async (downloadInfoDto: DownloadInfoDto, key?: string, options: AxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'downloadInfoDto' is not null or undefined
+ assertParamExists('getDownloadInfo', 'downloadInfoDto', downloadInfoDto)
+ const localVarPath = `/asset/download/info`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@@ -5396,7 +5426,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
baseOptions = configuration.baseOptions;
}
- const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+ const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
@@ -5409,31 +5439,18 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
- if (assetIds) {
- localVarQueryParameter['assetIds'] = assetIds;
- }
-
- if (albumId !== undefined) {
- localVarQueryParameter['albumId'] = albumId;
- }
-
- if (userId !== undefined) {
- localVarQueryParameter['userId'] = userId;
- }
-
- if (archiveSize !== undefined) {
- localVarQueryParameter['archiveSize'] = archiveSize;
- }
-
if (key !== undefined) {
localVarQueryParameter['key'] = key;
}
+ localVarHeaderParameter['Content-Type'] = 'application/json';
+
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+ localVarRequestOptions.data = serializeDataIfNeeded(downloadInfoDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
@@ -6141,16 +6158,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
},
/**
*
- * @param {Array} [assetIds]
- * @param {string} [albumId]
- * @param {string} [userId]
- * @param {number} [archiveSize]
+ * @param {DownloadInfoDto} downloadInfoDto
* @param {string} [key]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
- async getDownloadInfo(assetIds?: Array, albumId?: string, userId?: string, archiveSize?: number, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
- const localVarAxiosArgs = await localVarAxiosParamCreator.getDownloadInfo(assetIds, albumId, userId, archiveSize, key, options);
+ async getDownloadInfo(downloadInfoDto: DownloadInfoDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.getDownloadInfo(downloadInfoDto, key, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@@ -6406,8 +6420,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
- getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest = {}, options?: AxiosRequestConfig): AxiosPromise {
- return localVarFp.getDownloadInfo(requestParameters.assetIds, requestParameters.albumId, requestParameters.userId, requestParameters.archiveSize, requestParameters.key, options).then((request) => request(axios, basePath));
+ getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest, options?: AxiosRequestConfig): AxiosPromise {
+ return localVarFp.getDownloadInfo(requestParameters.downloadInfoDto, requestParameters.key, options).then((request) => request(axios, basePath));
},
/**
*
@@ -6788,31 +6802,10 @@ export interface AssetApiGetByTimeBucketRequest {
export interface AssetApiGetDownloadInfoRequest {
/**
*
- * @type {Array}
+ * @type {DownloadInfoDto}
* @memberof AssetApiGetDownloadInfo
*/
- readonly assetIds?: Array
-
- /**
- *
- * @type {string}
- * @memberof AssetApiGetDownloadInfo
- */
- readonly albumId?: string
-
- /**
- *
- * @type {string}
- * @memberof AssetApiGetDownloadInfo
- */
- readonly userId?: string
-
- /**
- *
- * @type {number}
- * @memberof AssetApiGetDownloadInfo
- */
- readonly archiveSize?: number
+ readonly downloadInfoDto: DownloadInfoDto
/**
*
@@ -7281,8 +7274,8 @@ export class AssetApi extends BaseAPI {
* @throws {RequiredError}
* @memberof AssetApi
*/
- public getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest = {}, options?: AxiosRequestConfig) {
- return AssetApiFp(this.configuration).getDownloadInfo(requestParameters.assetIds, requestParameters.albumId, requestParameters.userId, requestParameters.archiveSize, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
+ public getDownloadInfo(requestParameters: AssetApiGetDownloadInfoRequest, options?: AxiosRequestConfig) {
+ return AssetApiFp(this.configuration).getDownloadInfo(requestParameters.downloadInfoDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
}
/**
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index 0a8ff758d2..cb29b503a0 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -1,6 +1,6 @@
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
import { downloadManager } from '$lib/stores/download';
-import { api, AssetApiGetDownloadInfoRequest, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto } from '@api';
+import { api, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto, DownloadInfoDto } from '@api';
import { handleError } from './handle-error';
export const addAssetsToAlbum = async (
@@ -32,15 +32,11 @@ const downloadBlob = (data: Blob, filename: string) => {
URL.revokeObjectURL(url);
};
-export const downloadArchive = async (
- fileName: string,
- options: Omit,
- key?: string,
-) => {
+export const downloadArchive = async (fileName: string, options: DownloadInfoDto, key?: string) => {
let downloadInfo: DownloadResponseDto | null = null;
try {
- const { data } = await api.assetApi.getDownloadInfo({ ...options, key });
+ const { data } = await api.assetApi.getDownloadInfo({ downloadInfoDto: options, key });
downloadInfo = data;
} catch (error) {
handleError(error, 'Unable to download files');
From 74da15e20d353e9c0cc0221a791962454f6abf90 Mon Sep 17 00:00:00 2001
From: Sergey Kondrikov
Date: Tue, 15 Aug 2023 19:02:38 +0300
Subject: [PATCH 07/14] fix(web,server): disable partner's archive access
(#3695)
---
server/src/domain/access/access.core.ts | 5 +++++
server/src/domain/asset/asset.service.ts | 3 +++
web/src/routes/(user)/partners/[userId]/+page.svelte | 2 +-
3 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts
index d815612365..3e3d4a469d 100644
--- a/server/src/domain/access/access.core.ts
+++ b/server/src/domain/access/access.core.ts
@@ -19,6 +19,8 @@ export enum Permission {
ALBUM_SHARE = 'album.share',
ALBUM_DOWNLOAD = 'album.download',
+ ARCHIVE_READ = 'archive.read',
+
LIBRARY_READ = 'library.read',
LIBRARY_DOWNLOAD = 'library.download',
}
@@ -156,6 +158,9 @@ export class AccessCore {
case Permission.ALBUM_REMOVE_ASSET:
return this.repository.album.hasOwnerAccess(authUser.id, id);
+ case Permission.ARCHIVE_READ:
+ return authUser.id === id;
+
case Permission.LIBRARY_READ:
return authUser.id === id || (await this.repository.library.hasPartnerAccess(authUser.id, id));
diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts
index ac655b8077..2d4051b0fa 100644
--- a/server/src/domain/asset/asset.service.ts
+++ b/server/src/domain/asset/asset.service.ts
@@ -148,6 +148,9 @@ export class AssetService {
if (dto.albumId) {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, [dto.albumId]);
} else if (dto.userId) {
+ if (dto.isArchived !== false) {
+ await this.access.requirePermission(authUser, Permission.ARCHIVE_READ, [dto.userId]);
+ }
await this.access.requirePermission(authUser, Permission.LIBRARY_READ, [dto.userId]);
} else {
dto.userId = authUser.id;
diff --git a/web/src/routes/(user)/partners/[userId]/+page.svelte b/web/src/routes/(user)/partners/[userId]/+page.svelte
index c7fd51f2a8..adc718e918 100644
--- a/web/src/routes/(user)/partners/[userId]/+page.svelte
+++ b/web/src/routes/(user)/partners/[userId]/+page.svelte
@@ -18,7 +18,7 @@
export let data: PageData;
- const assetStore = new AssetStore({ size: TimeBucketSize.Month, userId: data.partner.id });
+ const assetStore = new AssetStore({ size: TimeBucketSize.Month, userId: data.partner.id, isArchived: false });
const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
From 35b4c9d3754b37d95685e1ac58dbe69974ec17f8 Mon Sep 17 00:00:00 2001
From: Vantao
Date: Wed, 16 Aug 2023 00:05:00 +0800
Subject: [PATCH 08/14] doc: update README_zh_CN.md (#3701)
---
README_zh_CN.md | 83 ++++++++++++++++++++++++-------------------------
1 file changed, 40 insertions(+), 43 deletions(-)
diff --git a/README_zh_CN.md b/README_zh_CN.md
index 10a946eee3..d018a4a285 100644
--- a/README_zh_CN.md
+++ b/README_zh_CN.md
@@ -13,7 +13,7 @@
Immich - 高性能的自托管照片和视频备份方案
-请注意: 此README不是由Immich团队维护, 这意味着它在某一时间点不会被更新,因为我们是依靠贡献者来更新的。感谢理解。
+请注意: 此 README 不是由 Immich 团队维护, 而是依靠贡献者来更新的,这意味着它可能并不会被及时更新。感谢理解。
@@ -31,29 +31,31 @@
## 免责声明
-- ⚠️ 本项目正在 **非常活跃** 的开发中。
-- ⚠️ 可能存在bug或者重大变更。
-- ⚠️ **不要把本软件作为你存储照片或视频的唯一方式!**
+- ⚠️ 本项目正在 **非常活跃** 地开发中。
+- ⚠️ 可能存在 bug 或者随时有重大变更。
+- ⚠️ **不要把本软件作为您存储照片或视频的唯一方式。**
+- ⚠️ 为了您宝贵的照片与视频,始终遵守 [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) 备份方案!
## 目录
-- [官方文档](https://immich.app/docs/overview/introduction)
+- [官方文档](https://immich.app/docs)
+- [路线图](https://github.com/orgs/immich-app/projects/1)
- [示例](#示例)
- [功能特性](#功能特性)
- [介绍](https://immich.app/docs/overview/introduction)
- [安装](https://immich.app/docs/install/requirements)
- [贡献指南](https://immich.app/docs/overview/support-the-project)
-- [支持本项目](#support-the-project)
-- [已知问题](#known-issues)
+- [支持本项目](#支持本项目)
## 官方文档
-你可以在 https://immich.app/ 找到包含安装手册的官方文档.
+您可以在 https://immich.app/ 找到官方文档(包含安装手册)。
+
## 示例
-你可以在 https://demo.immich.app 访问示例.
+您可以在 https://demo.immich.app 访问示例。
-在移动端, 你可以使用 `https://demo.immich.app/api`获取`服务终端链接`
+在移动端, 您可以使用 `https://demo.immich.app/api` 获取 `服务终端链接`
```bash title="示例认证信息"
认证信息
@@ -62,57 +64,52 @@
```
```
-规格: 甲骨文免费虚拟机套餐-阿姆斯特丹 4核 2.4Ghz ARM64 CPU, 24GB RAM。
+规格: 甲骨文免费虚拟机套餐——阿姆斯特丹 4核 2.4Ghz ARM64 CPU, 24GB RAM。
```
# 功能特性
| 功能特性 | 移动端 | 网页端 |
| ------------------------------------------- | ------- | --- |
-| 上传并查看照片和视频 | 是 | 是 |
-| 软件运行时自动备份 | 是 | N/A |
+| 上传并查看照片和视频 | 是 | 是 |
+| 软件运行时自动备份 | 是 | N/A |
| 选择需要备份的相册 | 是 | N/A |
-| 下载照片和视频到本地 | 是 | 是 |
+| 下载照片和视频到本地 | 是 | 是 |
| 多用户支持 | 是 | 是 |
| 相册 | 是 | 是 |
| 共享相册 | 是 | 是 |
| 可拖动的快速导航栏 | 是 | 是 |
| 支持RAW格式 (HEIC, HEIF, DNG, Apple ProRaw) | 是 | 是 |
-| 元数据视图 (EXIF, 地图) | 是 | 是 |
-| 通过元数据、对象和标签进行搜索 | 是 | No |
-| 管理功能 (用户管理) | N/A | 是 |
-| 后台备份 | Android | N/A |
+| 元数据视图(EXIF, 地图) | 是 | 是 |
+| 通过元数据、对象和标签进行搜索 | 是 | 是 |
+| 管理功能(用户管理) | 否 | 是 |
+| 后台备份 | 是 | N/A |
| 虚拟滚动 | 是 | 是 |
-| OAuth支持 | 是 | 是 |
-| 实时照片备份和查看 (仅iOS) | 是 | 是 |
+| OAuth 支持 | 是 | 是 |
+| API Keys|N/A|是|
+| 实况照片备份和查看 | 仅 iOS | 是 |
+|用户自定义存储结构|是|是|
+|公共分享|否|是|
+|归档与收藏功能|是|是|
+|全局地图|否|是|
+|好友分享|是|是|
+|人像识别与分组|是|是|
+|回忆(那年今日)|是|是|
+|离线支持|是|否|
+|只读相册|是|是|
# 支持本项目
-我已经致力于本项目并且将我会持续更新文档、新增功能和修复问题。但是我不能一个人走下去,所以我需要你给予我走下去的动力。
+我已经致力于本项目并且将我会持续更新文档、新增功能和修复问题。但是独木不成林,我需要您给予我坚持下去的动力。
-就像我主页里面 [selfhosted.show - In the episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) 说的一样,这是我和团队的一项艰巨的任务。我希望某一天我能够全职开发本项目,在此我希望你们能够助我梦想成真。
+就像我在 [selfhosted.show - In the episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) 节目里说的一样,这是我和团队的一项艰巨任务。并且我希望某一天我能够全职开发本项目,在此我请求您能够助我梦想成真。
-如果你使用了本项目一段时间,并且觉得上面的话有道理,那么请你按照如下方式帮助我吧。
+如果您使用了本项目一段时间,并且觉得上面的话有道理,那么请您考虑通过下列任一方式支持我吧。
## 捐赠
-- [按月捐赠](https://github.com/sponsors/alextran1502) via GitHub Sponsors
-- [一次捐赠](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via Github Sponsors
-
-# 已知问题
-
-## TensorFlow 构建问题
-
-_这是一个针对于Proxmox的已知问题_
-
-TensorFlow 不能运行在很旧的CPU架构上, 需要运行在AVX和AVX2指令集的CPU上。如果你在docker-compose的命令行中遇到了 `illegal instruction core dump`的错误, 通过如下命令检查你的CPU flag寄存器然后确保你能够看到`AVX`和`AVX2`的字样:
-
-```bash
-more /proc/cpuinfo | grep flags
-```
-
-如果你在Proxmox中运行虚拟机, 虚拟机中没有启用flag寄存器。
-
-你需要在虚拟机的硬件面板中把CPU类型从`kvm64`改为`host`。
-
-`Hardware > Processors > Edit > Advanced > Type (dropdown menu) > host`
+- 通过 GitHub Sponsors [按月捐赠](https://github.com/sponsors/alextran1502)
+- 通过 Github Sponsors [单次捐赠](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
+- [Librepay](https://liberapay.com/alex.tran1502/)
+- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
+- 比特币: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
From af1f00dff94a7f09ac76f28fec2f5c250aa6d558 Mon Sep 17 00:00:00 2001
From: Jason Rasmussen
Date: Tue, 15 Aug 2023 12:05:32 -0400
Subject: [PATCH 09/14] chore(server): cleanup (#3699)
---
server/src/domain/server-info/server-info.service.ts | 6 +++---
server/src/domain/user/user.core.ts | 1 -
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/domain/server-info/server-info.service.ts
index ceeb85e295..4a7e7f22bb 100644
--- a/server/src/domain/server-info/server-info.service.ts
+++ b/server/src/domain/server-info/server-info.service.ts
@@ -69,9 +69,9 @@ export class ServerInfoService {
getSupportedMediaTypes(): ServerMediaTypesResponseDto {
return {
- video: [...Object.keys(mimeTypes.video)],
- image: [...Object.keys(mimeTypes.image)],
- sidecar: [...Object.keys(mimeTypes.sidecar)],
+ video: Object.keys(mimeTypes.video),
+ image: Object.keys(mimeTypes.image),
+ sidecar: Object.keys(mimeTypes.sidecar),
};
}
}
diff --git a/server/src/domain/user/user.core.ts b/server/src/domain/user/user.core.ts
index 6c59d9ca71..e7c0f7f2b1 100644
--- a/server/src/domain/user/user.core.ts
+++ b/server/src/domain/user/user.core.ts
@@ -60,7 +60,6 @@ export class UserCore {
dto.externalPath = null;
}
- console.log(dto.memoriesEnabled);
return this.userRepository.update(id, dto);
} catch (e) {
Logger.error(e, 'Failed to update user info');
From 0abbd85134035b04ec3b980661e9b497d63394e0 Mon Sep 17 00:00:00 2001
From: Jason Rasmussen
Date: Tue, 15 Aug 2023 14:34:02 -0400
Subject: [PATCH 10/14] fix(web,server): album share performance (#3698)
---
server/src/domain/album/album.repository.ts | 6 ++-
server/src/domain/album/album.service.spec.ts | 7 +--
server/src/domain/album/album.service.ts | 24 +++++-----
.../infra/repositories/album.repository.ts | 44 ++++++++++---------
.../(user)/albums/[albumId]/+page.svelte | 6 +--
5 files changed, 47 insertions(+), 40 deletions(-)
diff --git a/server/src/domain/album/album.repository.ts b/server/src/domain/album/album.repository.ts
index 811b85ec9a..bc6fa37524 100644
--- a/server/src/domain/album/album.repository.ts
+++ b/server/src/domain/album/album.repository.ts
@@ -7,8 +7,12 @@ export interface AlbumAssetCount {
assetCount: number;
}
+export interface AlbumInfoOptions {
+ withAssets: boolean;
+}
+
export interface IAlbumRepository {
- getById(id: string): Promise;
+ getById(id: string, options: AlbumInfoOptions): Promise;
getByIds(ids: string[]): Promise;
getByAssetId(ownerId: string, assetId: string): Promise;
hasAsset(id: string, assetId: string): Promise;
diff --git a/server/src/domain/album/album.service.spec.ts b/server/src/domain/album/album.service.spec.ts
index 463d94d7f4..f9ab153744 100644
--- a/server/src/domain/album/album.service.spec.ts
+++ b/server/src/domain/album/album.service.spec.ts
@@ -364,6 +364,7 @@ describe(AlbumService.name, () => {
updatedAt: expect.any(Date),
sharedUsers: [],
});
+ expect(albumMock.getById).toHaveBeenCalledWith(albumStub.sharedWithUser.id, { withAssets: false });
});
it('should prevent removing a shared user from a not-owned album (shared with auth user)', async () => {
@@ -432,7 +433,7 @@ describe(AlbumService.name, () => {
await sut.get(authStub.admin, albumStub.oneAsset.id, {});
- expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id);
+ expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id, { withAssets: true });
expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, albumStub.oneAsset.id);
});
@@ -442,7 +443,7 @@ describe(AlbumService.name, () => {
await sut.get(authStub.adminSharedLink, 'album-123', {});
- expect(albumMock.getById).toHaveBeenCalledWith('album-123');
+ expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true });
expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLinkId,
'album-123',
@@ -455,7 +456,7 @@ describe(AlbumService.name, () => {
await sut.get(authStub.user1, 'album-123', {});
- expect(albumMock.getById).toHaveBeenCalledWith('album-123');
+ expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true });
expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalledWith(authStub.user1.id, 'album-123');
});
diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts
index 925b87ac9a..71c11d9357 100644
--- a/server/src/domain/album/album.service.ts
+++ b/server/src/domain/album/album.service.ts
@@ -12,7 +12,7 @@ import {
mapAlbumWithAssets,
mapAlbumWithoutAssets,
} from './album-response.dto';
-import { IAlbumRepository } from './album.repository';
+import { AlbumInfoOptions, IAlbumRepository } from './album.repository';
import { AddUsersDto, AlbumInfoDto, CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto } from './dto';
@Injectable()
@@ -84,7 +84,7 @@ export class AlbumService {
async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto) {
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
await this.albumRepository.updateThumbnails();
- return mapAlbum(await this.findOrFail(id), !dto.withoutAssets);
+ return mapAlbum(await this.findOrFail(id, { withAssets: true }), !dto.withoutAssets);
}
async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise {
@@ -111,7 +111,7 @@ export class AlbumService {
async update(authUser: AuthUserDto, id: string, dto: UpdateAlbumDto): Promise {
await this.access.requirePermission(authUser, Permission.ALBUM_UPDATE, id);
- const album = await this.findOrFail(id);
+ const album = await this.findOrFail(id, { withAssets: true });
if (dto.albumThumbnailAssetId) {
const valid = await this.albumRepository.hasAsset(id, dto.albumThumbnailAssetId);
@@ -129,13 +129,13 @@ export class AlbumService {
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [updatedAlbum.id] } });
- return mapAlbumWithAssets(updatedAlbum);
+ return mapAlbumWithoutAssets(updatedAlbum);
}
async delete(authUser: AuthUserDto, id: string): Promise {
await this.access.requirePermission(authUser, Permission.ALBUM_DELETE, id);
- const album = await this.albumRepository.getById(id);
+ const album = await this.findOrFail(id, { withAssets: false });
if (!album) {
throw new BadRequestException('Album not found');
}
@@ -145,7 +145,7 @@ export class AlbumService {
}
async addAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise {
- const album = await this.findOrFail(id);
+ const album = await this.findOrFail(id, { withAssets: true });
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
@@ -181,7 +181,7 @@ export class AlbumService {
}
async removeAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise {
- const album = await this.findOrFail(id);
+ const album = await this.findOrFail(id, { withAssets: true });
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
@@ -225,7 +225,7 @@ export class AlbumService {
async addUsers(authUser: AuthUserDto, id: string, dto: AddUsersDto): Promise {
await this.access.requirePermission(authUser, Permission.ALBUM_SHARE, id);
- const album = await this.findOrFail(id);
+ const album = await this.findOrFail(id, { withAssets: false });
for (const userId of dto.sharedUserIds) {
const exists = album.sharedUsers.find((user) => user.id === userId);
@@ -247,7 +247,7 @@ export class AlbumService {
updatedAt: new Date(),
sharedUsers: album.sharedUsers,
})
- .then(mapAlbumWithAssets);
+ .then(mapAlbumWithoutAssets);
}
async removeUser(authUser: AuthUserDto, id: string, userId: string | 'me'): Promise {
@@ -255,7 +255,7 @@ export class AlbumService {
userId = authUser.id;
}
- const album = await this.findOrFail(id);
+ const album = await this.findOrFail(id, { withAssets: false });
if (album.ownerId === userId) {
throw new BadRequestException('Cannot remove album owner');
@@ -278,8 +278,8 @@ export class AlbumService {
});
}
- private async findOrFail(id: string) {
- const album = await this.albumRepository.getById(id);
+ private async findOrFail(id: string, options: AlbumInfoOptions) {
+ const album = await this.albumRepository.getById(id, options);
if (!album) {
throw new BadRequestException('Album not found');
}
diff --git a/server/src/infra/repositories/album.repository.ts b/server/src/infra/repositories/album.repository.ts
index fcf8c54c1f..5e4ac06ca0 100644
--- a/server/src/infra/repositories/album.repository.ts
+++ b/server/src/infra/repositories/album.repository.ts
@@ -1,7 +1,7 @@
-import { AlbumAssetCount, IAlbumRepository } from '@app/domain';
+import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from '@app/domain';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
-import { In, IsNull, Not, Repository } from 'typeorm';
+import { FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
import { dataSource } from '../database.config';
import { AlbumEntity, AssetEntity } from '../entities';
@@ -12,25 +12,27 @@ export class AlbumRepository implements IAlbumRepository {
@InjectRepository(AlbumEntity) private repository: Repository,
) {}
- getById(id: string): Promise {
- return this.repository.findOne({
- where: {
- id,
- },
- relations: {
- owner: true,
- sharedUsers: true,
- assets: {
- exifInfo: true,
- },
- sharedLinks: true,
- },
- order: {
- assets: {
- fileCreatedAt: 'DESC',
- },
- },
- });
+ getById(id: string, options: AlbumInfoOptions): Promise {
+ const relations: FindOptionsRelations = {
+ owner: true,
+ sharedUsers: true,
+ assets: false,
+ sharedLinks: true,
+ };
+
+ const order: FindOptionsOrder = {};
+
+ if (options.withAssets) {
+ relations.assets = {
+ exifInfo: true,
+ };
+
+ order.assets = {
+ fileCreatedAt: 'DESC',
+ };
+ }
+
+ return this.repository.findOne({ where: { id }, relations, order });
}
getByIds(ids: string[]): Promise {
diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte
index fb169c3998..8922ab073b 100644
--- a/web/src/routes/(user)/albums/[albumId]/+page.svelte
+++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte
@@ -100,7 +100,7 @@
});
const refreshAlbum = async () => {
- const { data } = await api.albumApi.getAlbumInfo({ id: album.id, withoutAssets: false });
+ const { data } = await api.albumApi.getAlbumInfo({ id: album.id, withoutAssets: true });
album = data;
};
@@ -261,9 +261,9 @@
}
};
- const handleUpdateDescription = (description: string) => {
+ const handleUpdateDescription = async (description: string) => {
try {
- api.albumApi.updateAlbumInfo({
+ await api.albumApi.updateAlbumInfo({
id: album.id,
updateAlbumDto: {
description,
From c27c12d975e94cf21cf5fcc40e0eac63bda6a36b Mon Sep 17 00:00:00 2001
From: martin <74269598+martabal@users.noreply.github.com>
Date: Wed, 16 Aug 2023 02:06:49 +0200
Subject: [PATCH 11/14] fix(server): people sorting (#3713)
---
server/src/domain/person/person.repository.ts | 1 +
server/src/domain/person/person.service.spec.ts | 6 +++---
server/src/domain/person/person.service.ts | 6 ++++--
server/src/infra/repositories/person.repository.ts | 14 ++++++++++----
4 files changed, 18 insertions(+), 9 deletions(-)
diff --git a/server/src/domain/person/person.repository.ts b/server/src/domain/person/person.repository.ts
index 3c8432be1e..973d940cd6 100644
--- a/server/src/domain/person/person.repository.ts
+++ b/server/src/domain/person/person.repository.ts
@@ -4,6 +4,7 @@ export const IPersonRepository = 'IPersonRepository';
export interface PersonSearchOptions {
minimumFaceCount: number;
+ withHidden: boolean;
}
export interface UpdateFacesData {
diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts
index 7aaa875f27..e5bca7c830 100644
--- a/server/src/domain/person/person.service.spec.ts
+++ b/server/src/domain/person/person.service.spec.ts
@@ -47,7 +47,7 @@ describe(PersonService.name, () => {
visible: 1,
people: [responseDto],
});
- expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1 });
+ expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1, withHidden: false });
});
it('should get all visible people with thumbnails', async () => {
personMock.getAll.mockResolvedValue([personStub.withName, personStub.hidden]);
@@ -56,7 +56,7 @@ describe(PersonService.name, () => {
visible: 1,
people: [responseDto],
});
- expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1 });
+ expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1, withHidden: false });
});
it('should get all hidden and visible people with thumbnails', async () => {
personMock.getAll.mockResolvedValue([personStub.withName, personStub.hidden]);
@@ -73,7 +73,7 @@ describe(PersonService.name, () => {
},
],
});
- expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1 });
+ expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1, withHidden: true });
});
});
diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts
index 0f66447f13..187ef3358d 100644
--- a/server/src/domain/person/person.service.ts
+++ b/server/src/domain/person/person.service.ts
@@ -26,8 +26,10 @@ export class PersonService {
) {}
async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise {
- const people = await this.repository.getAll(authUser.id, { minimumFaceCount: 1 });
-
+ const people = await this.repository.getAll(authUser.id, {
+ minimumFaceCount: 1,
+ withHidden: dto.withHidden || false,
+ });
const persons: PersonResponseDto[] = people
// with thumbnails
.filter((person) => !!person.thumbnailPath)
diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/infra/repositories/person.repository.ts
index 8595484365..1203bba381 100644
--- a/server/src/infra/repositories/person.repository.ts
+++ b/server/src/infra/repositories/person.repository.ts
@@ -51,16 +51,22 @@ export class PersonRepository implements IPersonRepository {
}
getAll(userId: string, options?: PersonSearchOptions): Promise {
- return this.personRepository
+ const queryBuilder = this.personRepository
.createQueryBuilder('person')
.leftJoin('person.faces', 'face')
.where('person.ownerId = :userId', { userId })
- .orderBy('COUNT(face.assetId)', 'DESC')
+ .orderBy('person.isHidden', 'ASC')
+ .addOrderBy("NULLIF(person.name, '') IS NULL", 'ASC')
+ .addOrderBy('COUNT(face.assetId)', 'DESC')
.addOrderBy("NULLIF(person.name, '')", 'ASC', 'NULLS LAST')
.having('COUNT(face.assetId) >= :faces', { faces: options?.minimumFaceCount || 1 })
.groupBy('person.id')
- .limit(500)
- .getMany();
+ .limit(500);
+ if (!options?.withHidden) {
+ queryBuilder.andWhere('person.isHidden = false');
+ }
+
+ return queryBuilder.getMany();
}
getAllWithoutFaces(): Promise {
From 4762fd83d414cc239124a9d2902c18be99a1d2f5 Mon Sep 17 00:00:00 2001
From: Jason Rasmussen
Date: Tue, 15 Aug 2023 21:34:57 -0400
Subject: [PATCH 12/14] fix(server): link live photos after metadata extraction
finishes (#3702)
* fix(server): link live photos after metadata extraction finishes
* chore: fix test
---------
Co-authored-by: Alex
---
server/src/domain/album/album.repository.ts | 2 +
server/src/domain/job/job.constants.ts | 2 +
server/src/domain/job/job.repository.ts | 1 +
server/src/domain/job/job.service.spec.ts | 4 ++
server/src/domain/job/job.service.ts | 6 +-
.../infra/repositories/album.repository.ts | 17 +++++-
server/src/microservices/app.service.ts | 1 +
.../metadata-extraction.processor.ts | 60 +++++++++++--------
.../repositories/album.repository.mock.ts | 1 +
9 files changed, 64 insertions(+), 30 deletions(-)
diff --git a/server/src/domain/album/album.repository.ts b/server/src/domain/album/album.repository.ts
index bc6fa37524..d501964ef5 100644
--- a/server/src/domain/album/album.repository.ts
+++ b/server/src/domain/album/album.repository.ts
@@ -16,6 +16,8 @@ export interface IAlbumRepository {
getByIds(ids: string[]): Promise;
getByAssetId(ownerId: string, assetId: string): Promise;
hasAsset(id: string, assetId: string): Promise;
+ /** Remove an asset from _all_ albums */
+ removeAsset(id: string): Promise;
getAssetCountForIds(ids: string[]): Promise;
getInvalidThumbnail(): Promise;
getOwned(ownerId: string): Promise;
diff --git a/server/src/domain/job/job.constants.ts b/server/src/domain/job/job.constants.ts
index a02248b306..02fa588c9b 100644
--- a/server/src/domain/job/job.constants.ts
+++ b/server/src/domain/job/job.constants.ts
@@ -32,6 +32,7 @@ export enum JobName {
// metadata
QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction',
METADATA_EXTRACTION = 'metadata-extraction',
+ LINK_LIVE_PHOTOS = 'link-live-photos',
// user deletion
USER_DELETION = 'user-deletion',
@@ -98,6 +99,7 @@ export const JOBS_TO_QUEUE: Record = {
// metadata
[JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
[JobName.METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
+ [JobName.LINK_LIVE_PHOTOS]: QueueName.METADATA_EXTRACTION,
// storage template
[JobName.STORAGE_TEMPLATE_MIGRATION]: QueueName.STORAGE_TEMPLATE_MIGRATION,
diff --git a/server/src/domain/job/job.repository.ts b/server/src/domain/job/job.repository.ts
index c088a2eedf..f605bef4b4 100644
--- a/server/src/domain/job/job.repository.ts
+++ b/server/src/domain/job/job.repository.ts
@@ -45,6 +45,7 @@ export type JobItem =
// Metadata Extraction
| { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
| { name: JobName.METADATA_EXTRACTION; data: IEntityJob }
+ | { name: JobName.LINK_LIVE_PHOTOS; data: IEntityJob }
// Sidecar Scanning
| { name: JobName.QUEUE_SIDECAR; data: IBaseJob }
diff --git a/server/src/domain/job/job.service.spec.ts b/server/src/domain/job/job.service.spec.ts
index a8c6ec9dc6..503440a5cf 100644
--- a/server/src/domain/job/job.service.spec.ts
+++ b/server/src/domain/job/job.service.spec.ts
@@ -252,6 +252,10 @@ describe(JobService.name, () => {
},
{
item: { name: JobName.METADATA_EXTRACTION, data: { id: 'asset-1' } },
+ jobs: [JobName.LINK_LIVE_PHOTOS],
+ },
+ {
+ item: { name: JobName.LINK_LIVE_PHOTOS, data: { id: 'asset-1' } },
jobs: [JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, JobName.SEARCH_INDEX_ASSET],
},
{
diff --git a/server/src/domain/job/job.service.ts b/server/src/domain/job/job.service.ts
index df79491367..1c82908911 100644
--- a/server/src/domain/job/job.service.ts
+++ b/server/src/domain/job/job.service.ts
@@ -149,6 +149,10 @@ export class JobService {
break;
case JobName.METADATA_EXTRACTION:
+ await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data });
+ break;
+
+ case JobName.LINK_LIVE_PHOTOS:
await this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: item.data });
break;
@@ -186,7 +190,7 @@ export class JobService {
case JobName.CLASSIFY_IMAGE:
case JobName.ENCODE_CLIP:
case JobName.RECOGNIZE_FACES:
- case JobName.METADATA_EXTRACTION:
+ case JobName.LINK_LIVE_PHOTOS:
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids: [item.data.id] } });
break;
}
diff --git a/server/src/infra/repositories/album.repository.ts b/server/src/infra/repositories/album.repository.ts
index 5e4ac06ca0..c59589710b 100644
--- a/server/src/infra/repositories/album.repository.ts
+++ b/server/src/infra/repositories/album.repository.ts
@@ -1,7 +1,7 @@
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from '@app/domain';
import { Injectable } from '@nestjs/common';
-import { InjectRepository } from '@nestjs/typeorm';
-import { FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
+import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
+import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
import { dataSource } from '../database.config';
import { AlbumEntity, AssetEntity } from '../entities';
@@ -10,6 +10,7 @@ export class AlbumRepository implements IAlbumRepository {
constructor(
@InjectRepository(AssetEntity) private assetRepository: Repository,
@InjectRepository(AlbumEntity) private repository: Repository,
+ @InjectDataSource() private dataSource: DataSource,
) {}
getById(id: string, options: AlbumInfoOptions): Promise {
@@ -84,7 +85,7 @@ export class AlbumRepository implements IAlbumRepository {
*/
async getInvalidThumbnail(): Promise {
// Using dataSource, because there is no direct access to albums_assets_assets.
- const albumHasAssets = dataSource
+ const albumHasAssets = this.dataSource
.createQueryBuilder()
.select('1')
.from('albums_assets_assets', 'albums_assets')
@@ -150,6 +151,16 @@ export class AlbumRepository implements IAlbumRepository {
});
}
+ async removeAsset(assetId: string): Promise {
+ // Using dataSource, because there is no direct access to albums_assets_assets.
+ await this.dataSource
+ .createQueryBuilder()
+ .delete()
+ .from('albums_assets_assets')
+ .where('"albums_assets_assets"."assetsId" = :assetId', { assetId })
+ .execute();
+ }
+
hasAsset(id: string, assetId: string): Promise {
return this.repository.exist({
where: {
diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts
index a8f30e1888..1204a6ebdd 100644
--- a/server/src/microservices/app.service.ts
+++ b/server/src/microservices/app.service.ts
@@ -66,6 +66,7 @@ export class AppService {
[JobName.VIDEO_CONVERSION]: (data) => this.mediaService.handleVideoConversion(data),
[JobName.QUEUE_METADATA_EXTRACTION]: (data) => this.metadataProcessor.handleQueueMetadataExtraction(data),
[JobName.METADATA_EXTRACTION]: (data) => this.metadataProcessor.handleMetadataExtraction(data),
+ [JobName.LINK_LIVE_PHOTOS]: (data) => this.metadataProcessor.handleLivePhotoLinking(data),
[JobName.QUEUE_RECOGNIZE_FACES]: (data) => this.facialRecognitionService.handleQueueRecognizeFaces(data),
[JobName.RECOGNIZE_FACES]: (data) => this.facialRecognitionService.handleRecognizeFaces(data),
[JobName.GENERATE_FACE_THUMBNAIL]: (data) => this.facialRecognitionService.handleGenerateFaceThumbnail(data),
diff --git a/server/src/microservices/processors/metadata-extraction.processor.ts b/server/src/microservices/processors/metadata-extraction.processor.ts
index a5f3ed4dd3..7c58f7102e 100644
--- a/server/src/microservices/processors/metadata-extraction.processor.ts
+++ b/server/src/microservices/processors/metadata-extraction.processor.ts
@@ -1,4 +1,5 @@
import {
+ IAlbumRepository,
IAssetRepository,
IBaseJob,
ICryptoRepository,
@@ -59,6 +60,7 @@ export class MetadataExtractionProcessor {
constructor(
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
+ @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IGeocodingRepository) private geocodingRepository: IGeocodingRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@@ -92,6 +94,38 @@ export class MetadataExtractionProcessor {
}
}
+ async handleLivePhotoLinking(job: IEntityJob) {
+ const { id } = job;
+ const [asset] = await this.assetRepository.getByIds([id]);
+ if (!asset?.exifInfo) {
+ return false;
+ }
+
+ if (!asset.exifInfo.livePhotoCID) {
+ return true;
+ }
+
+ const otherType = asset.type === AssetType.VIDEO ? AssetType.IMAGE : AssetType.VIDEO;
+ const match = await this.assetRepository.findLivePhotoMatch({
+ livePhotoCID: asset.exifInfo.livePhotoCID,
+ ownerId: asset.ownerId,
+ otherAssetId: asset.id,
+ type: otherType,
+ });
+
+ if (!match) {
+ return true;
+ }
+
+ const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset];
+
+ await this.assetRepository.save({ id: photoAsset.id, livePhotoVideoId: motionAsset.id });
+ await this.assetRepository.save({ id: motionAsset.id, isVisible: false });
+ await this.albumRepository.removeAsset(motionAsset.id);
+
+ return true;
+ }
+
async handleQueueMetadataExtraction(job: IBaseJob) {
const { force } = job;
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
@@ -351,19 +385,6 @@ export class MetadataExtractionProcessor {
}
newExif.livePhotoCID = getExifProperty('MediaGroupUUID');
- if (newExif.livePhotoCID && !asset.livePhotoVideoId) {
- const motionAsset = await this.assetRepository.findLivePhotoMatch({
- livePhotoCID: newExif.livePhotoCID,
- otherAssetId: asset.id,
- ownerId: asset.ownerId,
- type: AssetType.VIDEO,
- });
- if (motionAsset) {
- await this.assetRepository.save({ id: asset.id, livePhotoVideoId: motionAsset.id });
- await this.assetRepository.save({ id: motionAsset.id, isVisible: false });
- }
- }
-
await this.applyReverseGeocoding(asset, newExif);
/**
@@ -428,19 +449,6 @@ export class MetadataExtractionProcessor {
newExif.fps = null;
newExif.livePhotoCID = exifData?.ContentIdentifier || null;
- if (newExif.livePhotoCID) {
- const photoAsset = await this.assetRepository.findLivePhotoMatch({
- livePhotoCID: newExif.livePhotoCID,
- ownerId: asset.ownerId,
- otherAssetId: asset.id,
- type: AssetType.IMAGE,
- });
- if (photoAsset) {
- await this.assetRepository.save({ id: photoAsset.id, livePhotoVideoId: asset.id });
- await this.assetRepository.save({ id: asset.id, isVisible: false });
- }
- }
-
if (videoTags && videoTags['location']) {
const location = videoTags['location'] as string;
const locationRegex = /([+-][0-9]+\.[0-9]+)([+-][0-9]+\.[0-9]+)\/$/;
diff --git a/server/test/repositories/album.repository.mock.ts b/server/test/repositories/album.repository.mock.ts
index 8656fa64a1..3d42630dfb 100644
--- a/server/test/repositories/album.repository.mock.ts
+++ b/server/test/repositories/album.repository.mock.ts
@@ -12,6 +12,7 @@ export const newAlbumRepositoryMock = (): jest.Mocked => {
getNotShared: jest.fn(),
deleteAll: jest.fn(),
getAll: jest.fn(),
+ removeAsset: jest.fn(),
hasAsset: jest.fn(),
create: jest.fn(),
update: jest.fn(),
From bc66b1a556bf51a496397a4a618fb415e2183985 Mon Sep 17 00:00:00 2001
From: martin <74269598+martabal@users.noreply.github.com>
Date: Wed, 16 Aug 2023 03:46:23 +0200
Subject: [PATCH 13/14] fix(web): user-management layout (#3704)
* fix: user-management layout
* better user form scrollbar
---------
Co-authored-by: Alex Tran
---
.../components/forms/edit-user-form.svelte | 2 +-
web/src/routes/admin/+layout.svelte | 4 +--
.../routes/admin/user-management/+page.svelte | 36 +++++++++----------
3 files changed, 19 insertions(+), 23 deletions(-)
diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte
index 760c5f48ef..5881488508 100644
--- a/web/src/lib/components/forms/edit-user-form.svelte
+++ b/web/src/lib/components/forms/edit-user-form.svelte
@@ -67,7 +67,7 @@
-
-
+
diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte
index 5dbe71aca3..586d5a3042 100644
--- a/web/src/routes/admin/user-management/+page.svelte
+++ b/web/src/routes/admin/user-management/+page.svelte
@@ -168,11 +168,11 @@
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
>
- | Email |
- First name |
- Last name |
- Can import |
- Action |
+ Email |
+ First name |
+ Last name |
+ Can import |
+ Action |
@@ -187,10 +187,10 @@
: 'bg-immich-bg dark:bg-immich-dark-gray/50'
}`}
>
- {user.email} |
- {user.firstName} |
- {user.lastName} |
-
+ | {user.email} |
+ {user.firstName} |
+ {user.lastName} |
+
{#if user.externalPath}
@@ -199,7 +199,7 @@
{/if}
|
-
+ |
{#if !isDeleted(user)}
|