mirror of
https://github.com/immich-app/immich.git
synced 2026-02-21 16:20:34 +09:00
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docker / pre-job (push) Has been cancelled
Docker / Re-Tag ML () (push) Has been cancelled
Docker / Re-Tag ML (-armnn) (push) Has been cancelled
Docker / Re-Tag ML (-cuda) (push) Has been cancelled
Docker / Re-Tag ML (-openvino) (push) Has been cancelled
Docker / Re-Tag ML (-rknn) (push) Has been cancelled
Docker / Re-Tag ML (-rocm) (push) Has been cancelled
Docker / Re-Tag Server () (push) Has been cancelled
Docker / Build and Push ML (armnn, linux/arm64, -armnn) (push) Has been cancelled
Docker / Build and Push ML (cpu) (push) Has been cancelled
Docker / Build and Push ML (cuda, linux/amd64, -cuda) (push) Has been cancelled
Docker / Build and Push ML (openvino, linux/amd64, -openvino) (push) Has been cancelled
Docker / Build and Push ML (rknn, linux/arm64, -rknn) (push) Has been cancelled
Docker / Build and Push ML (rocm, linux/amd64, {"linux/amd64": "mich"}, -rocm) (push) Has been cancelled
Docker / Build and Push Server (push) Has been cancelled
Docker / Docker Build & Push Server Success (push) Has been cancelled
Docker / Docker Build & Push ML Success (push) Has been cancelled
Docs build / pre-job (push) Has been cancelled
Docs build / Docs Build (push) Has been cancelled
Zizmor / Zizmor (push) Has been cancelled
Manage release PR / bump (push) Has been cancelled
Static Code Analysis / pre-job (push) Has been cancelled
Static Code Analysis / Run Dart Code Analysis (push) Has been cancelled
Test / pre-job (push) Has been cancelled
Test / Test & Lint Server (push) Has been cancelled
Test / Unit Test CLI (push) Has been cancelled
Test / Unit Test CLI (Windows) (push) Has been cancelled
Test / Lint Web (push) Has been cancelled
Test / Test Web (push) Has been cancelled
Test / Test i18n (push) Has been cancelled
Test / End-to-End Lint (push) Has been cancelled
Test / Medium Tests (Server) (push) Has been cancelled
Test / End-to-End Tests (Server & CLI) (ubuntu-24.04-arm) (push) Has been cancelled
Test / End-to-End Tests (Server & CLI) (ubuntu-latest) (push) Has been cancelled
Test / End-to-End Tests (Web) (ubuntu-24.04-arm) (push) Has been cancelled
Test / End-to-End Tests (Web) (ubuntu-latest) (push) Has been cancelled
Test / End-to-End Tests Success (push) Has been cancelled
Test / Unit Test Mobile (push) Has been cancelled
Test / Unit Test ML (push) Has been cancelled
Test / .github Files Formatting (push) Has been cancelled
Test / ShellCheck (push) Has been cancelled
Test / OpenAPI Clients (push) Has been cancelled
Test / SQL Schema Checks (push) Has been cancelled
* platform clients * uppercase http method * fix hot reload * custom user agent * init before app launch * set defaults * move to bootstrap * unrelated change * disable disk cache by default * optimized decoding * remove incremental * android impl * memory optimization * lock approach is slower on ios * conditional cronet * clarify parameter * enable disk cache * set user agent * flutter-side decode * optimized http * fixed locking * refactor * potential race conditions * embedded cronet * refactor, fix capacity handling * fast path for known content length * ios optimizations * re-enable cache * formatting * bump concurrency * clear cache button * fix eviction race condition * add extra cancellation check * tighten dispose * better error handling * fix disposal --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
import { HttpException, StreamableFile } from '@nestjs/common';
|
|
import { NextFunction, Response } from 'express';
|
|
import { access, constants } from 'node:fs/promises';
|
|
import { basename, extname } from 'node:path';
|
|
import { promisify } from 'node:util';
|
|
import { CacheControl } from 'src/enum';
|
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
|
import { ImmichReadStream } from 'src/repositories/storage.repository';
|
|
import { isConnectionAborted } from 'src/utils/misc';
|
|
|
|
export function getFileNameWithoutExtension(path: string): string {
|
|
return basename(path, extname(path));
|
|
}
|
|
|
|
export function getFilenameExtension(path: string): string {
|
|
return extname(path);
|
|
}
|
|
|
|
export function getLivePhotoMotionFilename(stillName: string, motionName: string) {
|
|
return getFileNameWithoutExtension(stillName) + extname(motionName);
|
|
}
|
|
|
|
export class ImmichFileResponse {
|
|
public readonly path!: string;
|
|
public readonly contentType!: string;
|
|
public readonly cacheControl!: CacheControl;
|
|
public readonly fileName?: string;
|
|
|
|
constructor(response: ImmichFileResponse) {
|
|
Object.assign(this, response);
|
|
}
|
|
}
|
|
type SendFile = Parameters<Response['sendFile']>;
|
|
type SendFileOptions = SendFile[1];
|
|
|
|
const cacheControlHeaders: Record<CacheControl, string | null> = {
|
|
[CacheControl.PrivateWithCache]:
|
|
'private, max-age=86400, no-transform, stale-while-revalidate=2592000, stale-if-error=2592000',
|
|
[CacheControl.PrivateWithoutCache]: 'private, no-cache, no-transform',
|
|
[CacheControl.None]: null, // falsy value to prevent adding Cache-Control header
|
|
};
|
|
|
|
export const sendFile = async (
|
|
res: Response,
|
|
next: NextFunction,
|
|
handler: () => Promise<ImmichFileResponse> | ImmichFileResponse,
|
|
logger: LoggingRepository,
|
|
): Promise<void> => {
|
|
// promisified version of 'res.sendFile' for cleaner async handling
|
|
const _sendFile = (path: string, options: SendFileOptions) =>
|
|
promisify<string, SendFileOptions>(res.sendFile).bind(res)(path, options);
|
|
|
|
try {
|
|
const file = await handler();
|
|
const cacheControlHeader = cacheControlHeaders[file.cacheControl];
|
|
if (cacheControlHeader) {
|
|
// set the header to Cache-Control
|
|
res.set('Cache-Control', cacheControlHeader);
|
|
}
|
|
|
|
res.header('Content-Type', file.contentType);
|
|
if (file.fileName) {
|
|
res.header('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(file.fileName)}`);
|
|
}
|
|
|
|
await access(file.path, constants.R_OK);
|
|
|
|
return await _sendFile(file.path, { dotfiles: 'allow' });
|
|
} catch (error: Error | any) {
|
|
// ignore client-closed connection
|
|
if (isConnectionAborted(error) || res.headersSent) {
|
|
return;
|
|
}
|
|
|
|
// log non-http errors
|
|
if (error instanceof HttpException === false) {
|
|
logger.error(`Unable to send file: ${error}`, error.stack);
|
|
}
|
|
|
|
res.header('Cache-Control', 'none');
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => {
|
|
return new StreamableFile(stream, { type, length });
|
|
};
|