211 lines
4.8 KiB
JavaScript
Executable File
211 lines
4.8 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import process from 'node:process';
|
|
import {spawnSync} from 'node:child_process';
|
|
|
|
import {
|
|
buildObjectKey,
|
|
createAwsV4Headers,
|
|
encodeObjectKey,
|
|
getFrontendSpaAliasContentType,
|
|
getFrontendSpaAliasKeys,
|
|
getCacheControl,
|
|
getContentType,
|
|
loadRepoEnv,
|
|
listFiles,
|
|
normalizeEndpoint,
|
|
requestDogeCloudTemporaryS3Session,
|
|
} from './oss-deploy-lib.mjs';
|
|
|
|
const repoRoot = process.cwd();
|
|
const frontDir = path.join(repoRoot, 'front');
|
|
const distDir = path.join(frontDir, 'dist');
|
|
|
|
function parseArgs(argv) {
|
|
return {
|
|
dryRun: argv.includes('--dry-run'),
|
|
skipBuild: argv.includes('--skip-build'),
|
|
};
|
|
}
|
|
|
|
function requireEnv(name) {
|
|
const value = process.env[name];
|
|
if (!value) {
|
|
throw new Error(`Missing required environment variable: ${name}`);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function runBuild() {
|
|
const result = spawnSync('npm', ['run', 'build'], {
|
|
cwd: frontDir,
|
|
stdio: 'inherit',
|
|
shell: process.platform === 'win32',
|
|
});
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error('Frontend build failed');
|
|
}
|
|
}
|
|
|
|
async function uploadFile({
|
|
bucket,
|
|
endpoint,
|
|
region,
|
|
objectKey,
|
|
filePath,
|
|
contentTypeOverride,
|
|
accessKeyId,
|
|
secretAccessKey,
|
|
sessionToken,
|
|
}) {
|
|
const body = await fs.readFile(filePath);
|
|
const contentType = contentTypeOverride || getContentType(objectKey);
|
|
const amzDate = new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
|
|
const url = `https://${bucket}.${normalizeEndpoint(endpoint)}/${encodeObjectKey(objectKey)}`;
|
|
const signatureHeaders = createAwsV4Headers({
|
|
method: 'PUT',
|
|
endpoint,
|
|
region,
|
|
bucket,
|
|
objectKey,
|
|
headers: {
|
|
'Content-Type': contentType,
|
|
},
|
|
amzDate,
|
|
accessKeyId,
|
|
secretAccessKey,
|
|
sessionToken,
|
|
});
|
|
|
|
const response = await fetch(url, {
|
|
method: 'PUT',
|
|
headers: {
|
|
...signatureHeaders,
|
|
'Cache-Control': getCacheControl(objectKey),
|
|
'Content-Length': String(body.byteLength),
|
|
},
|
|
body,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
throw new Error(`Upload failed for ${objectKey}: ${response.status} ${response.statusText}\n${text}`);
|
|
}
|
|
}
|
|
|
|
async function uploadSpaAliases({
|
|
bucket,
|
|
endpoint,
|
|
region,
|
|
distIndexPath,
|
|
accessKeyId,
|
|
secretAccessKey,
|
|
sessionToken,
|
|
remotePrefix,
|
|
dryRun,
|
|
}) {
|
|
const aliases = getFrontendSpaAliasKeys();
|
|
const contentType = getFrontendSpaAliasContentType();
|
|
|
|
for (const alias of aliases) {
|
|
const objectKey = buildObjectKey(remotePrefix, alias);
|
|
|
|
if (dryRun) {
|
|
console.log(`[dry-run] upload alias ${alias} -> ${objectKey}`);
|
|
continue;
|
|
}
|
|
|
|
await uploadFile({
|
|
bucket,
|
|
endpoint,
|
|
region,
|
|
objectKey,
|
|
filePath: distIndexPath,
|
|
contentTypeOverride: contentType,
|
|
accessKeyId,
|
|
secretAccessKey,
|
|
sessionToken,
|
|
});
|
|
console.log(`uploaded alias ${objectKey}`);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const {dryRun, skipBuild} = parseArgs(process.argv.slice(2));
|
|
|
|
await loadRepoEnv({repoRoot});
|
|
|
|
const apiAccessKey = requireEnv('YOYUZH_DOGECLOUD_API_ACCESS_KEY');
|
|
const apiSecretKey = requireEnv('YOYUZH_DOGECLOUD_API_SECRET_KEY');
|
|
const scope = requireEnv('YOYUZH_DOGECLOUD_FRONT_SCOPE');
|
|
const apiBaseUrl = process.env.YOYUZH_DOGECLOUD_API_BASE_URL || 'https://api.dogecloud.com';
|
|
const region = process.env.YOYUZH_DOGECLOUD_S3_REGION || 'automatic';
|
|
const remotePrefix = process.env.YOYUZH_DOGECLOUD_FRONT_PREFIX || '';
|
|
const ttlSeconds = Number(process.env.YOYUZH_DOGECLOUD_FRONT_TTL_SECONDS || '3600');
|
|
const {
|
|
accessKeyId,
|
|
secretAccessKey,
|
|
sessionToken,
|
|
endpoint,
|
|
bucket,
|
|
} = await requestDogeCloudTemporaryS3Session({
|
|
apiBaseUrl,
|
|
accessKey: apiAccessKey,
|
|
secretKey: apiSecretKey,
|
|
scope,
|
|
ttlSeconds,
|
|
});
|
|
|
|
if (!skipBuild) {
|
|
runBuild();
|
|
}
|
|
|
|
const files = await listFiles(distDir);
|
|
if (files.length === 0) {
|
|
throw new Error('No files found in front/dist. Run the frontend build first.');
|
|
}
|
|
|
|
for (const filePath of files) {
|
|
const relativePath = path.relative(distDir, filePath).split(path.sep).join('/');
|
|
const objectKey = buildObjectKey(remotePrefix, relativePath);
|
|
|
|
if (dryRun) {
|
|
console.log(`[dry-run] upload ${relativePath} -> ${objectKey}`);
|
|
continue;
|
|
}
|
|
|
|
await uploadFile({
|
|
bucket,
|
|
endpoint,
|
|
region,
|
|
objectKey,
|
|
filePath,
|
|
accessKeyId,
|
|
secretAccessKey,
|
|
sessionToken,
|
|
});
|
|
console.log(`uploaded ${objectKey}`);
|
|
}
|
|
|
|
await uploadSpaAliases({
|
|
bucket,
|
|
endpoint,
|
|
region,
|
|
distIndexPath: path.join(distDir, 'index.html'),
|
|
accessKeyId,
|
|
secretAccessKey,
|
|
sessionToken,
|
|
remotePrefix,
|
|
dryRun,
|
|
});
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exitCode = 1;
|
|
});
|