wip
This commit is contained in:
parent
d91a4e3dec
commit
5f5aa599ba
|
@ -6,8 +6,11 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import * as fs from 'node:fs';
|
||||
import * as stream from 'node:stream/promises';
|
||||
import { Transform } from 'node:stream';
|
||||
import { type MultipartFile } from '@fastify/multipart';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { AttachmentFile } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||
|
@ -200,18 +203,6 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
return;
|
||||
}
|
||||
|
||||
const [path, cleanup] = await createTemp();
|
||||
await stream.pipeline(multipartData.file, fs.createWriteStream(path));
|
||||
|
||||
// ファイルサイズが制限を超えていた場合
|
||||
// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
|
||||
if (multipartData.file.truncated) {
|
||||
cleanup();
|
||||
reply.code(413);
|
||||
reply.send();
|
||||
return;
|
||||
}
|
||||
|
||||
const fields = {} as Record<string, unknown>;
|
||||
for (const [k, v] of Object.entries(multipartData.fields)) {
|
||||
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
|
||||
|
@ -226,10 +217,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
return;
|
||||
}
|
||||
this.authenticateService.authenticate(token).then(([user, app]) => {
|
||||
this.call(endpoint, user, app, fields, {
|
||||
name: multipartData.filename,
|
||||
path: path,
|
||||
}, request).then((res) => {
|
||||
this.call(endpoint, user, app, fields, multipartData, request).then((res) => {
|
||||
this.send(reply, res);
|
||||
}).catch((err: ApiError) => {
|
||||
this.#sendApiError(reply, err);
|
||||
|
@ -294,10 +282,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
user: MiLocalUser | null | undefined,
|
||||
token: MiAccessToken | null | undefined,
|
||||
data: any,
|
||||
file: {
|
||||
name: string;
|
||||
path: string;
|
||||
} | null,
|
||||
multipartFile: MultipartFile | null,
|
||||
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
||||
) {
|
||||
const isSecure = user != null && token == null;
|
||||
|
@ -435,18 +420,86 @@ export class ApiCallService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
let attachmentFile: AttachmentFile | null = null;
|
||||
let cleanup = () => {};
|
||||
if (ep.meta.requireFile && request.method === 'POST' && multipartFile) {
|
||||
const result = await this.handleAttachmentFile(request, multipartFile);
|
||||
attachmentFile = result.attachmentFile;
|
||||
cleanup = result.cleanup;
|
||||
}
|
||||
|
||||
// API invoking
|
||||
if (this.config.sentryForBackend) {
|
||||
return await Sentry.startSpan({
|
||||
name: 'API: ' + ep.name,
|
||||
}, () => ep.exec(data, user, token, file, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
|
||||
}, () => {
|
||||
return ep.exec(data, user, token, attachmentFile, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
|
||||
.finally(() => cleanup());
|
||||
});
|
||||
} else {
|
||||
return await ep.exec(data, user, token, file, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id));
|
||||
return await ep.exec(data, user, token, attachmentFile, request.ip, request.headers)
|
||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
|
||||
.finally(() => cleanup());
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async handleAttachmentFile(
|
||||
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
||||
multipartFile: MultipartFile,
|
||||
) {
|
||||
function createTooLongError() {
|
||||
return new ApiError({
|
||||
httpStatusCode: 413,
|
||||
kind: 'client',
|
||||
message: 'File size is too large.',
|
||||
code: 'FILE_SIZE_TOO_LARGE',
|
||||
id: 'ff827ce8-9b4b-4808-8511-422222a3362f',
|
||||
});
|
||||
}
|
||||
|
||||
function createLimitStream(limit: number) {
|
||||
let total = 0;
|
||||
|
||||
return new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
total += chunk.length;
|
||||
if (total > limit) {
|
||||
callback(createTooLongError());
|
||||
} else {
|
||||
callback(null, chunk);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const fileSizeLimit = this.config.maxFileSize;
|
||||
if (request.headers['content-length'] && Number(request.headers['content-length']) > fileSizeLimit) {
|
||||
throw createTooLongError();
|
||||
}
|
||||
|
||||
const [path, cleanup] = await createTemp();
|
||||
await stream.pipeline(multipartFile.file, createLimitStream(fileSizeLimit), fs.createWriteStream(path));
|
||||
|
||||
// ファイルサイズが制限を超えていた場合
|
||||
// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
|
||||
if (multipartFile.file.truncated) {
|
||||
cleanup();
|
||||
throw createTooLongError();
|
||||
}
|
||||
|
||||
const attachmentFile = {
|
||||
name: multipartFile.filename,
|
||||
path,
|
||||
};
|
||||
|
||||
return {
|
||||
attachmentFile,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
clearInterval(this.userIpHistoriesClearIntervalId);
|
||||
|
|
|
@ -21,23 +21,23 @@ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
|
|||
|
||||
export type Response = Record<string, any> | void;
|
||||
|
||||
type File = {
|
||||
export type AttachmentFile = {
|
||||
name: string | null;
|
||||
path: string;
|
||||
};
|
||||
|
||||
// TODO: paramsの型をT['params']のスキーマ定義から推論する
|
||||
type Executor<T extends IEndpointMeta, Ps extends Schema> =
|
||||
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
|
||||
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
|
||||
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
|
||||
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
|
||||
|
||||
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
|
||||
public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
|
||||
public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
|
||||
|
||||
constructor(meta: T, paramDef: Ps, cb: Executor<T, Ps>) {
|
||||
const validate = ajv.compile(paramDef);
|
||||
|
||||
this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
|
||||
this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record<string, string> | null) => {
|
||||
let cleanup: undefined | (() => void) = undefined;
|
||||
|
||||
if (meta.requireFile) {
|
||||
|
|
Loading…
Reference in New Issue