first commit

This commit is contained in:
luoyangwei
2026-06-06 00:49:07 +08:00
commit 2b860d201e
134 changed files with 22773 additions and 0 deletions
+219
View File
@@ -0,0 +1,219 @@
import { createHmac } from "node:crypto";
import { extname } from "node:path";
type OssConfig = {
accessKeyId: string;
accessKeySecret: string;
bucket: string;
endpoint: string;
publicBaseUrl: string | null;
};
export type OssUploadResult =
| {
ok: true;
bucket: string;
objectKey: string;
url: string;
}
| {
ok: false;
error: string;
};
function getOssConfig(): OssConfig | null {
const accessKeyId = process.env.OSS_ACCESS_KEY_ID;
const accessKeySecret = process.env.OSS_ACCESS_KEY_SECRET;
const bucket = process.env.OSS_BUCKET;
const endpoint = getOssEndpoint();
if (!accessKeyId || !accessKeySecret || !bucket || !endpoint) {
return null;
}
return {
accessKeyId,
accessKeySecret,
bucket,
endpoint,
publicBaseUrl:
process.env.OSS_PUBLIC_BASE_URL ??
process.env.OSS_PUBLIC_URL ??
null,
};
}
function getOssEndpoint() {
const endpoint = process.env.OSS_ENDPOINT?.trim();
if (endpoint) {
return endpoint.startsWith("http") ? endpoint : `https://${endpoint}`;
}
const region = process.env.OSS_REGION?.trim();
if (!region) {
return null;
}
if (region.startsWith("http")) {
return region;
}
return region.startsWith("oss-")
? `https://${region}.aliyuncs.com`
: `https://oss-${region}.aliyuncs.com`;
}
function contentTypeToExtension(contentType: string) {
if (contentType.includes("png")) {
return "png";
}
if (contentType.includes("webp")) {
return "webp";
}
if (contentType.includes("gif")) {
return "gif";
}
if (contentType.includes("avif")) {
return "avif";
}
return "jpg";
}
function extensionFromUrl(url: string, contentType: string) {
const pathname = new URL(url).pathname;
const extension = extname(pathname).replace(".", "").toLowerCase();
if (extension && /^[a-z0-9]{2,5}$/.test(extension)) {
return extension === "jpeg" ? "jpg" : extension;
}
return contentTypeToExtension(contentType);
}
function normalizeBaseUrl(value: string) {
return value.replace(/\/+$/, "");
}
function getBucketHost(config: OssConfig) {
const endpointUrl = new URL(config.endpoint);
return `${config.bucket}.${endpointUrl.host}`;
}
function getErrorMessage(error: unknown) {
return error instanceof Error ? error.message : "Unknown OSS error.";
}
function signPutObject({
config,
contentType,
date,
objectKey,
}: {
config: OssConfig;
contentType: string;
date: string;
objectKey: string;
}) {
const canonicalizedResource = `/${config.bucket}/${objectKey}`;
const stringToSign = [
"PUT",
"",
contentType,
date,
canonicalizedResource,
].join("\n");
const signature = createHmac("sha1", config.accessKeySecret)
.update(stringToSign)
.digest("base64");
return `OSS ${config.accessKeyId}:${signature}`;
}
export async function uploadRemoteImageToAliyunOss({
imageUrl,
objectKeyPrefix,
}: {
imageUrl: string;
objectKeyPrefix: string;
}): Promise<OssUploadResult> {
try {
const config = getOssConfig();
if (!config) {
return {
ok: false,
error: "Aliyun OSS is not configured.",
};
}
const imageResponse = await fetch(imageUrl, {
headers: {
accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
"user-agent": "WalloraGalleryMirror/1.0",
},
cache: "no-store",
}).catch((error: unknown) => {
throw new Error(`Image download failed: ${getErrorMessage(error)}`);
});
if (!imageResponse.ok) {
return {
ok: false,
error: `Image download failed with ${imageResponse.status}.`,
};
}
const contentType =
imageResponse.headers.get("content-type") ?? "image/jpeg";
const extension = extensionFromUrl(imageUrl, contentType);
const objectKey = `${objectKeyPrefix}.${extension}`;
const body = Buffer.from(await imageResponse.arrayBuffer());
const date = new Date().toUTCString();
const bucketHost = getBucketHost(config);
const protocol = new URL(config.endpoint).protocol;
const putUrl = `${protocol}//${bucketHost}/${objectKey}`;
const uploadResponse = await fetch(putUrl, {
method: "PUT",
headers: {
authorization: signPutObject({
config,
contentType,
date,
objectKey,
}),
"content-type": contentType,
date,
},
body,
}).catch((error: unknown) => {
throw new Error(`OSS upload failed: ${getErrorMessage(error)}`);
});
if (!uploadResponse.ok) {
const detail = await uploadResponse.text();
return {
ok: false,
error: `OSS upload failed with ${uploadResponse.status}: ${detail.slice(0, 240)}`,
};
}
return {
ok: true,
bucket: config.bucket,
objectKey,
url: config.publicBaseUrl
? `${normalizeBaseUrl(config.publicBaseUrl)}/${objectKey}`
: putUrl,
};
} catch (error) {
return {
ok: false,
error: getErrorMessage(error),
};
}
}