const readline = require("node:readline/promises"); const { stdin: input, stdout: output } = require("node:process"); // Configuration const config = { GITEA_URL: "https://git.zeldon.ru", GITEA_TOKEN: "", REPO_OWNER: "", REPO_NAME: "", KEEP_LAST_N: 10, // Number of latest attachments to keep }; // Helper function for API calls async function fetchApi(endpoint) { const response = await fetch(`${config.GITEA_URL}/api/v1${endpoint}`, { headers: { Authorization: `token ${config.GITEA_TOKEN}`, Accept: "application/json", }, }); if (!response.ok) { throw new Error( `API call failed: ${response.status} ${response.statusText}` ); } return response.json(); } async function getAllReleases() { let page = 1; let allReleases = []; while (true) { const releases = await fetchApi( `/repos/${config.REPO_OWNER}/${config.REPO_NAME}/releases?page=${page}&limit=50` ); if (releases.length === 0) { break; } allReleases = [...allReleases, ...releases]; page++; } return allReleases; } async function getAttachmentsToDelete() { try { // Get all releases using pagination const releases = await getAllReleases(); // Collect all attachments with their metadata const allAttachments = releases.reduce((acc, release) => { const attachments = (release.assets || []).map((asset) => ({ id: asset.id, name: asset.name, created: new Date(release.created_at), releaseId: release.id, size: asset.size, })); return [...acc, ...attachments]; }, []); // Sort attachments by creation date (newest first) allAttachments.sort((a, b) => b.created - a.created); // Get attachments to delete (skip the first KEEP_LAST_N) const attachmentsToDelete = allAttachments.slice(config.KEEP_LAST_N); // Calculate total size in MB const totalSizeMB = ( allAttachments.reduce((sum, att) => sum + att.size, 0) / (1024 * 1024) ).toFixed(2); const sizeToDeleteMB = ( attachmentsToDelete.reduce((sum, att) => sum + att.size, 0) / (1024 * 1024) ).toFixed(2); const sizeToKeepMB = (totalSizeMB - sizeToDeleteMB).toFixed(2); console.log( `Found ${allAttachments.length} total attachments (${totalSizeMB} MB)` ); console.log( `Keeping ${config.KEEP_LAST_N} most recent attachments (${sizeToKeepMB} MB)` ); console.log( `${attachmentsToDelete.length} attachments will be deleted (${sizeToDeleteMB} MB)` ); // Return list of attachment IDs to delete return attachmentsToDelete.map((att) => ({ id: att.id, name: att.name, releaseId: att.releaseId, })); } catch (error) { console.error("Error getting attachments:", error.message); throw error; } } async function deleteAttachment(releaseId, attachmentId) { const response = await fetch( `${config.GITEA_URL}/api/v1/repos/${config.REPO_OWNER}/${config.REPO_NAME}/releases/${releaseId}/assets/${attachmentId}`, { method: "DELETE", headers: { Authorization: `token ${config.GITEA_TOKEN}`, }, } ); if (!response.ok) { throw new Error( `Failed to delete attachment ${attachmentId}: ${response.status} ${response.statusText}` ); } } async function main() { try { const attachments = await getAttachmentsToDelete(); const rl = readline.createInterface({ input, output }); const answer = await rl.question( "\nDo you want to proceed with deletion? (y/N) " ); rl.close(); if (answer.toLowerCase() === "y") { console.log("\nDeleting attachments..."); let deleted = 0; for (const att of attachments) { try { await deleteAttachment(att.releaseId, att.id); console.log(`Deleted: ${att.name}`); deleted++; } catch (error) { console.error(`Failed to delete ${att.name}:`, error.message); } } console.log( `\nDeletion complete. Successfully deleted ${deleted}/${attachments.length} attachments.` ); } else { console.log("Operation cancelled by user."); } } catch (error) { console.error("Script failed:", error); process.exit(1); } } main();