mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
refactor: asset v1, app.utils (#8152)
This commit is contained in:
parent
87ccba7f9d
commit
382b63954c
34 changed files with 518 additions and 548 deletions
|
|
@ -1,4 +1,11 @@
|
|||
import { basename, extname } from 'node:path';
|
||||
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';
|
||||
import { ImmichReadStream } from 'src/interfaces/storage.interface';
|
||||
import { ImmichLogger } from 'src/utils/logger';
|
||||
import { isConnectionAborted } from 'src/utils/misc';
|
||||
|
||||
export function getFileNameWithoutExtension(path: string): string {
|
||||
return basename(path, extname(path));
|
||||
|
|
@ -23,3 +30,59 @@ export class ImmichFileResponse {
|
|||
Object.assign(this, response);
|
||||
}
|
||||
}
|
||||
type SendFile = Parameters<Response['sendFile']>;
|
||||
type SendFileOptions = SendFile[1];
|
||||
|
||||
const logger = new ImmichLogger('SendFile');
|
||||
|
||||
export const sendFile = async (
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
handler: () => Promise<ImmichFileResponse>,
|
||||
): 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);
|
||||
|
||||
const options: SendFileOptions = { dotfiles: 'allow' };
|
||||
if (!isAbsolute(file.path)) {
|
||||
options.root = process.cwd();
|
||||
}
|
||||
|
||||
await access(file.path, constants.R_OK);
|
||||
|
||||
return _sendFile(file.path, options);
|
||||
} catch (error: Error | any) {
|
||||
// ignore client-closed connection
|
||||
if (isConnectionAborted(error)) {
|
||||
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 });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ import { snakeCase, startCase } from 'lodash';
|
|||
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
|
||||
import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import { excludePaths } from 'src/config';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { excludePaths, serverVersion } from 'src/constants';
|
||||
import { DecorateAll } from 'src/decorators';
|
||||
|
||||
let metricsEnabled = process.env.IMMICH_METRICS === 'true';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,23 @@
|
|||
import { CLIP_MODEL_INFO } from 'src/constants';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import {
|
||||
DocumentBuilder,
|
||||
OpenAPIObject,
|
||||
SwaggerCustomOptions,
|
||||
SwaggerDocumentOptions,
|
||||
SwaggerModule,
|
||||
} from '@nestjs/swagger';
|
||||
import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
|
||||
import _ from 'lodash';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
CLIP_MODEL_INFO,
|
||||
IMMICH_ACCESS_COOKIE,
|
||||
IMMICH_API_KEY_HEADER,
|
||||
IMMICH_API_KEY_NAME,
|
||||
serverVersion,
|
||||
} from 'src/constants';
|
||||
import { Metadata } from 'src/middleware/auth.guard';
|
||||
import { ImmichLogger } from 'src/utils/logger';
|
||||
|
||||
export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED';
|
||||
|
|
@ -30,3 +49,130 @@ export function getCLIPModelInfo(modelName: string) {
|
|||
|
||||
return modelInfo;
|
||||
}
|
||||
|
||||
function sortKeys<T>(target: T): T {
|
||||
if (!target || typeof target !== 'object' || Array.isArray(target)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
const result: Partial<T> = {};
|
||||
const keys = Object.keys(target).sort() as Array<keyof T>;
|
||||
for (const key of keys) {
|
||||
result[key] = sortKeys(target[key]);
|
||||
}
|
||||
return result as T;
|
||||
}
|
||||
|
||||
export const routeToErrorMessage = (methodName: string) =>
|
||||
'Failed to ' + methodName.replaceAll(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`);
|
||||
|
||||
const patchOpenAPI = (document: OpenAPIObject) => {
|
||||
document.paths = sortKeys(document.paths);
|
||||
|
||||
if (document.components?.schemas) {
|
||||
const schemas = document.components.schemas as Record<string, SchemaObject>;
|
||||
|
||||
document.components.schemas = sortKeys(schemas);
|
||||
|
||||
for (const schema of Object.values(schemas)) {
|
||||
if (schema.properties) {
|
||||
schema.properties = sortKeys(schema.properties);
|
||||
}
|
||||
|
||||
if (schema.required) {
|
||||
schema.required = schema.required.sort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(document.paths)) {
|
||||
const newKey = key.replace('/api/', '/');
|
||||
delete document.paths[key];
|
||||
document.paths[newKey] = value;
|
||||
}
|
||||
|
||||
for (const path of Object.values(document.paths)) {
|
||||
const operations = {
|
||||
get: path.get,
|
||||
put: path.put,
|
||||
post: path.post,
|
||||
delete: path.delete,
|
||||
options: path.options,
|
||||
head: path.head,
|
||||
patch: path.patch,
|
||||
trace: path.trace,
|
||||
};
|
||||
|
||||
for (const operation of Object.values(operations)) {
|
||||
if (!operation) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((operation.security || []).some((item) => !!item[Metadata.PUBLIC_SECURITY])) {
|
||||
delete operation.security;
|
||||
}
|
||||
|
||||
if (operation.summary === '') {
|
||||
delete operation.summary;
|
||||
}
|
||||
|
||||
if (operation.operationId) {
|
||||
// console.log(`${routeToErrorMessage(operation.operationId).padEnd(40)} (${operation.operationId})`);
|
||||
}
|
||||
|
||||
if (operation.description === '') {
|
||||
delete operation.description;
|
||||
}
|
||||
|
||||
if (operation.parameters) {
|
||||
operation.parameters = _.orderBy(operation.parameters, 'name');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return document;
|
||||
};
|
||||
|
||||
export const useSwagger = (app: INestApplication, isDevelopment: boolean) => {
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Immich')
|
||||
.setDescription('Immich API')
|
||||
.setVersion(serverVersion.toString())
|
||||
.addBearerAuth({
|
||||
type: 'http',
|
||||
scheme: 'Bearer',
|
||||
in: 'header',
|
||||
})
|
||||
.addCookieAuth(IMMICH_ACCESS_COOKIE)
|
||||
.addApiKey(
|
||||
{
|
||||
type: 'apiKey',
|
||||
in: 'header',
|
||||
name: IMMICH_API_KEY_HEADER,
|
||||
},
|
||||
IMMICH_API_KEY_NAME,
|
||||
)
|
||||
.addServer('/api')
|
||||
.build();
|
||||
|
||||
const options: SwaggerDocumentOptions = {
|
||||
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
|
||||
};
|
||||
|
||||
const specification = SwaggerModule.createDocument(app, config, options);
|
||||
|
||||
const customOptions: SwaggerCustomOptions = {
|
||||
swaggerOptions: {
|
||||
persistAuthorization: true,
|
||||
},
|
||||
customSiteTitle: 'Immich API Documentation',
|
||||
};
|
||||
|
||||
SwaggerModule.setup('doc', app, specification, customOptions);
|
||||
|
||||
if (isDevelopment) {
|
||||
// Generate API Documentation only in development mode
|
||||
const outputPath = path.resolve(process.cwd(), '../open-api/immich-openapi-specs.json');
|
||||
writeFileSync(outputPath, JSON.stringify(patchOpenAPI(specification), null, 2), { encoding: 'utf8' });
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue