Fixed 267U.pre2

This commit is contained in:
2026-07-26 18:25:37 +09:00
parent 50bfaeafdf
commit 317d00a284
1286 changed files with 80222 additions and 1 deletions
@@ -0,0 +1,207 @@
import * as fs from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import send from "koa-send";
import rename from "rename";
import { serverLogger } from "../index.js";
import { contentDisposition } from "../../misc/content-disposition.js";
import { DriveFiles } from "../../models/index.js";
import { InternalStorage } from "../../services/drive/internal-storage.js";
import { createTemp } from "../../misc/create-temp.js";
import { downloadUrl } from "../../misc/download-url.js";
import { detectType } from "../../misc/get-file-info.js";
import { convertToWebp } from "../../services/drive/image-processor.js";
import { GenerateVideoThumbnail } from "../../services/drive/generate-video-thumbnail.js";
import { StatusError } from "../../misc/fetch.js";
import { FILE_TYPE_BROWSERSAFE, MINUTE } from "../../const.js";
import { getIpHash } from "../../misc/get-ip-hash.js";
import { limiter } from "../api/limiter.js";
import authenticate from "../api/authenticate.js";
import { isIgnorableConnectionError } from "../is-ignorable-connection-error.js";
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const assets = `${_dirname}/../../server/file/assets/`;
const commonReadableHandlerGenerator = (ctx)=>(e)=>{
if (isIgnorableConnectionError(e)) return;
serverLogger.error(e);
ctx.status = 500;
ctx.set("Cache-Control", "max-age=300");
};
export default async function(ctx) {
const key = ctx.params.key;
// Fetch drive file
const file = await DriveFiles.createQueryBuilder("file").where("file.accessKey = :accessKey", {
accessKey: key
}).orWhere("file.thumbnailAccessKey = :thumbnailAccessKey", {
thumbnailAccessKey: key
}).orWhere("file.webpublicAccessKey = :webpublicAccessKey", {
webpublicAccessKey: key
}).getOne();
if (file == null) {
ctx.status = 404;
ctx.set("Cache-Control", "max-age=86400");
await send(ctx, "/dummy.png", {
root: assets
});
return;
}
ctx.set("X-Content-Type-Options", "nosniff");
const isThumbnail = file.thumbnailAccessKey === key;
const isWebpublic = file.webpublicAccessKey === key;
const requestedDownload = ctx.query.download === "1";
const requestedStream = ctx.query.stream === "1";
const isStreamableMedia = file.type.startsWith("video/") || file.type.startsWith("audio/");
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
const limitActor = getIpHash(ctx.ip);
const isMediaPlayback = isStreamableMedia && !requestedDownload;
const limit = {
key: `drive-file:${key}`,
duration: MINUTE * 10,
max: isMediaPlayback ? 600 : 10
};
await limiter(limit, limitActor).catch((e)=>{
const remainingTime = e.remainingTime ? `Please try again in ${e.remainingTime}.` : "Please try again later.";
ctx.status = 429;
ctx.body = "Rate limit exceeded. " + remainingTime;
ctx.set("Content-Type", "text/plain; charset=utf-8");
ctx.set("Cache-Control", "no-store");
});
if (ctx.status == 429) return;
const requesterId = requestedDownload ? await getRequesterId(ctx) : null;
const isOwner = requesterId != null && requesterId === file.userId;
const protectedOriginal = !isThumbnail && !isWebpublic && !file.allowDownload && !isOwner;
if (requestedDownload && protectedOriginal) {
ctx.status = 403;
ctx.body = "Download is not allowed for this file.";
ctx.set("Cache-Control", "max-age=300");
return;
}
if (!file.storedInternal) {
if (file.isLink && file.uri) {
// 期限切れリモートファイル
const [path, cleanup] = await createTemp();
try {
await downloadUrl(file.uri, path);
const { mime, ext } = await detectType(path);
const convertFile = async ()=>{
if (isThumbnail) {
if ([
"image/jpeg",
"image/webp",
"image/png",
"image/svg+xml",
"image/avif"
].includes(mime)) {
return await convertToWebp(path, 996, 560);
} else if (mime.startsWith("video/")) {
return await GenerateVideoThumbnail(path);
}
}
if (isWebpublic) {
if ([
"image/svg+xml"
].includes(mime)) {
return await convertToWebp(path, 2048, 2048, 100);
}
}
return {
data: fs.readFileSync(path),
ext,
type: mime
};
};
const image = await convertFile();
ctx.body = image.data;
ctx.set("Content-Type", FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : "application/octet-stream");
ctx.set("Cache-Control", "max-age=31536000, immutable");
} catch (e) {
if (isIgnorableConnectionError(e)) return;
serverLogger.error(`${e}`);
if (e instanceof StatusError && !e.isRetryable) {
ctx.status = e.statusCode;
ctx.set("Cache-Control", "max-age=86400");
} else {
ctx.status = 500;
ctx.set("Cache-Control", "max-age=300");
}
} finally{
cleanup();
}
return;
}
ctx.status = 204;
ctx.set("Cache-Control", "max-age=86400");
return;
}
if (isThumbnail || isWebpublic) {
const { mime, ext } = await detectType(InternalStorage.resolvePath(key));
const filename = rename(file.name, {
suffix: isThumbnail ? "-thumb" : "-web",
extname: ext ? `.${ext}` : undefined
}).toString();
ctx.body = InternalStorage.read(key);
ctx.set("Content-Type", FILE_TYPE_BROWSERSAFE.includes(mime) ? mime : "application/octet-stream");
ctx.set("Cache-Control", "max-age=31536000, immutable");
ctx.set("Content-Disposition", contentDisposition("inline", filename));
} else {
const storageKey = file.accessKey;
const disposition = requestedDownload ? "attachment" : "inline";
ctx.set("Content-Type", isStreamableMedia || FILE_TYPE_BROWSERSAFE.includes(file.type) ? file.type : "application/octet-stream");
ctx.set("Cache-Control", "max-age=31536000, immutable");
ctx.set("Content-Disposition", contentDisposition(disposition, file.name));
if (!requestedDownload && isStreamableMedia) {
const servedRange = await sendRange(ctx, storageKey);
if (servedRange) return;
}
const readable = InternalStorage.read(storageKey);
readable.on("error", commonReadableHandlerGenerator(ctx));
ctx.body = readable;
}
}
async function getRequesterId(ctx) {
const queryToken = typeof ctx.query.i === "string" ? ctx.query.i : null;
try {
const [user] = await authenticate(ctx.get("authorization") || null, queryToken, true);
return user?.id ?? null;
} catch {
return null;
}
}
async function sendRange(ctx, key) {
const range = ctx.get("range");
if (!range) {
ctx.set("Accept-Ranges", "bytes");
return false;
}
const path = InternalStorage.resolvePath(key);
const stat = await fs.promises.stat(path);
const match = /^bytes=(\d*)-(\d*)$/.exec(range);
if (!match) {
ctx.status = 416;
ctx.set("Content-Range", `bytes */${stat.size}`);
return true;
}
let start = match[1] === "" ? 0 : Number(match[1]);
let end = match[2] === "" ? stat.size - 1 : Number(match[2]);
if (match[1] === "" && match[2] !== "") {
const suffixLength = Number(match[2]);
start = Math.max(stat.size - suffixLength, 0);
end = stat.size - 1;
}
if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || start < 0 || end >= stat.size) {
ctx.status = 416;
ctx.set("Content-Range", `bytes */${stat.size}`);
return true;
}
ctx.status = 206;
ctx.set("Accept-Ranges", "bytes");
ctx.set("Content-Range", `bytes ${start}-${end}/${stat.size}`);
ctx.set("Content-Length", String(end - start + 1));
const readable = fs.createReadStream(path, {
start,
end
});
readable.on("error", commonReadableHandlerGenerator(ctx));
ctx.body = readable;
return true;
}