export class Gitea { constructor(repositoryUrl, token, fetchImpl = fetch) { const url = new URL(repositoryUrl); this.origin = url.origin; this.base = `${url.origin}/api/v1/repos${url.pathname.replace(/\/$/, '')}`; this.token = token; this.fetch = fetchImpl; } async request(path, { method = 'GET', body, missing = false, raw = false } = {}) { const response = await this.fetch(`${this.base}${path}`, { method, redirect: 'error', signal: AbortSignal.timeout(60_000), headers: { Authorization: `token ${this.token}`, ...(body && !(body instanceof FormData) ? { 'Content-Type': 'application/json' } : {}) }, body: body instanceof FormData ? body : body ? JSON.stringify(body) : undefined, }); if (missing && response.status === 404) return null; if (!response.ok) throw new Error(`Gitea ${method} ${path}: HTTP ${response.status}`); if (raw) return Buffer.from(await response.arrayBuffer()); return response.status === 204 ? null : response.json(); } release(tag) { return this.request(`/releases/tags/${encodeURIComponent(tag)}`, { missing: true }); } tag(tag) { return this.request(`/tags/${encodeURIComponent(tag)}`, { missing: true }); } async releases() { const releases = []; for (let page = 1; ; page++) { const batch = await this.request(`/releases?limit=50&page=${page}`); releases.push(...batch); if (batch.length < 50) return releases; } } async asset(release, name) { const assets = await this.request(`/releases/${release.id}/assets`); const asset = assets.find(item => item.name === name); if (!asset) return null; let url = new URL(asset.browser_download_url); if (url.origin !== this.origin) throw new Error('Unerwartete Herkunft der Release-Datei.'); for (let redirects = 0; redirects < 5; redirects++) { const response = await this.fetch(url.href, { redirect: 'manual', signal: AbortSignal.timeout(60_000), headers: url.origin === this.origin ? { Authorization: `token ${this.token}` } : {}, }); if ([301, 302, 303, 307, 308].includes(response.status)) { url = new URL(response.headers.get('location'), url); if (url.protocol !== 'https:') throw new Error('Unsicheres Download-Redirect.'); continue; } if (!response.ok) throw new Error(`Release-Download: HTTP ${response.status}`); return Buffer.from(await response.arrayBuffer()); } throw new Error('Zu viele Download-Redirects.'); } async upload(release, name, data) { const assets = await this.request(`/releases/${release.id}/assets`); const existing = assets.find(item => item.name === name); if (existing) { const saved = await this.asset(release, name); if (!saved.equals(Buffer.from(data))) throw new Error(`Release-Datei ${name} existiert bereits mit anderem Inhalt.`); return; } const form = new FormData(); form.append('attachment', new Blob([data]), name); await this.request(`/releases/${release.id}/assets?name=${encodeURIComponent(name)}`, { method: 'POST', body: form }); } }