115 lines
3.8 KiB
JavaScript
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 };
|