Initial commit: CF Multisite 멀티테넌트 정적 호스팅
Some checks failed
Deploy to CF Multisite / deploy (push) Failing after 1m53s
Some checks failed
Deploy to CF Multisite / deploy (push) Failing after 1m53s
- Cloudflare Workers + R2 기반 - Edge 캐싱으로 비용 절감 - 티어별 Rate Limiting (free/basic/pro) - KV 기반 사용량 추적 - Admin API (usage, customers, tiers, stats) - Gitea Actions 배포 워크플로우 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
145
scripts/upload.js
Normal file
145
scripts/upload.js
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* R2 업로드 스크립트
|
||||
*
|
||||
* 사용법:
|
||||
* node scripts/upload.js <customer-id> <source-dir>
|
||||
*
|
||||
* 예시:
|
||||
* node scripts/upload.js demo-site ./sample-site
|
||||
*
|
||||
* 환경변수:
|
||||
* R2_ENDPOINT - R2 엔드포인트 URL
|
||||
* R2_ACCESS_KEY - R2 접근 키
|
||||
* R2_SECRET_KEY - R2 시크릿 키
|
||||
* R2_BUCKET - 버킷 이름 (기본: multisite-bucket)
|
||||
*/
|
||||
|
||||
const { S3Client, PutObjectCommand, ListObjectsV2Command, DeleteObjectsCommand } = require('@aws-sdk/client-s3');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// MIME 타입 매핑
|
||||
const MIME_TYPES = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.css': 'text/css',
|
||||
'.js': 'application/javascript',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
'.pdf': 'application/pdf',
|
||||
'.xml': 'application/xml',
|
||||
'.txt': 'text/plain',
|
||||
'.md': 'text/markdown',
|
||||
};
|
||||
|
||||
// 재귀적으로 모든 파일 찾기
|
||||
function getAllFiles(dir, files = []) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
getAllFiles(fullPath, files);
|
||||
} else {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [,, customerId, sourceDir] = process.argv;
|
||||
|
||||
if (!customerId || !sourceDir) {
|
||||
console.error('Usage: node scripts/upload.js <customer-id> <source-dir>');
|
||||
console.error('Example: node scripts/upload.js demo-site ./sample-site');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 환경변수 체크
|
||||
const endpoint = process.env.R2_ENDPOINT;
|
||||
const accessKeyId = process.env.R2_ACCESS_KEY;
|
||||
const secretAccessKey = process.env.R2_SECRET_KEY;
|
||||
const bucket = process.env.R2_BUCKET || 'multisite-bucket';
|
||||
|
||||
if (!endpoint || !accessKeyId || !secretAccessKey) {
|
||||
console.error('Missing environment variables:');
|
||||
console.error(' R2_ENDPOINT, R2_ACCESS_KEY, R2_SECRET_KEY');
|
||||
console.error('');
|
||||
console.error('R2 API 토큰 생성: Cloudflare Dashboard > R2 > Manage R2 API Tokens');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// S3 클라이언트 생성 (R2는 S3 호환)
|
||||
const s3 = new S3Client({
|
||||
region: 'auto',
|
||||
endpoint,
|
||||
credentials: { accessKeyId, secretAccessKey },
|
||||
});
|
||||
|
||||
const sourcePath = path.resolve(sourceDir);
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
console.error(`Source directory not found: ${sourcePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Uploading to R2...`);
|
||||
console.log(` Customer: ${customerId}`);
|
||||
console.log(` Source: ${sourcePath}`);
|
||||
console.log(` Bucket: ${bucket}`);
|
||||
console.log('');
|
||||
|
||||
// 모든 파일 수집
|
||||
const files = getAllFiles(sourcePath);
|
||||
const prefix = `sites/${customerId}`;
|
||||
|
||||
// 업로드
|
||||
let uploaded = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const relativePath = path.relative(sourcePath, file);
|
||||
const key = `${prefix}/${relativePath}`.replace(/\\/g, '/');
|
||||
const ext = path.extname(file).toLowerCase();
|
||||
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||
|
||||
try {
|
||||
const body = fs.readFileSync(file);
|
||||
|
||||
await s3.send(new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: body,
|
||||
ContentType: contentType,
|
||||
}));
|
||||
|
||||
console.log(` ✓ ${key}`);
|
||||
uploaded++;
|
||||
} catch (error) {
|
||||
console.error(` ✗ ${key}: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(`Done! Uploaded: ${uploaded}, Failed: ${failed}`);
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user