1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(server): improve API specification (#1853)

This commit is contained in:
Michel Heusschen 2023-02-24 17:01:10 +01:00 committed by GitHub
parent da9b9c8c69
commit 9323cc76d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 965 additions and 223 deletions

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.

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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -22,7 +22,7 @@ import { AddUsersDto } from './dto/add-users.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto'; import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto'; import { UpdateAlbumDto } from './dto/update-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto'; import { GetAlbumsDto } from './dto/get-albums.dto';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AlbumResponseDto } from '@app/domain'; import { AlbumResponseDto } from '@app/domain';
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto'; import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto'; import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
@ -37,7 +37,6 @@ import { CreateAlbumShareLinkDto as CreateAlbumSharedLinkDto } from './dto/creat
// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe. // TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe.
@ApiBearerAuth()
@ApiTags('Album') @ApiTags('Album')
@Controller('album') @Controller('album')
export class AlbumController { export class AlbumController {
@ -134,12 +133,13 @@ export class AlbumController {
@Authenticated({ isShared: true }) @Authenticated({ isShared: true })
@Get('/:albumId/download') @Get('/:albumId/download')
@ApiOkResponse({ content: { 'application/zip': { schema: { type: 'string', format: 'binary' } } } })
async downloadArchive( async downloadArchive(
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string, @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string,
@Query(new ValidationPipe({ transform: true })) dto: DownloadDto, @Query(new ValidationPipe({ transform: true })) dto: DownloadDto,
@Response({ passthrough: true }) res: Res, @Response({ passthrough: true }) res: Res,
): Promise<any> { ) {
this.albumService.checkDownloadAccess(authUser); this.albumService.checkDownloadAccess(authUser);
const { stream, fileName, fileSize, fileCount, complete } = await this.albumService.downloadArchive( const { stream, fileName, fileSize, fileCount, complete } = await this.albumService.downloadArchive(

View File

@ -28,7 +28,7 @@ import { Response as Res } from 'express';
import { DeleteAssetDto } from './dto/delete-asset.dto'; import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto'; import { SearchAssetDto } from './dto/search-asset.dto';
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger'; import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import { AssetResponseDto, ImmichReadStream } from '@app/domain'; import { AssetResponseDto, ImmichReadStream } from '@app/domain';
@ -62,7 +62,6 @@ function asStreamableFile({ stream, type, length }: ImmichReadStream) {
return new StreamableFile(stream, { type, length }); return new StreamableFile(stream, { type, length });
} }
@ApiBearerAuth()
@ApiTags('Asset') @ApiTags('Asset')
@Controller('asset') @Controller('asset')
export class AssetController { export class AssetController {
@ -108,21 +107,23 @@ export class AssetController {
@Authenticated({ isShared: true }) @Authenticated({ isShared: true })
@Get('/download/:assetId') @Get('/download/:assetId')
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
async downloadFile( async downloadFile(
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Response({ passthrough: true }) res: Res, @Response({ passthrough: true }) res: Res,
@Param('assetId') assetId: string, @Param('assetId') assetId: string,
): Promise<any> { ) {
return this.assetService.downloadFile(authUser, assetId).then(asStreamableFile); return this.assetService.downloadFile(authUser, assetId).then(asStreamableFile);
} }
@Authenticated({ isShared: true }) @Authenticated({ isShared: true })
@Post('/download-files') @Post('/download-files')
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
async downloadFiles( async downloadFiles(
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Response({ passthrough: true }) res: Res, @Response({ passthrough: true }) res: Res,
@Body(new ValidationPipe()) dto: DownloadFilesDto, @Body(new ValidationPipe()) dto: DownloadFilesDto,
): Promise<any> { ) {
this.assetService.checkDownloadAccess(authUser); this.assetService.checkDownloadAccess(authUser);
await this.assetService.checkAssetsAccess(authUser, [...dto.assetIds]); await this.assetService.checkAssetsAccess(authUser, [...dto.assetIds]);
const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadFiles(dto); const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadFiles(dto);
@ -138,11 +139,12 @@ export class AssetController {
*/ */
@Authenticated({ isShared: true }) @Authenticated({ isShared: true })
@Get('/download-library') @Get('/download-library')
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
async downloadLibrary( async downloadLibrary(
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Query(new ValidationPipe({ transform: true })) dto: DownloadDto, @Query(new ValidationPipe({ transform: true })) dto: DownloadDto,
@Response({ passthrough: true }) res: Res, @Response({ passthrough: true }) res: Res,
): Promise<any> { ) {
this.assetService.checkDownloadAccess(authUser); this.assetService.checkDownloadAccess(authUser);
const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadLibrary(authUser, dto); const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadLibrary(authUser, dto);
res.attachment(fileName); res.attachment(fileName);
@ -155,13 +157,14 @@ export class AssetController {
@Authenticated({ isShared: true }) @Authenticated({ isShared: true })
@Get('/file/:assetId') @Get('/file/:assetId')
@Header('Cache-Control', 'max-age=31536000') @Header('Cache-Control', 'max-age=31536000')
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
async serveFile( async serveFile(
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Headers() headers: Record<string, string>, @Headers() headers: Record<string, string>,
@Response({ passthrough: true }) res: Res, @Response({ passthrough: true }) res: Res,
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto, @Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
@Param('assetId') assetId: string, @Param('assetId') assetId: string,
): Promise<any> { ) {
await this.assetService.checkAssetsAccess(authUser, [assetId]); await this.assetService.checkAssetsAccess(authUser, [assetId]);
return this.assetService.serveFile(authUser, assetId, query, res, headers); return this.assetService.serveFile(authUser, assetId, query, res, headers);
} }
@ -169,13 +172,14 @@ export class AssetController {
@Authenticated({ isShared: true }) @Authenticated({ isShared: true })
@Get('/thumbnail/:assetId') @Get('/thumbnail/:assetId')
@Header('Cache-Control', 'max-age=31536000') @Header('Cache-Control', 'max-age=31536000')
@ApiOkResponse({ content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } })
async getAssetThumbnail( async getAssetThumbnail(
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Headers() headers: Record<string, string>, @Headers() headers: Record<string, string>,
@Response({ passthrough: true }) res: Res, @Response({ passthrough: true }) res: Res,
@Param('assetId') assetId: string, @Param('assetId') assetId: string,
@Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto, @Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto,
): Promise<any> { ) {
await this.assetService.checkAssetsAccess(authUser, [assetId]); await this.assetService.checkAssetsAccess(authUser, [assetId]);
return this.assetService.getAssetThumbnail(assetId, query, res, headers); return this.assetService.getAssetThumbnail(assetId, query, res, headers);
} }

View File

@ -1,5 +1,5 @@
import { Body, Controller, Get, Param, Put, ValidationPipe } from '@nestjs/common'; import { Body, Controller, Get, Param, Put, ValidationPipe } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Authenticated } from '../../decorators/authenticated.decorator'; import { Authenticated } from '../../decorators/authenticated.decorator';
import { AllJobStatusResponseDto } from './response-dto/all-job-status-response.dto'; import { AllJobStatusResponseDto } from './response-dto/all-job-status-response.dto';
import { GetJobDto } from './dto/get-job.dto'; import { GetJobDto } from './dto/get-job.dto';
@ -8,7 +8,6 @@ import { JobCommandDto } from './dto/job-command.dto';
@Authenticated({ admin: true }) @Authenticated({ admin: true })
@ApiTags('Job') @ApiTags('Job')
@ApiBearerAuth()
@Controller('jobs') @Controller('jobs')
export class JobController { export class JobController {
constructor(private readonly jobService: JobService) {} constructor(private readonly jobService: JobService) {}

View File

@ -14,7 +14,7 @@ import {
ValidateAccessTokenResponseDto, ValidateAccessTokenResponseDto,
} from '@app/domain'; } from '@app/domain';
import { Body, Controller, Ip, Post, Req, Res, ValidationPipe } from '@nestjs/common'; import { Body, Controller, Ip, Post, Req, Res, ValidationPipe } from '@nestjs/common';
import { ApiBadRequestResponse, ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { ApiBadRequestResponse, ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { GetAuthUser } from '../decorators/auth-user.decorator'; import { GetAuthUser } from '../decorators/auth-user.decorator';
import { Authenticated } from '../decorators/authenticated.decorator'; import { Authenticated } from '../decorators/authenticated.decorator';
@ -45,7 +45,6 @@ export class AuthController {
} }
@Authenticated() @Authenticated()
@ApiBearerAuth()
@Post('validateToken') @Post('validateToken')
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
validateAccessToken(@GetAuthUser() authUser: AuthUserDto): ValidateAccessTokenResponseDto { validateAccessToken(@GetAuthUser() authUser: AuthUserDto): ValidateAccessTokenResponseDto {
@ -53,7 +52,6 @@ export class AuthController {
} }
@Authenticated() @Authenticated()
@ApiBearerAuth()
@Post('change-password') @Post('change-password')
async changePassword(@GetAuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> { async changePassword(@GetAuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
return this.authService.changePassword(authUser, dto); return this.authService.changePassword(authUser, dto);

View File

@ -5,12 +5,11 @@ import {
UpsertDeviceInfoDto as UpsertDto, UpsertDeviceInfoDto as UpsertDto,
} from '@app/domain'; } from '@app/domain';
import { Body, Controller, Put, ValidationPipe } from '@nestjs/common'; import { Body, Controller, Put, ValidationPipe } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { GetAuthUser } from '../decorators/auth-user.decorator'; import { GetAuthUser } from '../decorators/auth-user.decorator';
import { Authenticated } from '../decorators/authenticated.decorator'; import { Authenticated } from '../decorators/authenticated.decorator';
@Authenticated() @Authenticated()
@ApiBearerAuth()
@ApiTags('Device Info') @ApiTags('Device Info')
@Controller('device-info') @Controller('device-info')
export class DeviceInfoController { export class DeviceInfoController {

View File

@ -1,10 +1,9 @@
import { SystemConfigDto, SystemConfigService, SystemConfigTemplateStorageOptionDto } from '@app/domain'; import { SystemConfigDto, SystemConfigService, SystemConfigTemplateStorageOptionDto } from '@app/domain';
import { Body, Controller, Get, Put, ValidationPipe } from '@nestjs/common'; import { Body, Controller, Get, Put, ValidationPipe } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Authenticated } from '../decorators/authenticated.decorator'; import { Authenticated } from '../decorators/authenticated.decorator';
@ApiTags('System Config') @ApiTags('System Config')
@ApiBearerAuth()
@Authenticated({ admin: true }) @Authenticated({ admin: true })
@Controller('system-config') @Controller('system-config')
export class SystemConfigController { export class SystemConfigController {

View File

@ -23,7 +23,7 @@ import { UpdateUserDto } from '@app/domain';
import { FileInterceptor } from '@nestjs/platform-express'; import { FileInterceptor } from '@nestjs/platform-express';
import { profileImageUploadOption } from '../config/profile-image-upload.config'; import { profileImageUploadOption } from '../config/profile-image-upload.config';
import { Response as Res } from 'express'; import { Response as Res } from 'express';
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { UserResponseDto } from '@app/domain'; import { UserResponseDto } from '@app/domain';
import { UserCountResponseDto } from '@app/domain'; import { UserCountResponseDto } from '@app/domain';
import { CreateProfileImageDto } from '@app/domain'; import { CreateProfileImageDto } from '@app/domain';
@ -36,7 +36,6 @@ export class UserController {
constructor(private readonly userService: UserService) {} constructor(private readonly userService: UserService) {}
@Authenticated() @Authenticated()
@ApiBearerAuth()
@Get() @Get()
async getAllUsers( async getAllUsers(
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@ -51,14 +50,12 @@ export class UserController {
} }
@Authenticated() @Authenticated()
@ApiBearerAuth()
@Get('me') @Get('me')
async getMyUserInfo(@GetAuthUser() authUser: AuthUserDto): Promise<UserResponseDto> { async getMyUserInfo(@GetAuthUser() authUser: AuthUserDto): Promise<UserResponseDto> {
return await this.userService.getUserInfo(authUser); return await this.userService.getUserInfo(authUser);
} }
@Authenticated({ admin: true }) @Authenticated({ admin: true })
@ApiBearerAuth()
@Post() @Post()
async createUser( async createUser(
@Body(new ValidationPipe({ transform: true })) createUserDto: CreateUserDto, @Body(new ValidationPipe({ transform: true })) createUserDto: CreateUserDto,
@ -72,21 +69,18 @@ export class UserController {
} }
@Authenticated({ admin: true }) @Authenticated({ admin: true })
@ApiBearerAuth()
@Delete('/:userId') @Delete('/:userId')
async deleteUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise<UserResponseDto> { async deleteUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise<UserResponseDto> {
return await this.userService.deleteUser(authUser, userId); return await this.userService.deleteUser(authUser, userId);
} }
@Authenticated({ admin: true }) @Authenticated({ admin: true })
@ApiBearerAuth()
@Post('/:userId/restore') @Post('/:userId/restore')
async restoreUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise<UserResponseDto> { async restoreUser(@GetAuthUser() authUser: AuthUserDto, @Param('userId') userId: string): Promise<UserResponseDto> {
return await this.userService.restoreUser(authUser, userId); return await this.userService.restoreUser(authUser, userId);
} }
@Authenticated() @Authenticated()
@ApiBearerAuth()
@Put() @Put()
async updateUser( async updateUser(
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@ -97,7 +91,6 @@ export class UserController {
@UseInterceptors(FileInterceptor('file', profileImageUploadOption)) @UseInterceptors(FileInterceptor('file', profileImageUploadOption))
@Authenticated() @Authenticated()
@ApiBearerAuth()
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@ApiBody({ @ApiBody({
description: 'A new avatar for the user', description: 'A new avatar for the user',

View File

@ -1,4 +1,5 @@
import { applyDecorators, SetMetadata } from '@nestjs/common'; import { applyDecorators, SetMetadata } from '@nestjs/common';
import { ApiBearerAuth, ApiCookieAuth, ApiQuery } from '@nestjs/swagger';
interface AuthenticatedOptions { interface AuthenticatedOptions {
admin?: boolean; admin?: boolean;
@ -12,7 +13,7 @@ export enum Metadata {
} }
export const Authenticated = (options?: AuthenticatedOptions) => { export const Authenticated = (options?: AuthenticatedOptions) => {
const decorators = [SetMetadata(Metadata.AUTH_ROUTE, true)]; const decorators: MethodDecorator[] = [ApiBearerAuth(), ApiCookieAuth(), SetMetadata(Metadata.AUTH_ROUTE, true)];
options = options || {}; options = options || {};
@ -22,6 +23,7 @@ export const Authenticated = (options?: AuthenticatedOptions) => {
if (options.isShared) { if (options.isShared) {
decorators.push(SetMetadata(Metadata.SHARED_ROUTE, true)); decorators.push(SetMetadata(Metadata.SHARED_ROUTE, true));
decorators.push(ApiQuery({ name: 'key', type: String, required: false }));
} }
return applyDecorators(...decorators); return applyDecorators(...decorators);

View File

@ -11,6 +11,7 @@ import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
import { json } from 'body-parser'; import { json } from 'body-parser';
import { patchOpenAPI } from './utils/patch-open-api.util'; import { patchOpenAPI } from './utils/patch-open-api.util';
import { getLogLevels, MACHINE_LEARNING_ENABLED } from '@app/common'; import { getLogLevels, MACHINE_LEARNING_ENABLED } from '@app/common';
import { IMMICH_ACCESS_COOKIE } from '@app/domain';
const logger = new Logger('ImmichServer'); const logger = new Logger('ImmichServer');
@ -42,6 +43,7 @@ async function bootstrap() {
scheme: 'Bearer', scheme: 'Bearer',
in: 'header', in: 'header',
}) })
.addCookieAuth(IMMICH_ACCESS_COOKIE)
.addServer('/api') .addServer('/api')
.build(); .build();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -93,6 +93,7 @@ describe('AlbumCard component', () => {
expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledWith( expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledWith(
'thumbnailIdOne', 'thumbnailIdOne',
ThumbnailFormat.Jpeg, ThumbnailFormat.Jpeg,
undefined,
{ responseType: 'blob' } { responseType: 'blob' }
); );
expect(createObjectURLMock).toHaveBeenCalledWith(thumbnailBlob); expect(createObjectURLMock).toHaveBeenCalledWith(thumbnailBlob);

View File

@ -34,9 +34,14 @@
return; return;
} }
const { data } = await api.assetApi.getAssetThumbnail(thubmnailId, ThumbnailFormat.Jpeg, { const { data } = await api.assetApi.getAssetThumbnail(
responseType: 'blob' thubmnailId,
}); ThumbnailFormat.Jpeg,
undefined,
{
responseType: 'blob'
}
);
if (data instanceof Blob) { if (data instanceof Blob) {
return URL.createObjectURL(data); return URL.createObjectURL(data);

View File

@ -170,11 +170,7 @@
{ {
assetIds: assets.map((a) => a.id) assetIds: assets.map((a) => a.id)
}, },
{ sharedLink?.key
params: {
key: sharedLink?.key
}
}
); );
if (data.album) { if (data.album) {
@ -269,10 +265,8 @@
const { data, status, headers } = await api.albumApi.downloadArchive( const { data, status, headers } = await api.albumApi.downloadArchive(
album.id, album.id,
skip || undefined, skip || undefined,
sharedLink?.key,
{ {
params: {
key: sharedLink?.key
},
responseType: 'blob', responseType: 'blob',
onDownloadProgress: function (progressEvent) { onDownloadProgress: function (progressEvent) {
const request = this as XMLHttpRequest; const request = this as XMLHttpRequest;

View File

@ -145,8 +145,7 @@
$downloadAssets[imageFileName] = 0; $downloadAssets[imageFileName] = 0;
const { data, status } = await api.assetApi.downloadFile(assetId, { const { data, status } = await api.assetApi.downloadFile(assetId, key, {
params: { key },
responseType: 'blob', responseType: 'blob',
onDownloadProgress: (progressEvent) => { onDownloadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) { if (progressEvent.lengthComputable) {

View File

@ -26,10 +26,7 @@
const loadAssetData = async () => { const loadAssetData = async () => {
try { try {
const { data } = await api.assetApi.serveFile(asset.id, false, true, { const { data } = await api.assetApi.serveFile(asset.id, false, true, publicSharedKey, {
params: {
key: publicSharedKey
},
responseType: 'blob' responseType: 'blob'
}); });

View File

@ -54,11 +54,7 @@
{ {
assetIds assetIds
}, },
{ sharedLink?.key
params: {
key: sharedLink?.key
}
}
); );
notificationController.show({ notificationController.show({
@ -76,11 +72,7 @@
{ {
assetIds: assets.filter((a) => !selectedAssets.has(a)).map((a) => a.id) assetIds: assets.filter((a) => !selectedAssets.has(a)).map((a) => a.id)
}, },
{ sharedLink?.key
params: {
key: sharedLink?.key
}
}
); );
assets = assets.filter((a) => !selectedAssets.has(a)); assets = assets.filter((a) => !selectedAssets.has(a));

View File

@ -11,9 +11,14 @@
return noThumbnailUrl; return noThumbnailUrl;
} }
const { data } = await api.assetApi.getAssetThumbnail(thubmnailId, ThumbnailFormat.Webp, { const { data } = await api.assetApi.getAssetThumbnail(
responseType: 'blob' thubmnailId,
}); ThumbnailFormat.Webp,
undefined,
{
responseType: 'blob'
}
);
if (data instanceof Blob) { if (data instanceof Blob) {
return URL.createObjectURL(data); return URL.createObjectURL(data);
} }

View File

@ -18,19 +18,17 @@ export const addAssetsToAlbum = async (
assetIds: Array<string>, assetIds: Array<string>,
key: string | undefined = undefined key: string | undefined = undefined
): Promise<AddAssetsResponseDto> => ): Promise<AddAssetsResponseDto> =>
api.albumApi api.albumApi.addAssetsToAlbum(albumId, { assetIds }, key).then(({ data: dto }) => {
.addAssetsToAlbum(albumId, { assetIds }, { params: { key } }) if (dto.successfullyAdded > 0) {
.then(({ data: dto }) => { // This might be 0 if the user tries to add an asset that is already in the album
if (dto.successfullyAdded > 0) { notificationController.show({
// This might be 0 if the user tries to add an asset that is already in the album message: `Added ${dto.successfullyAdded} to ${dto.album?.albumName}`,
notificationController.show({ type: NotificationType.Info
message: `Added ${dto.successfullyAdded} to ${dto.album?.albumName}`, });
type: NotificationType.Info }
});
}
return dto; return dto;
}); });
export async function bulkDownload( export async function bulkDownload(
fileName: string, fileName: string,
@ -53,24 +51,20 @@ export async function bulkDownload(
let total = 0; let total = 0;
const { data, status, headers } = await api.assetApi.downloadFiles( const { data, status, headers } = await api.assetApi.downloadFiles({ assetIds }, key, {
{ assetIds }, responseType: 'blob',
{ onDownloadProgress: function (progressEvent) {
params: { key }, const request = this as XMLHttpRequest;
responseType: 'blob', if (!total) {
onDownloadProgress: function (progressEvent) { total = Number(request.getResponseHeader('X-Immich-Content-Length-Hint')) || 0;
const request = this as XMLHttpRequest; }
if (!total) {
total = Number(request.getResponseHeader('X-Immich-Content-Length-Hint')) || 0;
}
if (total) { if (total) {
const current = progressEvent.loaded; const current = progressEvent.loaded;
downloadAssets.set({ [downloadFileName]: Math.floor((current / total) * 100) }); downloadAssets.set({ [downloadFileName]: Math.floor((current / total) * 100) });
}
} }
} }
); });
const isNotComplete = headers['x-immich-archive-complete'] === 'false'; const isNotComplete = headers['x-immich-archive-complete'] === 'false';
const fileCount = Number(headers['x-immich-archive-file-count']) || 0; const fileCount = Number(headers['x-immich-archive-file-count']) || 0;

View File

@ -108,11 +108,7 @@ async function fileUploader(
deviceAssetId: String(deviceAssetId), deviceAssetId: String(deviceAssetId),
deviceId: 'WEB' deviceId: 'WEB'
}, },
{ sharedKey
params: {
key: sharedKey
}
}
); );
if (status === 200 && data.isExist && data.id) { if (status === 200 && data.isExist && data.id) {

View File

@ -12,7 +12,7 @@ export const load: PageServerLoad = async ({ params, parent }) => {
const { key } = params; const { key } = params;
try { try {
const { data: sharedLink } = await api.shareApi.getMySharedLink({ params: { key } }); const { data: sharedLink } = await api.shareApi.getMySharedLink(key);
const assetCount = sharedLink.assets.length; const assetCount = sharedLink.assets.length;
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id; const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;

View File

@ -7,9 +7,7 @@ import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => { export const load: PageServerLoad = async ({ params }) => {
try { try {
const { key, assetId } = params; const { key, assetId } = params;
const { data: asset } = await api.assetApi.getAssetById(assetId, { const { data: asset } = await api.assetApi.getAssetById(assetId, key);
params: { key }
});
if (!asset) { if (!asset) {
return error(404, 'Asset not found'); return error(404, 'Asset not found');