From d42b5323ebe0366766af62aebbc60abcedfd19c9 Mon Sep 17 00:00:00 2001 From: Timofey Gelazoniya Date: Sun, 2 Feb 2025 11:04:27 +0300 Subject: [PATCH] initial commit --- README.md | 47 ++++++++++++++++ index.js | 159 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 README.md create mode 100644 index.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f9a440 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Gitea Release Attachments Cleaner + +A Node.js script to manage and clean up release attachments in Gitea repositories. This tool helps maintain repository size by keeping only the N most recent release attachments and removing older ones. + +## Prerequisites + +- Node.js installed on your system +- Access to a Gitea instance +- Personal access token with repository access permissions + +## Configuration + +Edit the `config` object in `index.js`: + +```javascript +const config = { + GITEA_URL: "https://git.zeldon.ru", + GITEA_TOKEN: "", // Your Gitea personal access token + REPO_OWNER: "", // Repository owner (user or organization) + REPO_NAME: "", // Repository name + KEEP_LAST_N: 10, // Number of latest attachments to keep +}; +``` + +## Usage + +1. Clone this repository +2. Run the script: + ```bash + node index.js + ``` + +The script will: + +1. List all attachments and their total size +2. Show how many attachments will be kept and deleted +3. Ask for confirmation before deletion +4. Delete the selected attachments and show progress + +Example output: + +``` +Found 25 total attachments (156.78 MB) +Keeping 10 most recent attachments (52.30 MB) +15 attachments will be deleted (104.48 MB) +Do you want to proceed with deletion? (y/N) +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..7039697 --- /dev/null +++ b/index.js @@ -0,0 +1,159 @@ +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();