109 lines
3.2 KiB
TypeScript
109 lines
3.2 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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);
|
|
});
|