Files
omniai-server/src/ossClient.js
T
2026-06-02 13:14:10 +08:00

115 lines
3.8 KiB
JavaScript

/**
* OSS object download utility.
* Uses the same credentials as STS to sign GET requests to Aliyun OSS.
*/
const crypto = require("node:crypto");
const OSS_ACCESS_KEY_ID = process.env.STS_ACCESS_KEY_ID || "";
const OSS_ACCESS_KEY_SECRET = process.env.STS_ACCESS_KEY_SECRET || "";
const OSS_BUCKET = process.env.OSS_BUCKET || "";
const OSS_REGION = process.env.OSS_REGION || "";
function getOssEndpoint() {
return `https://${OSS_BUCKET}.${OSS_REGION}.aliyuncs.com`;
}
function isOssConfigured() {
return !!(OSS_ACCESS_KEY_ID && OSS_ACCESS_KEY_SECRET && OSS_BUCKET && OSS_REGION);
}
function buildCanonicalOssHeaders(headers = {}) {
return Object.entries(headers)
.filter(([key]) => key.toLowerCase().startsWith("x-oss-"))
.map(([key, value]) => [key.toLowerCase(), String(value).trim()])
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, value]) => `${key}:${value}\n`)
.join("");
}
function signOssRequest(method, objectKey, date, contentType = "", extraHeaders = {}) {
const contentMd5 = "";
const canonicalResource = `/${OSS_BUCKET}/${objectKey}`;
const canonicalHeaders = buildCanonicalOssHeaders(extraHeaders);
const stringToSign = `${method}\n${contentMd5}\n${contentType}\n${date}\n${canonicalHeaders}${canonicalResource}`;
const signature = crypto
.createHmac("sha1", OSS_ACCESS_KEY_SECRET)
.update(stringToSign)
.digest("base64");
return `OSS ${OSS_ACCESS_KEY_ID}:${signature}`;
}
function createSignedReadUrl(objectKey, expiresInSeconds = 24 * 60 * 60) {
if (!isOssConfigured()) {
throw new Error("OSS is not configured");
}
const expires = Math.floor(Date.now() / 1000) + expiresInSeconds;
const canonicalResource = `/${OSS_BUCKET}/${objectKey}`;
const stringToSign = `GET\n\n\n${expires}\n${canonicalResource}`;
const signature = crypto
.createHmac("sha1", OSS_ACCESS_KEY_SECRET)
.update(stringToSign)
.digest("base64");
const encodedKey = encodeURIComponent(objectKey).replace(/%2F/g, "/");
const query = new URLSearchParams({
OSSAccessKeyId: OSS_ACCESS_KEY_ID,
Expires: String(expires),
Signature: signature,
});
return `${getOssEndpoint()}/${encodedKey}?${query.toString()}`;
}
async function getObject(objectKey) {
if (!isOssConfigured()) {
throw new Error("OSS is not configured");
}
const date = new Date().toUTCString();
const authorization = signOssRequest("GET", objectKey, date);
const url = `${getOssEndpoint()}/${encodeURIComponent(objectKey).replace(/%2F/g, "/")}`;
const response = await fetch(url, {
method: "GET",
headers: { Date: date, Authorization: authorization },
});
if (!response.ok) {
const text = await response.text().catch(() => "");
const error = new Error(`OSS GET failed (${response.status}): ${text.slice(0, 200)}`);
error.status = response.status;
if (response.status === 404 && /<Code>\s*NoSuchKey\s*<\/Code>|NoSuchKey/i.test(text)) {
error.code = "oss_no_such_key";
}
throw error;
}
return response.text();
}
async function putObject(objectKey, body, contentType = "application/octet-stream", extraHeaders = {}) {
if (!isOssConfigured()) {
throw new Error("OSS is not configured");
}
const date = new Date().toUTCString();
const authorization = signOssRequest("PUT", objectKey, date, contentType, extraHeaders);
const url = `${getOssEndpoint()}/${encodeURIComponent(objectKey).replace(/%2F/g, "/")}`;
const response = await fetch(url, {
method: "PUT",
headers: { Date: date, Authorization: authorization, "Content-Type": contentType, ...extraHeaders },
body,
});
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`OSS PUT failed (${response.status}): ${text.slice(0, 200)}`);
}
return { ossKey: objectKey, url };
}
module.exports = { getObject, putObject, isOssConfigured, createSignedReadUrl };