fixed db scripts
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import type { S3ImageStorageConfig } from './imageStorageConfig.js';
|
||||
|
||||
const awsDate = (date: Date) => date.toISOString().replace(/[:-]|\.\d{3}/g, '');
|
||||
const shortDate = (date: Date) => date.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
|
||||
const hmac = (key: crypto.BinaryLike, value: string) => crypto.createHmac('sha256', key).update(value).digest();
|
||||
const sha256Hex = (value: crypto.BinaryLike) => crypto.createHash('sha256').update(value).digest('hex');
|
||||
|
||||
const encodeObjectKey = (key: string) => key.split('/').map(encodeURIComponent).join('/');
|
||||
const encodeQueryValue = (value: string) => encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
||||
|
||||
const getSigningKey = (secretAccessKey: string, date: string, region: string) => {
|
||||
const dateKey = hmac(`AWS4${secretAccessKey}`, date);
|
||||
const regionKey = hmac(dateKey, region);
|
||||
const serviceKey = hmac(regionKey, 's3');
|
||||
return hmac(serviceKey, 'aws4_request');
|
||||
};
|
||||
|
||||
const buildObjectUrl = (config: S3ImageStorageConfig, objectKey: string) => {
|
||||
const endpoint = config.endpoint.replace(/\/+$/, '');
|
||||
return new URL(`${endpoint}/${encodeURIComponent(config.bucket)}/${encodeObjectKey(objectKey)}`);
|
||||
};
|
||||
|
||||
const signS3Request = ({
|
||||
config,
|
||||
method,
|
||||
objectKey,
|
||||
contentHash,
|
||||
contentType,
|
||||
now = new Date(),
|
||||
}: {
|
||||
config: S3ImageStorageConfig;
|
||||
method: 'DELETE' | 'PUT';
|
||||
objectKey: string;
|
||||
contentHash: string;
|
||||
contentType?: string;
|
||||
now?: Date;
|
||||
}) => {
|
||||
const url = buildObjectUrl(config, objectKey);
|
||||
const amzDate = awsDate(now);
|
||||
const date = shortDate(now);
|
||||
const credentialScope = `${date}/${config.region}/s3/aws4_request`;
|
||||
const headers: Record<string, string> = {
|
||||
host: url.host,
|
||||
'x-amz-content-sha256': contentHash,
|
||||
'x-amz-date': amzDate,
|
||||
};
|
||||
|
||||
if (contentType) {
|
||||
headers['content-type'] = contentType;
|
||||
}
|
||||
|
||||
const signedHeaders = Object.keys(headers).sort().join(';');
|
||||
const canonicalHeaders = Object.keys(headers)
|
||||
.sort()
|
||||
.map((key) => `${key}:${headers[key]}\n`)
|
||||
.join('');
|
||||
const canonicalRequest = [method, url.pathname, url.search.slice(1), canonicalHeaders, signedHeaders, contentHash].join('\n');
|
||||
const stringToSign = ['AWS4-HMAC-SHA256', amzDate, credentialScope, sha256Hex(canonicalRequest)].join('\n');
|
||||
const signature = crypto.createHmac('sha256', getSigningKey(config.secretAccessKey, date, config.region)).update(stringToSign).digest('hex');
|
||||
|
||||
return {
|
||||
url,
|
||||
headers: {
|
||||
...headers,
|
||||
Authorization: `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getPublicObjectUrl = (config: S3ImageStorageConfig, objectKey: string) => {
|
||||
if (config.publicBaseUrl) {
|
||||
return `${config.publicBaseUrl.replace(/\/+$/, '')}/${encodeObjectKey(objectKey)}`;
|
||||
}
|
||||
|
||||
return buildObjectUrl(config, objectKey).toString();
|
||||
};
|
||||
|
||||
export const getSignedS3ObjectUrl = ({
|
||||
config,
|
||||
objectKey,
|
||||
expiresInSeconds = 900,
|
||||
now = new Date(),
|
||||
}: {
|
||||
config: S3ImageStorageConfig;
|
||||
objectKey: string;
|
||||
expiresInSeconds?: number;
|
||||
now?: Date;
|
||||
}) => {
|
||||
const url = buildObjectUrl(config, objectKey);
|
||||
const amzDate = awsDate(now);
|
||||
const date = shortDate(now);
|
||||
const credentialScope = `${date}/${config.region}/s3/aws4_request`;
|
||||
const credential = `${config.accessKeyId}/${credentialScope}`;
|
||||
const queryParams: Record<string, string> = {
|
||||
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
||||
'X-Amz-Credential': credential,
|
||||
'X-Amz-Date': amzDate,
|
||||
'X-Amz-Expires': String(Math.min(Math.max(expiresInSeconds, 1), 604800)),
|
||||
'X-Amz-SignedHeaders': 'host',
|
||||
};
|
||||
const canonicalQuery = Object.keys(queryParams)
|
||||
.sort()
|
||||
.map((key) => `${encodeQueryValue(key)}=${encodeQueryValue(queryParams[key])}`)
|
||||
.join('&');
|
||||
const canonicalHeaders = `host:${url.host}\n`;
|
||||
const canonicalRequest = ['GET', url.pathname, canonicalQuery, canonicalHeaders, 'host', 'UNSIGNED-PAYLOAD'].join('\n');
|
||||
const stringToSign = ['AWS4-HMAC-SHA256', amzDate, credentialScope, sha256Hex(canonicalRequest)].join('\n');
|
||||
const signature = crypto.createHmac('sha256', getSigningKey(config.secretAccessKey, date, config.region)).update(stringToSign).digest('hex');
|
||||
|
||||
url.search = `${canonicalQuery}&X-Amz-Signature=${signature}`;
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
export const putS3Object = async ({
|
||||
config,
|
||||
objectKey,
|
||||
content,
|
||||
contentType,
|
||||
}: {
|
||||
config: S3ImageStorageConfig;
|
||||
objectKey: string;
|
||||
content: Buffer;
|
||||
contentType: string;
|
||||
}) => {
|
||||
const contentHash = sha256Hex(content);
|
||||
const signed = signS3Request({ config, method: 'PUT', objectKey, contentHash, contentType });
|
||||
const response = await fetch(signed.url, {
|
||||
method: 'PUT',
|
||||
headers: signed.headers,
|
||||
body: content,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => '');
|
||||
throw new Error(`Wasabi upload failed with ${response.status}${errorText ? `: ${errorText.slice(0, 300)}` : ''}`);
|
||||
}
|
||||
|
||||
return getPublicObjectUrl(config, objectKey);
|
||||
};
|
||||
|
||||
export const deleteS3Object = async ({
|
||||
config,
|
||||
objectKey,
|
||||
}: {
|
||||
config: S3ImageStorageConfig;
|
||||
objectKey: string;
|
||||
}) => {
|
||||
const signed = signS3Request({ config, method: 'DELETE', objectKey, contentHash: sha256Hex('') });
|
||||
const response = await fetch(signed.url, {
|
||||
method: 'DELETE',
|
||||
headers: signed.headers,
|
||||
});
|
||||
|
||||
if (!response.ok && response.status !== 404) {
|
||||
const errorText = await response.text().catch(() => '');
|
||||
throw new Error(`Wasabi delete failed with ${response.status}${errorText ? `: ${errorText.slice(0, 300)}` : ''}`);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user