Migrate storage to DogeCloud and expand admin dashboard

This commit is contained in:
yoyuzh
2026-04-02 12:20:50 +08:00
parent 2424fbd2a7
commit 97edc4cc32
65 changed files with 2842 additions and 380 deletions

View File

@@ -68,29 +68,179 @@ export function getFrontendSpaAliasContentType() {
return 'text/html; charset=utf-8';
}
export function createAuthorizationHeader({
function toAmzDateParts(amzDate) {
return {
amzDate,
dateStamp: amzDate.slice(0, 8),
};
}
function sha256Hex(value) {
return crypto.createHash('sha256').update(value).digest('hex');
}
function hmac(key, value, encoding) {
const digest = crypto.createHmac('sha256', key).update(value).digest();
return encoding ? digest.toString(encoding) : digest;
}
function hmacSha1Hex(key, value) {
return crypto.createHmac('sha1', key).update(value).digest('hex');
}
function buildSigningKey(secretAccessKey, dateStamp, region, service) {
const kDate = hmac(`AWS4${secretAccessKey}`, dateStamp);
const kRegion = hmac(kDate, region);
const kService = hmac(kRegion, service);
return hmac(kService, 'aws4_request');
}
function encodeQueryComponent(value) {
return encodeURIComponent(value).replace(/[!'()*]/g, (character) =>
`%${character.charCodeAt(0).toString(16).toUpperCase()}`
);
}
function toCanonicalQueryString(query) {
const params = new URLSearchParams(query);
return [...params.entries()]
.sort(([leftKey, leftValue], [rightKey, rightValue]) =>
leftKey === rightKey ? leftValue.localeCompare(rightValue) : leftKey.localeCompare(rightKey)
)
.map(([key, value]) => `${encodeQueryComponent(key)}=${encodeQueryComponent(value)}`)
.join('&');
}
export function extractDogeCloudScopeBucketName(scope) {
const separatorIndex = scope.indexOf(':');
return separatorIndex >= 0 ? scope.slice(0, separatorIndex) : scope;
}
export function createDogeCloudApiAuthorization({apiPath, body, accessKey, secretKey}) {
return `TOKEN ${accessKey}:${hmacSha1Hex(secretKey, `${apiPath}\n${body}`)}`;
}
export async function requestDogeCloudTemporaryS3Session({
apiBaseUrl = 'https://api.dogecloud.com',
accessKey,
secretKey,
scope,
ttlSeconds = 3600,
fetchImpl = fetch,
}) {
const apiPath = '/auth/tmp_token.json';
const body = JSON.stringify({
channel: 'OSS_FULL',
ttl: ttlSeconds,
scopes: [scope],
});
const response = await fetchImpl(`${apiBaseUrl.replace(/\/+$/, '')}${apiPath}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: createDogeCloudApiAuthorization({
apiPath,
body,
accessKey,
secretKey,
}),
},
body,
});
if (!response.ok) {
throw new Error(`DogeCloud tmp_token request failed: HTTP ${response.status}`);
}
const payload = await response.json();
if (payload.code !== 200) {
throw new Error(`DogeCloud tmp_token request failed: ${payload.msg || 'unknown error'}`);
}
const bucketName = extractDogeCloudScopeBucketName(scope);
const buckets = Array.isArray(payload.data?.Buckets) ? payload.data.Buckets : [];
const bucket = buckets.find((entry) => entry.name === bucketName) ?? buckets[0];
if (!bucket) {
throw new Error(`DogeCloud tmp_token response did not include a bucket for scope: ${bucketName}`);
}
return {
accessKeyId: payload.data.Credentials?.accessKeyId || '',
secretAccessKey: payload.data.Credentials?.secretAccessKey || '',
sessionToken: payload.data.Credentials?.sessionToken || '',
bucket: bucket.s3Bucket,
endpoint: bucket.s3Endpoint,
bucketName: bucket.name,
expiresAt: payload.data.ExpiredAt,
};
}
export function createAwsV4Headers({
method,
endpoint,
bucket,
objectKey,
contentType,
date,
query = '',
headers: extraHeaders = {},
amzDate = new Date().toISOString().replace(/[:-]|\.\d{3}/g, ''),
accessKeyId,
accessKeySecret,
secretAccessKey,
sessionToken,
region = 'automatic',
}) {
const stringToSign = [
const {dateStamp} = toAmzDateParts(amzDate);
const normalizedEndpoint = normalizeEndpoint(endpoint);
const host = `${bucket}.${normalizedEndpoint}`;
const canonicalUri = `/${encodeObjectKey(objectKey)}`;
const canonicalQueryString = toCanonicalQueryString(query);
const payloadHash = 'UNSIGNED-PAYLOAD';
const service = 's3';
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
const signedHeaderEntries = [
['host', host],
['x-amz-content-sha256', payloadHash],
['x-amz-date', amzDate],
];
for (const [key, value] of Object.entries(extraHeaders)) {
signedHeaderEntries.push([key.toLowerCase(), String(value).trim()]);
}
if (sessionToken) {
signedHeaderEntries.push(['x-amz-security-token', sessionToken]);
}
signedHeaderEntries.sort(([left], [right]) => left.localeCompare(right));
const signedHeaders = signedHeaderEntries.map(([key]) => key).join(';');
const canonicalHeadersText = signedHeaderEntries.map(([key, value]) => `${key}:${value}\n`).join('');
const canonicalRequest = [
method.toUpperCase(),
'',
contentType,
date,
`/${bucket}/${objectKey}`,
canonicalUri,
canonicalQueryString,
canonicalHeadersText,
signedHeaders,
payloadHash,
].join('\n');
const stringToSign = [
'AWS4-HMAC-SHA256',
amzDate,
credentialScope,
sha256Hex(canonicalRequest),
].join('\n');
const signature = hmac(buildSigningKey(secretAccessKey, dateStamp, region, service), stringToSign, 'hex');
const signature = crypto
.createHmac('sha1', accessKeySecret)
.update(stringToSign)
.digest('base64');
const resultHeaders = {
Authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
'x-amz-content-sha256': payloadHash,
'x-amz-date': amzDate,
...extraHeaders,
};
return `OSS ${accessKeyId}:${signature}`;
if (sessionToken) {
resultHeaders['x-amz-security-token'] = sessionToken;
}
return resultHeaders;
}
export function encodeObjectKey(objectKey) {