Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Tran 7277ea3d7a feat(server): asset_user table 2025-01-28 22:04:21 -06:00
39 changed files with 139 additions and 165 deletions
+3 -3
View File
@@ -1,12 +1,12 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.48", "version": "2.2.47",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.48", "version": "2.2.47",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
@@ -52,7 +52,7 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.125.7", "version": "1.125.6",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.48", "version": "2.2.47",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "type": "module",
"exports": "./dist/index.js", "exports": "./dist/index.js",
-1
View File
@@ -48,7 +48,6 @@ services:
vaapi-wsl: # use this for VAAPI if you're running Immich in WSL2 vaapi-wsl: # use this for VAAPI if you're running Immich in WSL2
devices: devices:
- /dev/dri:/dev/dri - /dev/dri:/dev/dri
- /dev/dxg:/dev/dxg
volumes: volumes:
- /usr/lib/wsl:/usr/lib/wsl - /usr/lib/wsl:/usr/lib/wsl
environment: environment:
+16 -17
View File
@@ -8,23 +8,22 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
## Image formats ## Image formats
| Format | Extension(s) | Supported? | Notes | | Format | Extension(s) | Supported? | Notes |
| :---------- | :---------------------------- | :----------------: | :-------------- | | :-------- | :---------------------------- | :----------------: | :-------------- |
| `AVIF` | `.avif` | :white_check_mark: | | | `AVIF` | `.avif` | :white_check_mark: | |
| `BMP` | `.bmp` | :white_check_mark: | | | `BMP` | `.bmp` | :white_check_mark: | |
| `GIF` | `.gif` | :white_check_mark: | | | `GIF` | `.gif` | :white_check_mark: | |
| `HEIC` | `.heic` | :white_check_mark: | | | `HEIC` | `.heic` | :white_check_mark: | |
| `HEIF` | `.heif` | :white_check_mark: | | | `HEIF` | `.heif` | :white_check_mark: | |
| `JPEG 2000` | `.jp2` | :white_check_mark: | | | `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | | | `JPEG XL` | `.jxl` | :white_check_mark: | |
| `JPEG XL` | `.jxl` | :white_check_mark: | | | `PNG` | `.webp` | :white_check_mark: | |
| `PNG` | `.webp` | :white_check_mark: | | | `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop | | `RAW` | `.raw` | :white_check_mark: | |
| `RAW` | `.raw` | :white_check_mark: | | | `RW2` | `.rw2` | :white_check_mark: | |
| `RW2` | `.rw2` | :white_check_mark: | | | `SVG` | `.svg` | :white_check_mark: | |
| `SVG` | `.svg` | :white_check_mark: | | | `TIFF` | `.tif` `.tiff` | :white_check_mark: | |
| `TIFF` | `.tif` `.tiff` | :white_check_mark: | | | `WEBP` | `.webp` | :white_check_mark: | |
| `WEBP` | `.webp` | :white_check_mark: | |
## Video formats ## Video formats
-4
View File
@@ -1,8 +1,4 @@
[ [
{
"label": "v1.125.7",
"url": "https://v1.125.7.archive.immich.app"
},
{ {
"label": "v1.125.6", "label": "v1.125.6",
"url": "https://v1.125.6.archive.immich.app" "url": "https://v1.125.6.archive.immich.app"
+4 -4
View File
@@ -1,12 +1,12 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.125.7", "version": "1.125.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.125.7", "version": "1.125.6",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
@@ -45,7 +45,7 @@
}, },
"../cli": { "../cli": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.48", "version": "2.2.47",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
@@ -92,7 +92,7 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.125.7", "version": "1.125.6",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.125.7", "version": "1.125.6",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
-19
View File
@@ -356,24 +356,5 @@ describe('/admin/users', () => {
expect(status).toBe(403); expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden); expect(body).toEqual(errorDto.forbidden);
}); });
it('should restore a user', async () => {
const user = await utils.userSetup(admin.accessToken, createUserDto.create('restore'));
await deleteUserAdmin({ id: user.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.post(`/admin/users/${user.userId}/restore`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
id: user.userId,
email: user.userEmail,
status: 'active',
deletedAt: null,
}),
);
});
}); });
}); });
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "machine-learning" name = "machine-learning"
version = "1.125.7" version = "1.125.6"
description = "" description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"] authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md" readme = "README.md"
+1 -1
View File
@@ -36,7 +36,7 @@ platform :android do
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 182, "android.injected.version.code" => 182,
"android.injected.version.name" => "1.125.7", "android.injected.version.name" => "1.125.6",
} }
) )
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') 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')
+1 -1
View File
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Release" desc "iOS Release"
lane :release do lane :release do
increment_version_number( increment_version_number(
version_number: "1.125.7" version_number: "1.125.6"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,
+5 -5
View File
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.125.7 - API version: 1.125.6
- Generator version: 7.8.0 - Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen - Build package: org.openapitools.codegen.languages.DartClientCodegen
@@ -93,17 +93,17 @@ Class | Method | HTTP request | Description
*AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} | *AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} |
*AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} | *AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} |
*AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} | *AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} |
*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | checkBulkUpload *AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Checks if assets exist by checksums
*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | checkExistingAssets *AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | Checks if multiple assets exist on the server and returns all existing - used by background backup
*AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets | *AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets |
*AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original |
*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | getAllUserAssetsByDeviceId *AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Get all asset of a device that are in the database, ID only.
*AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} |
*AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | *AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics |
*AssetsApi* | [**getMemoryLane**](doc//AssetsApi.md#getmemorylane) | **GET** /assets/memory-lane | *AssetsApi* | [**getMemoryLane**](doc//AssetsApi.md#getmemorylane) | **GET** /assets/memory-lane |
*AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | *AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random |
*AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback |
*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | replaceAsset *AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace the asset with new file, without changing its id
*AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs |
*AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} |
*AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets | *AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets |
-16
View File
@@ -16,8 +16,6 @@ class AssetsApi {
final ApiClient apiClient; final ApiClient apiClient;
/// checkBulkUpload
///
/// Checks if assets exist by checksums /// Checks if assets exist by checksums
/// ///
/// Note: This method returns the HTTP [Response]. /// Note: This method returns the HTTP [Response].
@@ -50,8 +48,6 @@ class AssetsApi {
); );
} }
/// checkBulkUpload
///
/// Checks if assets exist by checksums /// Checks if assets exist by checksums
/// ///
/// Parameters: /// Parameters:
@@ -72,8 +68,6 @@ class AssetsApi {
return null; return null;
} }
/// checkExistingAssets
///
/// Checks if multiple assets exist on the server and returns all existing - used by background backup /// Checks if multiple assets exist on the server and returns all existing - used by background backup
/// ///
/// Note: This method returns the HTTP [Response]. /// Note: This method returns the HTTP [Response].
@@ -106,8 +100,6 @@ class AssetsApi {
); );
} }
/// checkExistingAssets
///
/// Checks if multiple assets exist on the server and returns all existing - used by background backup /// Checks if multiple assets exist on the server and returns all existing - used by background backup
/// ///
/// Parameters: /// Parameters:
@@ -223,8 +215,6 @@ class AssetsApi {
return null; return null;
} }
/// getAllUserAssetsByDeviceId
///
/// Get all asset of a device that are in the database, ID only. /// Get all asset of a device that are in the database, ID only.
/// ///
/// Note: This method returns the HTTP [Response]. /// Note: This method returns the HTTP [Response].
@@ -258,8 +248,6 @@ class AssetsApi {
); );
} }
/// getAllUserAssetsByDeviceId
///
/// Get all asset of a device that are in the database, ID only. /// Get all asset of a device that are in the database, ID only.
/// ///
/// Parameters: /// Parameters:
@@ -576,8 +564,6 @@ class AssetsApi {
return null; return null;
} }
/// replaceAsset
///
/// Replace the asset with new file, without changing its id /// Replace the asset with new file, without changing its id
/// ///
/// Note: This method returns the HTTP [Response]. /// Note: This method returns the HTTP [Response].
@@ -659,8 +645,6 @@ class AssetsApi {
); );
} }
/// replaceAsset
///
/// Replace the asset with new file, without changing its id /// Replace the asset with new file, without changing its id
/// ///
/// Parameters: /// Parameters:
+1 -1
View File
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none' publish_to: 'none'
version: 1.125.7+182 version: 1.125.6+182
environment: environment:
sdk: '>=3.3.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'
+6 -10
View File
@@ -539,7 +539,7 @@
} }
], ],
"responses": { "responses": {
"200": { "201": {
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -1424,7 +1424,6 @@
}, },
"/assets/bulk-upload-check": { "/assets/bulk-upload-check": {
"post": { "post": {
"description": "Checks if assets exist by checksums",
"operationId": "checkBulkUpload", "operationId": "checkBulkUpload",
"parameters": [], "parameters": [],
"requestBody": { "requestBody": {
@@ -1460,7 +1459,7 @@
"api_key": [] "api_key": []
} }
], ],
"summary": "checkBulkUpload", "summary": "Checks if assets exist by checksums",
"tags": [ "tags": [
"Assets" "Assets"
] ]
@@ -1468,7 +1467,6 @@
}, },
"/assets/device/{deviceId}": { "/assets/device/{deviceId}": {
"get": { "get": {
"description": "Get all asset of a device that are in the database, ID only.",
"operationId": "getAllUserAssetsByDeviceId", "operationId": "getAllUserAssetsByDeviceId",
"parameters": [ "parameters": [
{ {
@@ -1506,7 +1504,7 @@
"api_key": [] "api_key": []
} }
], ],
"summary": "getAllUserAssetsByDeviceId", "summary": "Get all asset of a device that are in the database, ID only.",
"tags": [ "tags": [
"Assets" "Assets"
] ]
@@ -1514,7 +1512,6 @@
}, },
"/assets/exist": { "/assets/exist": {
"post": { "post": {
"description": "Checks if multiple assets exist on the server and returns all existing - used by background backup",
"operationId": "checkExistingAssets", "operationId": "checkExistingAssets",
"parameters": [], "parameters": [],
"requestBody": { "requestBody": {
@@ -1550,7 +1547,7 @@
"api_key": [] "api_key": []
} }
], ],
"summary": "checkExistingAssets", "summary": "Checks if multiple assets exist on the server and returns all existing - used by background backup",
"tags": [ "tags": [
"Assets" "Assets"
] ]
@@ -1906,7 +1903,6 @@
] ]
}, },
"put": { "put": {
"description": "Replace the asset with new file, without changing its id",
"operationId": "replaceAsset", "operationId": "replaceAsset",
"parameters": [ "parameters": [
{ {
@@ -1960,7 +1956,7 @@
"api_key": [] "api_key": []
} }
], ],
"summary": "replaceAsset", "summary": "Replace the asset with new file, without changing its id",
"tags": [ "tags": [
"Assets" "Assets"
], ],
@@ -7458,7 +7454,7 @@
"info": { "info": {
"title": "Immich", "title": "Immich",
"description": "Immich API", "description": "Immich API",
"version": "1.125.7", "version": "1.125.6",
"contact": {} "contact": {}
}, },
"tags": [], "tags": [],
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.125.7", "version": "1.125.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.125.7", "version": "1.125.6",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.125.7", "version": "1.125.6",
"description": "Auto-generated TypeScript SDK for the Immich API", "description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module", "type": "module",
"main": "./build/index.js", "main": "./build/index.js",
+6 -6
View File
@@ -1,6 +1,6 @@
/** /**
* Immich * Immich
* 1.125.7 * 1.125.6
* DO NOT MODIFY - This file has been generated using oazapfts. * DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts * See https://www.npmjs.com/package/oazapfts
*/ */
@@ -1475,7 +1475,7 @@ export function restoreUserAdmin({ id }: {
id: string; id: string;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 201;
data: UserAdminResponseDto; data: UserAdminResponseDto;
}>(`/admin/users/${encodeURIComponent(id)}/restore`, { }>(`/admin/users/${encodeURIComponent(id)}/restore`, {
...opts, ...opts,
@@ -1703,7 +1703,7 @@ export function updateAssets({ assetBulkUpdateDto }: {
}))); })));
} }
/** /**
* checkBulkUpload * Checks if assets exist by checksums
*/ */
export function checkBulkUpload({ assetBulkUploadCheckDto }: { export function checkBulkUpload({ assetBulkUploadCheckDto }: {
assetBulkUploadCheckDto: AssetBulkUploadCheckDto; assetBulkUploadCheckDto: AssetBulkUploadCheckDto;
@@ -1718,7 +1718,7 @@ export function checkBulkUpload({ assetBulkUploadCheckDto }: {
}))); })));
} }
/** /**
* getAllUserAssetsByDeviceId * Get all asset of a device that are in the database, ID only.
*/ */
export function getAllUserAssetsByDeviceId({ deviceId }: { export function getAllUserAssetsByDeviceId({ deviceId }: {
deviceId: string; deviceId: string;
@@ -1731,7 +1731,7 @@ export function getAllUserAssetsByDeviceId({ deviceId }: {
})); }));
} }
/** /**
* checkExistingAssets * Checks if multiple assets exist on the server and returns all existing - used by background backup
*/ */
export function checkExistingAssets({ checkExistingAssetsDto }: { export function checkExistingAssets({ checkExistingAssetsDto }: {
checkExistingAssetsDto: CheckExistingAssetsDto; checkExistingAssetsDto: CheckExistingAssetsDto;
@@ -1839,7 +1839,7 @@ export function downloadAsset({ id, key }: {
})); }));
} }
/** /**
* replaceAsset * Replace the asset with new file, without changing its id
*/ */
export function replaceAsset({ id, key, assetMediaReplaceDto }: { export function replaceAsset({ id, key, assetMediaReplaceDto }: {
id: string; id: string;
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "immich", "name": "immich",
"version": "1.125.7", "version": "1.125.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich", "name": "immich",
"version": "1.125.7", "version": "1.125.6",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@nestjs/bullmq": "^11.0.0", "@nestjs/bullmq": "^11.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich", "name": "immich",
"version": "1.125.7", "version": "1.125.6",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
@@ -14,7 +14,7 @@ import {
UploadedFiles, UploadedFiles,
UseInterceptors, UseInterceptors,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import { EndpointLifecycle } from 'src/decorators'; import { EndpointLifecycle } from 'src/decorators';
import { import {
@@ -94,10 +94,6 @@ export class AssetMediaController {
@UseInterceptors(FileUploadInterceptor) @UseInterceptors(FileUploadInterceptor)
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@EndpointLifecycle({ addedAt: 'v1.106.0' }) @EndpointLifecycle({ addedAt: 'v1.106.0' })
@ApiOperation({
summary: 'replaceAsset',
description: 'Replace the asset with new file, without changing its id',
})
@Authenticated({ sharedLink: true }) @Authenticated({ sharedLink: true })
async replaceAsset( async replaceAsset(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@@ -145,10 +141,6 @@ export class AssetMediaController {
*/ */
@Post('exist') @Post('exist')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'checkExistingAssets',
description: 'Checks if multiple assets exist on the server and returns all existing - used by background backup',
})
@Authenticated() @Authenticated()
checkExistingAssets( checkExistingAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@@ -162,10 +154,6 @@ export class AssetMediaController {
*/ */
@Post('bulk-upload-check') @Post('bulk-upload-check')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'checkBulkUpload',
description: 'Checks if assets exist by checksums',
})
@Authenticated() @Authenticated()
checkBulkUpload( checkBulkUpload(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
+1 -5
View File
@@ -1,5 +1,5 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { EndpointLifecycle } from 'src/decorators'; import { EndpointLifecycle } from 'src/decorators';
import { AssetResponseDto, MemoryLaneResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto, MemoryLaneResponseDto } from 'src/dtos/asset-response.dto';
import { import {
@@ -41,10 +41,6 @@ export class AssetController {
* Get all asset of a device that are in the database, ID only. * Get all asset of a device that are in the database, ID only.
*/ */
@Get('/device/:deviceId') @Get('/device/:deviceId')
@ApiOperation({
summary: 'getAllUserAssetsByDeviceId',
description: 'Get all asset of a device that are in the database, ID only.',
})
@Authenticated() @Authenticated()
getAllUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) { getAllUserAssetsByDeviceId(@Auth() auth: AuthDto, @Param() { deviceId }: DeviceIdDto) {
return this.service.getUserAssetsByDeviceId(auth, deviceId); return this.service.getUserAssetsByDeviceId(auth, deviceId);
@@ -1,4 +1,4 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
@@ -75,7 +75,6 @@ export class UserAdminController {
@Post(':id/restore') @Post(':id/restore')
@Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true }) @Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true })
@HttpCode(HttpStatus.OK)
restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> { restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
return this.service.restore(auth, id); return this.service.restore(auth, id);
} }
+15 -13
View File
@@ -3,21 +3,16 @@
* Please do not edit it manually. * Please do not edit it manually.
*/ */
import type { ColumnType } from "kysely"; import type { ColumnType } from 'kysely';
export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[] export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[] ? U[] : ArrayTypeImpl<T>;
? U[]
: ArrayTypeImpl<T>;
export type ArrayTypeImpl<T> = T extends ColumnType<infer S, infer I, infer U> export type ArrayTypeImpl<T> = T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S[], I[], U[]> : T[];
? ColumnType<S[], I[], U[]>
: T[];
export type AssetsStatusEnum = "active" | "deleted" | "trashed"; export type AssetsStatusEnum = 'active' | 'deleted' | 'trashed';
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U> export type Generated<T> =
? ColumnType<S, I | undefined, U> T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S, I | undefined, U> : ColumnType<T, T | undefined, T>;
: ColumnType<T, T | undefined, T>;
export type Int8 = ColumnType<string, bigint | number | string, bigint | number | string>; export type Int8 = ColumnType<string, bigint | number | string, bigint | number | string>;
@@ -33,7 +28,7 @@ export type JsonPrimitive = boolean | number | string | null;
export type JsonValue = JsonArray | JsonObject | JsonPrimitive; export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
export type Sourcetype = "exif" | "machine-learning"; export type Sourcetype = 'exif' | 'machine-learning';
export type Timestamp = ColumnType<Date, Date | string, Date | string>; export type Timestamp = ColumnType<Date, Date | string, Date | string>;
@@ -154,6 +149,12 @@ export interface AssetStack {
primaryAssetId: string; primaryAssetId: string;
} }
export interface AssetUser {
assetId: string;
createdAt: Timestamp;
userId: string;
}
export interface Audit { export interface Audit {
action: string; action: string;
createdAt: Generated<Timestamp>; createdAt: Generated<Timestamp>;
@@ -413,6 +414,7 @@ export interface DB {
asset_files: AssetFiles; asset_files: AssetFiles;
asset_job_status: AssetJobStatus; asset_job_status: AssetJobStatus;
asset_stack: AssetStack; asset_stack: AssetStack;
asset_user: AssetUser;
assets: Assets; assets: Assets;
audit: Audit; audit: Audit;
exif: Exif; exif: Exif;
@@ -438,6 +440,6 @@ export interface DB {
tags_closure: TagsClosure; tags_closure: TagsClosure;
user_metadata: UserMetadata; user_metadata: UserMetadata;
users: Users; users: Users;
"vectors.pg_vector_index_stat": VectorsPgVectorIndexStat; 'vectors.pg_vector_index_stat': VectorsPgVectorIndexStat;
version_history: VersionHistory; version_history: VersionHistory;
} }
+22
View File
@@ -0,0 +1,22 @@
import { AssetEntity } from 'src/entities/asset.entity';
import { UserEntity } from 'src/entities/user.entity';
import { Column, Entity, Index, ManyToOne, PrimaryColumn } from 'typeorm';
@Entity('asset_user')
@Index('IDX_assetId_userId', ['assetId', 'userId'])
export class AssetUserEntity {
@PrimaryColumn()
assetId!: string;
@ManyToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
asset!: AssetEntity;
@PrimaryColumn()
userId!: string;
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
user!: UserEntity;
@Column()
createdAt!: Date;
}
+2
View File
@@ -5,6 +5,7 @@ import { APIKeyEntity } from 'src/entities/api-key.entity';
import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetUserEntity } from 'src/entities/asset-user.entity';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { AuditEntity } from 'src/entities/audit.entity'; import { AuditEntity } from 'src/entities/audit.entity';
import { ExifEntity } from 'src/entities/exif.entity'; import { ExifEntity } from 'src/entities/exif.entity';
@@ -34,6 +35,7 @@ export const entities = [
AssetEntity, AssetEntity,
AssetFaceEntity, AssetFaceEntity,
AssetFileEntity, AssetFileEntity,
AssetUserEntity,
AssetJobStatusEntity, AssetJobStatusEntity,
AuditEntity, AuditEntity,
ExifEntity, ExifEntity,
-1
View File
@@ -36,7 +36,6 @@ export interface IUserRepository {
getUserStats(): Promise<UserStatsQueryResponse[]>; getUserStats(): Promise<UserStatsQueryResponse[]>;
create(user: Insertable<Users>): Promise<UserEntity>; create(user: Insertable<Users>): Promise<UserEntity>;
update(id: string, user: Updateable<Users>): Promise<UserEntity>; update(id: string, user: Updateable<Users>): Promise<UserEntity>;
restore(id: string): Promise<UserEntity>;
upsertMetadata<T extends keyof UserMetadata>(id: string, item: { key: T; value: UserMetadata[T] }): Promise<void>; upsertMetadata<T extends keyof UserMetadata>(id: string, item: { key: T; value: UserMetadata[T] }): Promise<void>;
deleteMetadata<T extends keyof UserMetadata>(id: string, key: T): Promise<void>; deleteMetadata<T extends keyof UserMetadata>(id: string, key: T): Promise<void>;
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>; delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddAssetUserTable1738099775096 implements MigrationInterface {
name = 'AddAssetUserTable1738099775096'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "asset_user" ("assetId" uuid NOT NULL, "userId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL, CONSTRAINT "PK_f3d7f17ab93d60e007282726058" PRIMARY KEY ("assetId", "userId"))`);
await queryRunner.query(`CREATE INDEX "IDX_assetId_userId" ON "asset_user" ("assetId", "userId") `);
await queryRunner.query(`ALTER TABLE "asset_user" ADD CONSTRAINT "FK_07c8478e0936e78b553aaeceedb" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE "asset_user" ADD CONSTRAINT "FK_85e2ef24493bdf649dfdfb769a2" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "asset_user" DROP CONSTRAINT "FK_85e2ef24493bdf649dfdfb769a2"`);
await queryRunner.query(`ALTER TABLE "asset_user" DROP CONSTRAINT "FK_07c8478e0936e78b553aaeceedb"`);
await queryRunner.query(`DROP INDEX "public"."IDX_assetId_userId"`);
await queryRunner.query(`DROP TABLE "asset_user"`);
}
}
+18 -1
View File
@@ -81,7 +81,24 @@ export class AssetRepository implements IAssetRepository {
} }
create(asset: Insertable<Assets>): Promise<AssetEntity> { create(asset: Insertable<Assets>): Promise<AssetEntity> {
return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirst() as any as Promise<AssetEntity>; return this.db.transaction().execute(async (tx) => {
const newAsset = (await tx
.insertInto('assets')
.values(asset)
.returningAll()
.executeTakeFirst()) as any as AssetEntity;
await tx
.insertInto('asset_user')
.values({
assetId: newAsset.id,
userId: newAsset.ownerId,
createdAt: new Date(),
})
.execute();
return newAsset;
});
} }
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] }) @GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
@@ -5,7 +5,6 @@ import { DB, UserMetadata as DbUserMetadata, Users } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators'; import { DummyValue, GenerateSql } from 'src/decorators';
import { UserMetadata } from 'src/entities/user-metadata.entity'; import { UserMetadata } from 'src/entities/user-metadata.entity';
import { UserEntity, withMetadata } from 'src/entities/user.entity'; import { UserEntity, withMetadata } from 'src/entities/user.entity';
import { UserStatus } from 'src/enum';
import { import {
IUserRepository, IUserRepository,
UserFindOptions, UserFindOptions,
@@ -141,16 +140,6 @@ export class UserRepository implements IUserRepository {
.executeTakeFirst() as unknown as Promise<UserEntity>; .executeTakeFirst() as unknown as Promise<UserEntity>;
} }
restore(id: string): Promise<UserEntity> {
return this.db
.updateTable('users')
.set({ status: UserStatus.ACTIVE, deletedAt: null })
.where('users.id', '=', asUuid(id))
.returning(columns)
.returning(withMetadata)
.executeTakeFirst() as unknown as Promise<UserEntity>;
}
async upsertMetadata<T extends keyof UserMetadata>(id: string, { key, value }: { key: T; value: UserMetadata[T] }) { async upsertMetadata<T extends keyof UserMetadata>(id: string, { key, value }: { key: T; value: UserMetadata[T] }) {
await this.db await this.db
.insertInto('user_metadata') .insertInto('user_metadata')
@@ -73,7 +73,6 @@ const validImages = [
'.heic', '.heic',
'.heif', '.heif',
'.iiq', '.iiq',
'.jp2',
'.jpeg', '.jpeg',
'.jpg', '.jpg',
'.jxl', '.jxl',
@@ -173,9 +173,9 @@ describe(UserAdminService.name, () => {
it('should restore an user', async () => { it('should restore an user', async () => {
userMock.get.mockResolvedValue(userStub.user1); userMock.get.mockResolvedValue(userStub.user1);
userMock.restore.mockResolvedValue(userStub.user1); userMock.update.mockResolvedValue(userStub.user1);
await expect(sut.restore(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUserAdmin(userStub.user1)); await expect(sut.restore(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUserAdmin(userStub.user1));
expect(userMock.restore).toHaveBeenCalledWith(userStub.user1.id); expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { status: UserStatus.ACTIVE, deletedAt: null });
}); });
}); });
}); });
+1 -1
View File
@@ -102,7 +102,7 @@ export class UserAdminService extends BaseService {
async restore(auth: AuthDto, id: string): Promise<UserAdminResponseDto> { async restore(auth: AuthDto, id: string): Promise<UserAdminResponseDto> {
await this.findOrFail(id, { withDeleted: true }); await this.findOrFail(id, { withDeleted: true });
await this.albumRepository.restoreAll(id); await this.albumRepository.restoreAll(id);
const user = await this.userRepository.restore(id); const user = await this.userRepository.update(id, { deletedAt: null, status: UserStatus.ACTIVE });
return mapUserAdmin(user); return mapUserAdmin(user);
} }
-1
View File
@@ -22,7 +22,6 @@ describe('mimeTypes', () => {
{ mimetype: 'image/heif', extension: '.heif' }, { mimetype: 'image/heif', extension: '.heif' },
{ mimetype: 'image/hif', extension: '.hif' }, { mimetype: 'image/hif', extension: '.hif' },
{ mimetype: 'image/iiq', extension: '.iiq' }, { mimetype: 'image/iiq', extension: '.iiq' },
{ mimetype: 'image/jp2', extension: '.jp2' },
{ mimetype: 'image/jpeg', extension: '.jpe' }, { mimetype: 'image/jpeg', extension: '.jpe' },
{ mimetype: 'image/jpeg', extension: '.jpeg' }, { mimetype: 'image/jpeg', extension: '.jpeg' },
{ mimetype: 'image/jpeg', extension: '.jpg' }, { mimetype: 'image/jpeg', extension: '.jpg' },
-1
View File
@@ -43,7 +43,6 @@ const image: Record<string, string[]> = {
'.heif': ['image/heif'], '.heif': ['image/heif'],
'.hif': ['image/hif'], '.hif': ['image/hif'],
'.insp': ['image/jpeg'], '.insp': ['image/jpeg'],
'.jp2': ['image/jp2'],
'.jpe': ['image/jpeg'], '.jpe': ['image/jpeg'],
'.jpeg': ['image/jpeg'], '.jpeg': ['image/jpeg'],
'.jpg': ['image/jpeg'], '.jpg': ['image/jpeg'],
@@ -13,7 +13,6 @@ export const newUserRepositoryMock = (): Mocked<IUserRepository> => {
create: vitest.fn(), create: vitest.fn(),
update: vitest.fn(), update: vitest.fn(),
delete: vitest.fn(), delete: vitest.fn(),
restore: vitest.fn(),
getDeletedUsers: vitest.fn(), getDeletedUsers: vitest.fn(),
hasAdmin: vitest.fn(), hasAdmin: vitest.fn(),
updateUsage: vitest.fn(), updateUsage: vitest.fn(),
+3 -3
View File
@@ -1,12 +1,12 @@
{ {
"name": "immich-web", "name": "immich-web",
"version": "1.125.7", "version": "1.125.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich-web", "name": "immich-web",
"version": "1.125.7", "version": "1.125.6",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8", "@formatjs/icu-messageformat-parser": "^2.9.8",
@@ -77,7 +77,7 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.125.7", "version": "1.125.6",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-web", "name": "immich-web",
"version": "1.125.7", "version": "1.125.6",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"scripts": { "scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000", "dev": "vite dev --host 0.0.0.0 --port 3000",
@@ -35,7 +35,6 @@
locale, locale,
type AlbumViewSettings, type AlbumViewSettings,
} from '$lib/stores/preferences.store'; } from '$lib/stores/preferences.store';
import { userInteraction } from '$lib/stores/user.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@@ -294,15 +293,6 @@
sharedAlbums[sharedAlbums.findIndex(({ id }) => id === album.id)] = album; sharedAlbums[sharedAlbums.findIndex(({ id }) => id === album.id)] = album;
}; };
const updateRecentAlbumInfo = (album: AlbumResponseDto) => {
for (const cachedAlbum of userInteraction.recentAlbums || []) {
if (cachedAlbum.id === album.id) {
Object.assign(cachedAlbum, { ...cachedAlbum, ...album });
break;
}
}
};
const successEditAlbumInfo = (album: AlbumResponseDto) => { const successEditAlbumInfo = (album: AlbumResponseDto) => {
albumToEdit = null; albumToEdit = null;
@@ -318,7 +308,6 @@
}); });
updateAlbumInfo(album); updateAlbumInfo(album);
updateRecentAlbumInfo(album);
}; };
const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => { const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => {