fix(web/server) uploaded asset in shared link not loaded (#1766)

* fix(web/server): Uploaded asset to shared link does not get added to the shared link/album

* remove unused code

* Add endpoints for each remove and add assets to shared link

* Update api

* Added deletion logic

* Convert callback to async/await

* Fix linter

* Fix test

* Fix server test

* added test

* Test coverage

* modify DTO

* Add notification

* fix test
This commit is contained in:
Alex 2023-02-15 15:21:22 -06:00 committed by GitHub
parent 125ec1e85f
commit b660240059
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 362 additions and 256 deletions

View file

@ -86,7 +86,6 @@ doc/ThumbnailFormat.md
doc/TimeGroupEnum.md doc/TimeGroupEnum.md
doc/UpdateAlbumDto.md doc/UpdateAlbumDto.md
doc/UpdateAssetDto.md doc/UpdateAssetDto.md
doc/UpdateAssetsToSharedLinkDto.md
doc/UpdateTagDto.md doc/UpdateTagDto.md
doc/UpdateUserDto.md doc/UpdateUserDto.md
doc/UpsertDeviceInfoDto.md doc/UpsertDeviceInfoDto.md
@ -189,7 +188,6 @@ lib/model/thumbnail_format.dart
lib/model/time_group_enum.dart lib/model/time_group_enum.dart
lib/model/update_album_dto.dart lib/model/update_album_dto.dart
lib/model/update_asset_dto.dart lib/model/update_asset_dto.dart
lib/model/update_assets_to_shared_link_dto.dart
lib/model/update_tag_dto.dart lib/model/update_tag_dto.dart
lib/model/update_user_dto.dart lib/model/update_user_dto.dart
lib/model/upsert_device_info_dto.dart lib/model/upsert_device_info_dto.dart
@ -281,7 +279,6 @@ test/thumbnail_format_test.dart
test/time_group_enum_test.dart test/time_group_enum_test.dart
test/update_album_dto_test.dart test/update_album_dto_test.dart
test/update_asset_dto_test.dart test/update_asset_dto_test.dart
test/update_assets_to_shared_link_dto_test.dart
test/update_tag_dto_test.dart test/update_tag_dto_test.dart
test/update_user_dto_test.dart test/update_user_dto_test.dart
test/upsert_device_info_dto_test.dart test/upsert_device_info_dto_test.dart

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,3 +1,4 @@
import { AddAssetsDto } from './../album/dto/add-assets.dto';
import { import {
Controller, Controller,
Post, Post,
@ -52,10 +53,10 @@ import {
import { DownloadFilesDto } from './dto/download-files.dto'; import { DownloadFilesDto } from './dto/download-files.dto';
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto'; import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
import { SharedLinkResponseDto } from '@app/domain'; import { SharedLinkResponseDto } from '@app/domain';
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
import { AssetSearchDto } from './dto/asset-search.dto'; import { AssetSearchDto } from './dto/asset-search.dto';
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config'; import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
import FileNotEmptyValidator from '../validation/file-not-empty-validator'; import FileNotEmptyValidator from '../validation/file-not-empty-validator';
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
function asStreamableFile({ stream, type, length }: ImmichReadStream) { function asStreamableFile({ stream, type, length }: ImmichReadStream) {
return new StreamableFile(stream, { type, length }); return new StreamableFile(stream, { type, length });
@ -330,11 +331,20 @@ export class AssetController {
} }
@Authenticated({ isShared: true }) @Authenticated({ isShared: true })
@Patch('/shared-link') @Patch('/shared-link/add')
async updateAssetsInSharedLink( async addAssetsToSharedLink(
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: UpdateAssetsToSharedLinkDto, @Body(ValidationPipe) dto: AddAssetsDto,
): Promise<SharedLinkResponseDto> { ): Promise<SharedLinkResponseDto> {
return await this.assetService.updateAssetsInSharedLink(authUser, dto); return await this.assetService.addAssetsToSharedLink(authUser, dto);
}
@Authenticated({ isShared: true })
@Patch('/shared-link/remove')
async removeAssetsFromSharedLink(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: RemoveAssetsDto,
): Promise<SharedLinkResponseDto> {
return await this.assetService.removeAssetsFromSharedLink(authUser, dto);
} }
} }

View file

@ -198,14 +198,31 @@ describe('AssetService', () => {
sharedLinkRepositoryMock.get.mockResolvedValue(null); sharedLinkRepositoryMock.get.mockResolvedValue(null);
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true); sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
await expect(sut.updateAssetsInSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException); await expect(sut.addAssetsToSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id); expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id);
expect(sharedLinkRepositoryMock.save).not.toHaveBeenCalled(); expect(sharedLinkRepositoryMock.save).not.toHaveBeenCalled();
}); });
it('should add assets to a shared link', async () => {
const asset1 = _getAsset_1();
const authDto = authStub.adminSharedLink;
const dto = { assetIds: [asset1.id] };
assetRepositoryMock.getById.mockResolvedValue(asset1);
sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.addAssetsToSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
});
it('should remove assets from a shared link', async () => { it('should remove assets from a shared link', async () => {
const asset1 = _getAsset_1(); const asset1 = _getAsset_1();
@ -217,11 +234,11 @@ describe('AssetService', () => {
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true); sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid); sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.updateAssetsInSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid); await expect(sut.removeAssetsFromSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id); expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId); expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id); expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
}); });
}); });

View file

@ -58,8 +58,9 @@ import { ISharedLinkRepository } from '@app/domain';
import { DownloadFilesDto } from './dto/download-files.dto'; import { DownloadFilesDto } from './dto/download-files.dto';
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto'; import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
import { mapSharedLink, SharedLinkResponseDto } from '@app/domain'; import { mapSharedLink, SharedLinkResponseDto } from '@app/domain';
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
import { AssetSearchDto } from './dto/asset-search.dto'; import { AssetSearchDto } from './dto/asset-search.dto';
import { AddAssetsDto } from '../album/dto/add-assets.dto';
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
const fileInfo = promisify(stat); const fileInfo = promisify(stat);
@ -606,23 +607,35 @@ export class AssetService {
return mapSharedLink(sharedLink); return mapSharedLink(sharedLink);
} }
async updateAssetsInSharedLink( async addAssetsToSharedLink(authUser: AuthUserDto, dto: AddAssetsDto): Promise<SharedLinkResponseDto> {
authUser: AuthUserDto,
dto: UpdateAssetsToSharedLinkDto,
): Promise<SharedLinkResponseDto> {
if (!authUser.sharedLinkId) { if (!authUser.sharedLinkId) {
throw new ForbiddenException(); throw new ForbiddenException();
} }
const assets = []; const assets = [];
await this.checkAssetsAccess(authUser, dto.assetIds);
for (const assetId of dto.assetIds) { for (const assetId of dto.assetIds) {
const asset = await this._assetRepository.getById(assetId); const asset = await this._assetRepository.getById(assetId);
assets.push(asset); assets.push(asset);
} }
const updatedLink = await this.shareCore.updateAssets(authUser.id, authUser.sharedLinkId, assets); const updatedLink = await this.shareCore.addAssets(authUser.id, authUser.sharedLinkId, assets);
return mapSharedLink(updatedLink);
}
async removeAssetsFromSharedLink(authUser: AuthUserDto, dto: RemoveAssetsDto): Promise<SharedLinkResponseDto> {
if (!authUser.sharedLinkId) {
throw new ForbiddenException();
}
const assets = [];
for (const assetId of dto.assetIds) {
const asset = await this._assetRepository.getById(assetId);
assets.push(asset);
}
const updatedLink = await this.shareCore.removeAssets(authUser.id, authUser.sharedLinkId, assets);
return mapSharedLink(updatedLink); return mapSharedLink(updatedLink);
} }

View file

@ -1,6 +0,0 @@
import { IsNotEmpty } from 'class-validator';
export class UpdateAssetsToSharedLinkDto {
@IsNotEmpty()
assetIds!: string[];
}

View file

@ -1869,9 +1869,11 @@
"bearer": [] "bearer": []
} }
] ]
}, }
},
"/asset/shared-link/add": {
"patch": { "patch": {
"operationId": "updateAssetsInSharedLink", "operationId": "addAssetsToSharedLink",
"description": "", "description": "",
"parameters": [], "parameters": [],
"requestBody": { "requestBody": {
@ -1879,7 +1881,44 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/UpdateAssetsToSharedLinkDto" "$ref": "#/components/schemas/AddAssetsDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SharedLinkResponseDto"
}
}
}
}
},
"tags": [
"Asset"
],
"security": [
{
"bearer": []
}
]
}
},
"/asset/shared-link/remove": {
"patch": {
"operationId": "removeAssetsFromSharedLink",
"description": "",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RemoveAssetsDto"
} }
} }
} }
@ -4171,7 +4210,21 @@
"assetIds" "assetIds"
] ]
}, },
"UpdateAssetsToSharedLinkDto": { "AddAssetsDto": {
"type": "object",
"properties": {
"assetIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"assetIds"
]
},
"RemoveAssetsDto": {
"type": "object", "type": "object",
"properties": { "properties": {
"assetIds": { "assetIds": {
@ -4267,20 +4320,6 @@
"sharedUserIds" "sharedUserIds"
] ]
}, },
"AddAssetsDto": {
"type": "object",
"properties": {
"assetIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"assetIds"
]
},
"AddAssetsResponseDto": { "AddAssetsResponseDto": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4302,20 +4341,6 @@
"alreadyInAlbum" "alreadyInAlbum"
] ]
}, },
"RemoveAssetsDto": {
"type": "object",
"properties": {
"assetIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"assetIds"
]
},
"UpdateAlbumDto": { "UpdateAlbumDto": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -63,13 +63,24 @@ export class ShareCore {
return this.repository.remove(link); return this.repository.remove(link);
} }
async updateAssets(userId: string, id: string, assets: AssetEntity[]) { async addAssets(userId: string, id: string, assets: AssetEntity[]) {
const link = await this.get(userId, id); const link = await this.get(userId, id);
if (!link) { if (!link) {
throw new BadRequestException('Shared link not found'); throw new BadRequestException('Shared link not found');
} }
return this.repository.save({ ...link, assets }); return this.repository.save({ ...link, assets: [...link.assets, ...assets] });
}
async removeAssets(userId: string, id: string, assets: AssetEntity[]) {
const link = await this.get(userId, id);
if (!link) {
throw new BadRequestException('Shared link not found');
}
const newAssets = link.assets.filter((asset) => assets.find((a) => a.id === asset.id));
return this.repository.save({ ...link, assets: newAssets });
} }
async hasAssetAccess(id: string, assetId: string): Promise<boolean> { async hasAssetAccess(id: string, assetId: string): Promise<boolean> {

View file

@ -140,9 +140,9 @@
}, },
"./libs/domain/": { "./libs/domain/": {
"branches": 80, "branches": 80,
"functions": 89, "functions": 88,
"lines": 95, "lines": 95,
"statements": 95 "statements": 94
} }
}, },
"testEnvironment": "node", "testEnvironment": "node",

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.46.1 * The version of the OpenAPI document: 1.47.2
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@ -2083,19 +2083,6 @@ export interface UpdateAssetDto {
*/ */
'isFavorite'?: boolean; 'isFavorite'?: boolean;
} }
/**
*
* @export
* @interface UpdateAssetsToSharedLinkDto
*/
export interface UpdateAssetsToSharedLinkDto {
/**
*
* @type {Array<string>}
* @memberof UpdateAssetsToSharedLinkDto
*/
'assetIds': Array<string>;
}
/** /**
* *
* @export * @export
@ -3588,6 +3575,45 @@ export class AlbumApi extends BaseAPI {
*/ */
export const AssetApiAxiosParamCreator = function (configuration?: Configuration) { export const AssetApiAxiosParamCreator = function (configuration?: Configuration) {
return { return {
/**
*
* @param {AddAssetsDto} addAssetsDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
addAssetsToSharedLink: async (addAssetsDto: AddAssetsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'addAssetsDto' is not null or undefined
assertParamExists('addAssetsToSharedLink', 'addAssetsDto', addAssetsDto)
const localVarPath = `/asset/shared-link/add`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(addAssetsDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* Check duplicated asset before uploading - for Web upload used * Check duplicated asset before uploading - for Web upload used
* @param {CheckDuplicateAssetDto} checkDuplicateAssetDto * @param {CheckDuplicateAssetDto} checkDuplicateAssetDto
@ -4232,6 +4258,45 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions, options: localVarRequestOptions,
}; };
}, },
/**
*
* @param {RemoveAssetsDto} removeAssetsDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
removeAssetsFromSharedLink: async (removeAssetsDto: RemoveAssetsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'removeAssetsDto' is not null or undefined
assertParamExists('removeAssetsFromSharedLink', 'removeAssetsDto', removeAssetsDto)
const localVarPath = `/asset/shared-link/remove`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(removeAssetsDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* *
* @param {SearchAssetDto} searchAssetDto * @param {SearchAssetDto} searchAssetDto
@ -4361,45 +4426,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions, options: localVarRequestOptions,
}; };
}, },
/**
*
* @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateAssetsInSharedLink: async (updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'updateAssetsToSharedLinkDto' is not null or undefined
assertParamExists('updateAssetsInSharedLink', 'updateAssetsToSharedLinkDto', updateAssetsToSharedLinkDto)
const localVarPath = `/asset/shared-link`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(updateAssetsToSharedLinkDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* *
* @param {AssetTypeEnum} assetType * @param {AssetTypeEnum} assetType
@ -4518,6 +4544,16 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
export const AssetApiFp = function(configuration?: Configuration) { export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = AssetApiAxiosParamCreator(configuration) const localVarAxiosParamCreator = AssetApiAxiosParamCreator(configuration)
return { return {
/**
*
* @param {AddAssetsDto} addAssetsDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async addAssetsToSharedLink(addAssetsDto: AddAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToSharedLink(addAssetsDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* Check duplicated asset before uploading - for Web upload used * Check duplicated asset before uploading - for Web upload used
* @param {CheckDuplicateAssetDto} checkDuplicateAssetDto * @param {CheckDuplicateAssetDto} checkDuplicateAssetDto
@ -4687,6 +4723,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getUserAssetsByDeviceId(deviceId, options); const localVarAxiosArgs = await localVarAxiosParamCreator.getUserAssetsByDeviceId(deviceId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @param {RemoveAssetsDto} removeAssetsDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async removeAssetsFromSharedLink(removeAssetsDto: RemoveAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetsFromSharedLink(removeAssetsDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {SearchAssetDto} searchAssetDto * @param {SearchAssetDto} searchAssetDto
@ -4720,16 +4766,6 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateAsset(assetId, updateAssetDto, options); const localVarAxiosArgs = await localVarAxiosParamCreator.updateAsset(assetId, updateAssetDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {AssetTypeEnum} assetType * @param {AssetTypeEnum} assetType
@ -4760,6 +4796,15 @@ export const AssetApiFp = function(configuration?: Configuration) {
export const AssetApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { export const AssetApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = AssetApiFp(configuration) const localVarFp = AssetApiFp(configuration)
return { return {
/**
*
* @param {AddAssetsDto} addAssetsDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
addAssetsToSharedLink(addAssetsDto: AddAssetsDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
return localVarFp.addAssetsToSharedLink(addAssetsDto, options).then((request) => request(axios, basePath));
},
/** /**
* Check duplicated asset before uploading - for Web upload used * Check duplicated asset before uploading - for Web upload used
* @param {CheckDuplicateAssetDto} checkDuplicateAssetDto * @param {CheckDuplicateAssetDto} checkDuplicateAssetDto
@ -4912,6 +4957,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
getUserAssetsByDeviceId(deviceId: string, options?: any): AxiosPromise<Array<string>> { getUserAssetsByDeviceId(deviceId: string, options?: any): AxiosPromise<Array<string>> {
return localVarFp.getUserAssetsByDeviceId(deviceId, options).then((request) => request(axios, basePath)); return localVarFp.getUserAssetsByDeviceId(deviceId, options).then((request) => request(axios, basePath));
}, },
/**
*
* @param {RemoveAssetsDto} removeAssetsDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
removeAssetsFromSharedLink(removeAssetsDto: RemoveAssetsDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
return localVarFp.removeAssetsFromSharedLink(removeAssetsDto, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {SearchAssetDto} searchAssetDto * @param {SearchAssetDto} searchAssetDto
@ -4942,15 +4996,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
updateAsset(assetId: string, updateAssetDto: UpdateAssetDto, options?: any): AxiosPromise<AssetResponseDto> { updateAsset(assetId: string, updateAssetDto: UpdateAssetDto, options?: any): AxiosPromise<AssetResponseDto> {
return localVarFp.updateAsset(assetId, updateAssetDto, options).then((request) => request(axios, basePath)); return localVarFp.updateAsset(assetId, updateAssetDto, options).then((request) => request(axios, basePath));
}, },
/**
*
* @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
return localVarFp.updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {AssetTypeEnum} assetType * @param {AssetTypeEnum} assetType
@ -4980,6 +5025,17 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @extends {BaseAPI} * @extends {BaseAPI}
*/ */
export class AssetApi extends BaseAPI { export class AssetApi extends BaseAPI {
/**
*
* @param {AddAssetsDto} addAssetsDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public addAssetsToSharedLink(addAssetsDto: AddAssetsDto, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).addAssetsToSharedLink(addAssetsDto, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* Check duplicated asset before uploading - for Web upload used * Check duplicated asset before uploading - for Web upload used
* @param {CheckDuplicateAssetDto} checkDuplicateAssetDto * @param {CheckDuplicateAssetDto} checkDuplicateAssetDto
@ -5166,6 +5222,17 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).getUserAssetsByDeviceId(deviceId, options).then((request) => request(this.axios, this.basePath)); return AssetApiFp(this.configuration).getUserAssetsByDeviceId(deviceId, options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @param {RemoveAssetsDto} removeAssetsDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public removeAssetsFromSharedLink(removeAssetsDto: RemoveAssetsDto, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).removeAssetsFromSharedLink(removeAssetsDto, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @param {SearchAssetDto} searchAssetDto * @param {SearchAssetDto} searchAssetDto
@ -5202,17 +5269,6 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).updateAsset(assetId, updateAssetDto, options).then((request) => request(this.axios, this.basePath)); return AssetApiFp(this.configuration).updateAsset(assetId, updateAssetDto, options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @param {AssetTypeEnum} assetType * @param {AssetTypeEnum} assetType

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.46.1 * The version of the OpenAPI document: 1.47.2
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.46.1 * The version of the OpenAPI document: 1.47.2
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.46.1 * The version of the OpenAPI document: 1.47.2
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.46.1 * The version of the OpenAPI document: 1.47.2
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -16,6 +16,7 @@
export let albumId: string; export let albumId: string;
export let assetsInAlbum: AssetResponseDto[]; export let assetsInAlbum: AssetResponseDto[];
const locale = navigator.language;
onMount(() => { onMount(() => {
$assetsInAlbumStoreState = assetsInAlbum; $assetsInAlbumStoreState = assetsInAlbum;
@ -28,8 +29,11 @@
assetInteractionStore.clearMultiselect(); assetInteractionStore.clearMultiselect();
}; };
const handleSelectFromComputerClicked = async () => {
const locale = navigator.language; await openFileUploadDialog(albumId, '');
assetInteractionStore.clearMultiselect();
dispatch('go-back');
};
</script> </script>
<section <section
@ -54,11 +58,7 @@
<svelte:fragment slot="trailing"> <svelte:fragment slot="trailing">
<button <button
on:click={() => on:click={handleSelectFromComputerClicked}
openFileUploadDialog(albumId, '', () => {
assetInteractionStore.clearMultiselect();
dispatch('go-back');
})}
class="text-immich-primary dark:text-immich-dark-primary text-sm hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/25 transition-all px-6 py-2 rounded-lg font-medium" class="text-immich-primary dark:text-immich-dark-primary text-sm hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/25 transition-all px-6 py-2 rounded-lg font-medium"
> >
Select from computer Select from computer

View file

@ -13,11 +13,11 @@
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte'; import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte';
import { import {
notificationController, notificationController,
NotificationType NotificationType
} from '../shared-components/notification/notification'; } from '../shared-components/notification/notification';
import ImmichLogo from '../shared-components/immich-logo.svelte';
export let sharedLink: SharedLinkResponseDto; export let sharedLink: SharedLinkResponseDto;
export let isOwned: boolean; export let isOwned: boolean;
@ -43,11 +43,15 @@
); );
}; };
const handleUploadAssets = () => { const handleUploadAssets = async () => {
openFileUploadDialog(undefined, sharedLink?.key, async (assetId) => { try {
await api.assetApi.updateAssetsInSharedLink( const results = await openFileUploadDialog(undefined, sharedLink?.key);
const assetIds = results.filter((id) => !!id) as string[];
await api.assetApi.addAssetsToSharedLink(
{ {
assetIds: [...assets.map((a) => a.id), assetId] assetIds
}, },
{ {
params: { params: {
@ -57,15 +61,17 @@
); );
notificationController.show({ notificationController.show({
message: 'Add asset to shared link successfully', message: `Successfully add ${assetIds.length} to the shared link`,
type: NotificationType.Info type: NotificationType.Info
}); });
}); } catch (e) {
console.error('handleUploadAssets', e);
}
}; };
const handleRemoveAssetsFromSharedLink = async () => { const handleRemoveAssetsFromSharedLink = async () => {
if (window.confirm('Do you want to remove selected assets from the shared link?')) { if (window.confirm('Do you want to remove selected assets from the shared link?')) {
await api.assetApi.updateAssetsInSharedLink( await api.assetApi.removeAssetsFromSharedLink(
{ {
assetIds: assets.filter((a) => !selectedAssets.has(a)).map((a) => a.id) assetIds: assets.filter((a) => !selectedAssets.has(a)).map((a) => a.id)
}, },

View file

@ -6,71 +6,65 @@ import { uploadAssetsStore } from '$lib/stores/upload';
import type { UploadAsset } from '../models/upload-asset'; import type { UploadAsset } from '../models/upload-asset';
import { api, AssetFileUploadResponseDto } from '@api'; import { api, AssetFileUploadResponseDto } from '@api';
import { addAssetsToAlbum, getFileMimeType, getFilenameExtension } from '$lib/utils/asset-utils'; import { addAssetsToAlbum, getFileMimeType, getFilenameExtension } from '$lib/utils/asset-utils';
import { Subject, mergeMap } from 'rxjs'; import { mergeMap, filter, firstValueFrom, from, of, combineLatestAll } from 'rxjs';
import axios from 'axios';
export const openFileUploadDialog = ( export const openFileUploadDialog = async (
albumId: string | undefined = undefined, albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined, sharedKey: string | undefined = undefined
onDone?: (id: string) => void
) => { ) => {
try { return new Promise<(string | undefined)[]>((resolve, reject) => {
const fileSelector = document.createElement('input'); try {
const fileSelector = document.createElement('input');
fileSelector.type = 'file'; fileSelector.type = 'file';
fileSelector.multiple = true; fileSelector.multiple = true;
// When adding a content type that is unsupported by browsers, make sure // When adding a content type that is unsupported by browsers, make sure
// to also add it to getFileMimeType() otherwise the upload will fail. // to also add it to getFileMimeType() otherwise the upload will fail.
fileSelector.accept = 'image/*,video/*,.heic,.heif,.dng,.3gp,.nef,.srw,.raf'; fileSelector.accept = 'image/*,video/*,.heic,.heif,.dng,.3gp,.nef,.srw,.raf';
fileSelector.onchange = async (e: Event) => { fileSelector.onchange = async (e: Event) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
if (!target.files) { if (!target.files) {
return; return;
} }
const files = Array.from<File>(target.files); const files = Array.from<File>(target.files);
await fileUploadHandler(files, albumId, sharedKey, onDone); resolve(await fileUploadHandler(files, albumId, sharedKey));
}; };
fileSelector.click(); fileSelector.click();
} catch (e) { } catch (e) {
console.log('Error selecting file', e); console.log('Error selecting file', e);
} reject(e);
}
});
}; };
export const fileUploadHandler = async ( export const fileUploadHandler = async (
files: File[], files: File[],
albumId: string | undefined = undefined, albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined, sharedKey: string | undefined = undefined
onDone?: (id: string) => void
) => { ) => {
const files$ = new Subject<File>(); return firstValueFrom(
files$ from(files).pipe(
.pipe( filter((file) => {
mergeMap(async (file) => { const assetType = getFileMimeType(file).split('/')[0];
await fileUploader(file, albumId, sharedKey, onDone); return assetType === 'video' || assetType === 'image';
}, 2) }),
mergeMap(async (file) => of(await fileUploader(file, albumId, sharedKey)), 2),
combineLatestAll()
) )
.subscribe(); );
const acceptedFile = files.filter((file) => {
const assetType = getFileMimeType(file).split('/')[0];
return assetType === 'video' || assetType === 'image';
});
for (const file of acceptedFile) {
files$.next(file);
}
}; };
//TODO: should probably use the @api SDK //TODO: should probably use the @api SDK
async function fileUploader( async function fileUploader(
asset: File, asset: File,
albumId: string | undefined = undefined, albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined, sharedKey: string | undefined = undefined
onDone?: (id: string) => void ): Promise<string | undefined> {
) {
console.log('uploading', asset.name);
const mimeType = getFileMimeType(asset); const mimeType = getFileMimeType(asset);
const assetType = mimeType.split('/')[0].toUpperCase(); const assetType = mimeType.split('/')[0].toUpperCase();
const fileExtension = getFilenameExtension(asset.name); const fileExtension = getFilenameExtension(asset.name);
@ -121,67 +115,50 @@ async function fileUploader(
} }
); );
if (status === 200) { if (status === 200 && data.isExist && data.id) {
if (data.isExist) { if (albumId) {
const dataId = data.id; await addAssetsToAlbum(albumId, [data.id], sharedKey);
if (albumId && dataId) {
addAssetsToAlbum(albumId, [dataId], sharedKey);
}
onDone && dataId && onDone(dataId);
return;
} }
return data.id;
} }
const request = new XMLHttpRequest(); const newUploadAsset: UploadAsset = {
request.upload.onloadstart = () => { id: deviceAssetId,
const newUploadAsset: UploadAsset = { file: asset,
id: deviceAssetId, progress: 0,
file: asset, fileExtension: fileExtension
progress: 0,
fileExtension: fileExtension
};
uploadAssetsStore.addNewUploadAsset(newUploadAsset);
}; };
request.upload.onload = () => { uploadAssetsStore.addNewUploadAsset(newUploadAsset);
uploadAssetsStore.removeUploadAsset(deviceAssetId);
const res: AssetFileUploadResponseDto = JSON.parse(request.response || '{}'); const response = await axios.post(`/api/asset/upload`, formData, {
if (albumId) { params: {
try { key: sharedKey
if (res.id) { },
addAssetsToAlbum(albumId, [res.id], sharedKey); onUploadProgress: (event) => {
} const percentComplete = Math.floor((event.loaded / event.total) * 100);
} catch (e) { uploadAssetsStore.updateProgress(deviceAssetId, percentComplete);
console.error('ERROR parsing data JSON in upload onload');
}
} }
onDone && onDone(res.id); });
};
// listen for `error` event if (response.status == 200 || response.status == 201) {
request.upload.onerror = () => { const res: AssetFileUploadResponseDto = response.data;
uploadAssetsStore.removeUploadAsset(deviceAssetId);
handleUploadError(asset, request.response);
};
// listen for `abort` event if (albumId && res.id) {
request.upload.onabort = () => { await addAssetsToAlbum(albumId, [res.id], sharedKey);
uploadAssetsStore.removeUploadAsset(deviceAssetId); }
handleUploadError(asset, request.response);
};
// listen for `progress` event setTimeout(() => {
request.upload.onprogress = (event) => { uploadAssetsStore.removeUploadAsset(deviceAssetId);
const percentComplete = Math.floor((event.loaded / event.total) * 100); }, 1000);
uploadAssetsStore.updateProgress(deviceAssetId, percentComplete);
};
request.open('POST', `/api/asset/upload?key=${sharedKey ?? ''}`); return res.id;
}
request.send(formData);
} catch (e) { } catch (e) {
console.log('error uploading file ', e); console.log('error uploading file ', e);
handleUploadError(asset, JSON.stringify(e));
uploadAssetsStore.removeUploadAsset(deviceAssetId);
} }
} }