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, isAbsolute } from 'node:path';
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-21 01:00:46 +02:00
|
|
|
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;
|
2024-12-02 03:21:08 +08:00
|
|
|
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];
|
|
|
|
|
|
|
|
|
|
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> => {
|
|
|
|
|
const _sendFile = (path: string, options: SendFileOptions) =>
|
|
|
|
|
promisify<string, SendFileOptions>(res.sendFile).bind(res)(path, options);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const file = await handler();
|
|
|
|
|
switch (file.cacheControl) {
|
|
|
|
|
case CacheControl.PRIVATE_WITH_CACHE: {
|
|
|
|
|
res.set('Cache-Control', 'private, max-age=86400, no-transform');
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case CacheControl.PRIVATE_WITHOUT_CACHE: {
|
|
|
|
|
res.set('Cache-Control', 'private, no-cache, no-transform');
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.header('Content-Type', file.contentType);
|
2024-12-02 03:21:08 +08:00
|
|
|
if (file.fileName) {
|
2024-12-06 08:43:58 -05:00
|
|
|
res.header('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(file.fileName)}`);
|
2024-12-02 03:21:08 +08:00
|
|
|
}
|
2024-03-21 08:07:47 -05:00
|
|
|
|
|
|
|
|
const options: SendFileOptions = { dotfiles: 'allow' };
|
|
|
|
|
if (!isAbsolute(file.path)) {
|
|
|
|
|
options.root = process.cwd();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await access(file.path, constants.R_OK);
|
|
|
|
|
|
2024-06-06 10:09:42 -04:00
|
|
|
return await _sendFile(file.path, options);
|
2024-03-21 08:07:47 -05:00
|
|
|
} catch (error: Error | any) {
|
|
|
|
|
// ignore client-closed connection
|
2024-06-13 14:30:34 -04:00
|
|
|
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.name}`, error.stack);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.header('Cache-Control', 'none');
|
|
|
|
|
next(error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => {
|
|
|
|
|
return new StreamableFile(stream, { type, length });
|
|
|
|
|
};
|