summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/file/FileServerUtils.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend/src/server/file/FileServerUtils.ts')
-rw-r--r--packages/backend/src/server/file/FileServerUtils.ts107
1 files changed, 107 insertions, 0 deletions
diff --git a/packages/backend/src/server/file/FileServerUtils.ts b/packages/backend/src/server/file/FileServerUtils.ts
new file mode 100644
index 0000000000..c5995a2cca
--- /dev/null
+++ b/packages/backend/src/server/file/FileServerUtils.ts
@@ -0,0 +1,107 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as fs from 'node:fs';
+import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
+import { contentDisposition } from '@/misc/content-disposition.js';
+import type { IImageStreamable } from '@/core/ImageProcessingService.js';
+import type { FastifyReply } from 'fastify';
+
+export type RangeStream = {
+ stream: fs.ReadStream;
+ start: number;
+ end: number;
+ chunksize: number;
+};
+
+/**
+ * Range リクエストに対応したストリームを作成する
+ */
+export function createRangeStream(rangeHeader: string, size: number, path: string): RangeStream {
+ const parts = rangeHeader.replace(/bytes=/, '').split('-');
+ const start = parseInt(parts[0], 10);
+ let end = parts[1] ? parseInt(parts[1], 10) : size - 1;
+ if (end > size) {
+ end = size - 1;
+ }
+ const chunksize = end - start + 1;
+
+ return {
+ stream: fs.createReadStream(path, { start, end }),
+ start,
+ end,
+ chunksize,
+ };
+}
+
+/**
+ * ストリームにcleanupハンドラを設定する
+ * ストリームでない場合は即座にcleanupを実行する
+ */
+export function attachStreamCleanup(data: IImageStreamable['data'], cleanup: () => void): void {
+ if ('pipe' in data && typeof data.pipe === 'function') {
+ data.on('end', cleanup);
+ data.on('close', cleanup);
+ } else {
+ cleanup();
+ }
+}
+
+/**
+ * MIME タイプがブラウザセーフかどうかに応じて Content-Type を返す
+ */
+export function getSafeContentType(mime: string): string {
+ return FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : 'application/octet-stream';
+}
+
+/**
+ * Range リクエストを処理してストリームを返す
+ * Range ヘッダーがない場合は通常のストリームを返す
+ */
+export function handleRangeRequest(
+ reply: FastifyReply,
+ rangeHeader: string | undefined,
+ size: number,
+ path: string,
+): fs.ReadStream {
+ if (rangeHeader && size > 0) {
+ const { stream, start, end, chunksize } = createRangeStream(rangeHeader, size, path);
+ reply.header('Content-Range', `bytes ${start}-${end}/${size}`);
+ reply.header('Accept-Ranges', 'bytes');
+ reply.header('Content-Length', chunksize);
+ reply.code(206);
+ return stream;
+ }
+ return fs.createReadStream(path);
+}
+
+export type FileResponseOptions = {
+ mime: string;
+ filename: string;
+ size?: number;
+ cacheControl?: string;
+};
+
+/**
+ * ファイルレスポンス用の共通ヘッダーを設定する
+ */
+export function setFileResponseHeaders(
+ reply: FastifyReply,
+ options: FileResponseOptions,
+): void {
+ reply.header('Content-Type', getSafeContentType(options.mime));
+ reply.header('Cache-Control', options.cacheControl ?? 'max-age=31536000, immutable');
+ reply.header('Content-Disposition', contentDisposition('inline', options.filename));
+ if (options.size !== undefined) {
+ reply.header('Content-Length', options.size);
+ }
+}
+
+/**
+ * cleanup が必要なファイルかどうかを判定する型ガード
+ */
+export function needsCleanup<T extends { kind?: string; cleanup?: () => void }>(file: T): file is T & { cleanup: () => void } {
+ return 'cleanup' in file && typeof file.cleanup === 'function';
+}