immich/server/src/utils/file.ts

87 lines
2.9 KiB
TypeScript
Raw Normal View History

2024-03-21 08:07:47 -05:00
import { HttpException, StreamableFile } from '@nestjs/common';
import { NextFunction, Response } from 'express';
import { access, constants } from 'node:fs/promises';
import { basename, extname } from 'node:path';
2024-03-21 08:07:47 -05:00
import { promisify } from 'node:util';
2024-09-27 10:28:42 -04:00
import { CacheControl } from 'src/enum';
2025-02-10 18:47:42 -05:00
import { LoggingRepository } from 'src/repositories/logging.repository';
2025-02-11 14:08:13 -05:00
import { ImmichReadStream } from 'src/repositories/storage.repository';
2024-03-21 08:07:47 -05:00
import { isConnectionAborted } from 'src/utils/misc';
2024-03-20 22:15:09 -05:00
export function getFileNameWithoutExtension(path: string): string {
return basename(path, extname(path));
}
export function getFilenameExtension(path: string): string {
return extname(path);
}
2024-03-20 22:15:09 -05:00
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;
2024-03-20 22:15:09 -05:00
constructor(response: ImmichFileResponse) {
Object.assign(this, response);
}
}
2024-03-21 08:07:47 -05:00
type SendFile = Parameters<Response['sendFile']>;
type SendFileOptions = SendFile[1];
const cacheControlHeaders: Record<CacheControl, string | null> = {
2025-07-15 14:50:13 -04:00
[CacheControl.PrivateWithCache]: 'private, max-age=86400, no-transform',
[CacheControl.PrivateWithoutCache]: 'private, no-cache, no-transform',
[CacheControl.None]: null, // falsy value to prevent adding Cache-Control header
};
2024-03-21 08:07:47 -05:00
export const sendFile = async (
res: Response,
next: NextFunction,
handler: () => Promise<ImmichFileResponse>,
2025-02-10 18:47:42 -05:00
logger: LoggingRepository,
2024-03-21 08:07:47 -05:00
): Promise<void> => {
// promisified version of 'res.sendFile' for cleaner async handling
2024-03-21 08:07:47 -05:00
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);
2024-03-21 08:07:47 -05:00
}
res.header('Content-Type', file.contentType);
if (file.fileName) {
res.header('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(file.fileName)}`);
}
2024-03-21 08:07:47 -05:00
await access(file.path, constants.R_OK);
return await _sendFile(file.path, { dotfiles: 'allow' });
2024-03-21 08:07:47 -05:00
} catch (error: Error | any) {
// ignore client-closed connection
if (isConnectionAborted(error) || res.headersSent) {
2024-03-21 08:07:47 -05:00
return;
}
// log non-http errors
if (error instanceof HttpException === false) {
logger.error(`Unable to send file: ${error}`, error.stack);
2024-03-21 08:07:47 -05:00
}
res.header('Cache-Control', 'none');
next(error);
}
};
export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => {
return new StreamableFile(stream, { type, length });
};