/** * Release asset uploader for Gitea * Streams large files without loading them into memory (bypasses curl's 2GB multipart limit) * * Usage: GITEA_TOKEN=xxx RELEASE_ID=123 GITEA_REPO=owner/repo tsx release-upload.ts */ import * as fs from 'fs'; import * as path from 'path'; import * as https from 'https'; const token = process.env.GITEA_TOKEN; const releaseId = process.env.RELEASE_ID; const repo = process.env.GITEA_REPO; if (!token || !releaseId || !repo) { console.error('Missing required env vars: GITEA_TOKEN, RELEASE_ID, GITEA_REPO'); process.exit(1); } const boundary = '----FormBoundary' + Date.now().toString(16); async function uploadFile(filepath: string): Promise { const filename = path.basename(filepath); const stats = fs.statSync(filepath); console.log(`Uploading ${filename} (${stats.size} bytes)...`); const header = Buffer.from( `--${boundary}\r\n` + `Content-Disposition: form-data; name="attachment"; filename="${filename}"\r\n` + `Content-Type: application/octet-stream\r\n\r\n` ); const footer = Buffer.from(`\r\n--${boundary}--\r\n`); const contentLength = header.length + stats.size + footer.length; return new Promise((resolve, reject) => { const req = https.request({ hostname: 'code.foss.global', path: `/api/v1/repos/${repo}/releases/${releaseId}/assets?name=${encodeURIComponent(filename)}`, method: 'POST', headers: { 'Authorization': `token ${token}`, 'Content-Type': `multipart/form-data; boundary=${boundary}`, 'Content-Length': contentLength } }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { clearInterval(progressInterval); console.log(data); if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { console.log(`✓ ${filename} uploaded successfully`); resolve(); } else { reject(new Error(`Upload failed: ${res.statusCode} ${data}`)); } }); }); req.on('error', (err) => { clearInterval(progressInterval); reject(err); }); // Track upload progress let bytesWritten = header.length; const progressInterval = setInterval(() => { const percent = Math.round((bytesWritten / contentLength) * 100); console.log(` ${filename}: ${percent}% (${Math.round(bytesWritten / 1024 / 1024)}MB / ${Math.round(contentLength / 1024 / 1024)}MB)`); }, 10000); // Stream: write header, pipe file, write footer req.write(header); const stream = fs.createReadStream(filepath); stream.on('data', (chunk) => { bytesWritten += chunk.length; }); stream.on('error', (err) => { clearInterval(progressInterval); reject(err); }); stream.on('end', () => { bytesWritten += footer.length; req.write(footer); req.end(); }); stream.pipe(req, { end: false }); }); } async function main() { const distDir = 'dist'; const files = fs.readdirSync(distDir) .map(f => path.join(distDir, f)) .filter(f => fs.statSync(f).isFile()); for (const file of files) { await uploadFile(file); } console.log('All assets uploaded successfully'); } main().catch(err => { console.error(err); process.exit(1); });