mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(mobile): use cached asset info if unchanged instead of downloading all assets (#1017)
* feat(mobile): use cached asset info if unchanged instead of downloading all assets This adds an HTTP ETag to the getAllAssets endpoint and client-side support in the app. If locally cache content is identical to the content on the server, the potentially large list of all assets does not need to be downloaded. * use ts import instead of require
This commit is contained in:
parent
efa7b3ba54
commit
47f5e4134e
18 changed files with 322 additions and 201 deletions
|
|
@ -14,6 +14,7 @@ import {
|
|||
Header,
|
||||
Put,
|
||||
UploadedFiles,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
||||
import { AssetService } from './asset.service';
|
||||
|
|
@ -21,12 +22,12 @@ import { FileFieldsInterceptor } from '@nestjs/platform-express';
|
|||
import { assetUploadOption } from '../../config/asset-upload.config';
|
||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||
import { ServeFileDto } from './dto/serve-file.dto';
|
||||
import { Response as Res } from 'express';
|
||||
import { Response as Res, Request as Req } from 'express';
|
||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
||||
import { SearchAssetDto } from './dto/search-asset.dto';
|
||||
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
||||
import { AssetResponseDto } from './response-dto/asset-response.dto';
|
||||
|
|
@ -49,6 +50,7 @@ import {
|
|||
IMMICH_ARCHIVE_FILE_COUNT,
|
||||
IMMICH_CONTENT_LENGTH_HINT,
|
||||
} from '../../constants/download.constant';
|
||||
import { etag } from '../../utils/etag';
|
||||
|
||||
@Authenticated()
|
||||
@ApiBearerAuth()
|
||||
|
|
@ -168,8 +170,28 @@ export class AssetController {
|
|||
* Get all AssetEntity belong to the user
|
||||
*/
|
||||
@Get('/')
|
||||
async getAllAssets(@GetAuthUser() authUser: AuthUserDto): Promise<AssetResponseDto[]> {
|
||||
return await this.assetService.getAllAssets(authUser);
|
||||
@ApiHeader({
|
||||
name: 'if-none-match',
|
||||
description: 'ETag of data already cached on the client',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
headers: { ETag: { required: true, schema: { type: 'string' } } },
|
||||
type: [AssetResponseDto],
|
||||
})
|
||||
async getAllAssets(@GetAuthUser() authUser: AuthUserDto, @Response() response: Res, @Request() request: Req) {
|
||||
const assets = await this.assetService.getAllAssets(authUser);
|
||||
const clientEtag = request.headers['if-none-match'];
|
||||
const json = JSON.stringify(assets);
|
||||
const serverEtag = await etag(json);
|
||||
response.setHeader('ETag', serverEtag);
|
||||
if (clientEtag === serverEtag) {
|
||||
response.status(304).end();
|
||||
} else {
|
||||
response.contentType('application/json').status(200).send(json);
|
||||
}
|
||||
}
|
||||
|
||||
@Post('/time-bucket')
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ export class AssetResponseDto {
|
|||
mimeType!: string | null;
|
||||
duration!: string;
|
||||
webpPath!: string | null;
|
||||
encodedVideoPath!: string | null;
|
||||
encodedVideoPath?: string | null;
|
||||
exifInfo?: ExifResponseDto;
|
||||
smartInfo?: SmartInfoResponseDto;
|
||||
livePhotoVideoId!: string | null;
|
||||
livePhotoVideoId?: string | null;
|
||||
}
|
||||
|
||||
export function mapAsset(entity: AssetEntity): AssetResponseDto {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export class UserResponseDto {
|
|||
profileImagePath!: string;
|
||||
shouldChangePassword!: boolean;
|
||||
isAdmin!: boolean;
|
||||
deletedAt!: Date | null;
|
||||
deletedAt?: Date;
|
||||
}
|
||||
|
||||
export function mapUser(entity: UserEntity): UserResponseDto {
|
||||
|
|
@ -22,6 +22,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
|
|||
profileImagePath: entity.profileImagePath,
|
||||
shouldChangePassword: entity.shouldChangePassword,
|
||||
isAdmin: entity.isAdmin,
|
||||
deletedAt: entity.deletedAt || null,
|
||||
deletedAt: entity.deletedAt,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
5
server/apps/immich/src/types/index.d.ts
vendored
Normal file
5
server/apps/immich/src/types/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
declare module 'crypto' {
|
||||
namespace webcrypto {
|
||||
const subtle: SubtleCrypto;
|
||||
}
|
||||
}
|
||||
10
server/apps/immich/src/utils/etag.ts
Normal file
10
server/apps/immich/src/utils/etag.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { webcrypto } from 'node:crypto';
|
||||
const { subtle } = webcrypto;
|
||||
|
||||
export async function etag(text: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(text);
|
||||
const buffer = await subtle.digest('SHA-1', data);
|
||||
const hash = Buffer.from(buffer).toString('base64').slice(0, 27);
|
||||
return `"${data.length}-${hash}"`;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue