555 lines
23 KiB
HTML
555 lines
23 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Gitea Upload</title>
|
||
<style>
|
||
:root{
|
||
--bg:#141517; --panel:#1c1d20; --panel-alt:#202225; --text:#e6e7ea; --muted:#8b9099;
|
||
--accent:#f07d1e; --accent-dark:#c9650f; --border:#2c2e33;
|
||
--ok:#5fd98a; --err:#ff8a80; --header-bg:#0d0e0f;
|
||
}
|
||
*{box-sizing:border-box;}
|
||
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--text);}
|
||
header{background:var(--header-bg);border-bottom:2px solid var(--accent);padding:16px 24px;
|
||
display:flex;align-items:center;justify-content:space-between;}
|
||
header .title{font-weight:800;font-size:1.3rem;letter-spacing:.5px;}
|
||
header .title .accent{color:var(--accent);}
|
||
header .subtitle{color:var(--muted);font-size:.8rem;margin-left:10px;font-weight:600;letter-spacing:.5px;}
|
||
header .icons{display:flex;gap:8px;}
|
||
header .icons button{background:var(--panel);border:1px solid var(--border);color:var(--text);
|
||
width:32px;height:32px;border-radius:6px;cursor:pointer;font-size:1rem;
|
||
display:flex;align-items:center;justify-content:center;padding:0;line-height:1;}
|
||
header .icons button:hover{border-color:var(--accent);color:var(--accent);}
|
||
button:focus{outline:none;}
|
||
button:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
|
||
main{max-width:760px;margin:24px auto 40px;padding:0 16px;}
|
||
.card{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:20px 22px;margin-bottom:18px;}
|
||
.card h2{margin:0 0 4px;font-size:1.02rem;}
|
||
.card .step-num{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;
|
||
border-radius:50%;background:var(--accent);color:#111;font-size:.8rem;font-weight:700;margin-right:8px;}
|
||
label{display:block;font-size:.85rem;color:var(--muted);margin:12px 0 4px;}
|
||
input[type=text],input[type=password],input[type=url],select{
|
||
width:100%;padding:9px 10px;border:1px solid var(--border);border-radius:6px;font-size:.93rem;
|
||
background:var(--panel-alt);color:var(--text);
|
||
}
|
||
input::placeholder{color:#5b5f66;}
|
||
input[type=checkbox]{margin-right:6px;accent-color:var(--accent);}
|
||
.checkline{display:flex;align-items:center;font-size:.85rem;color:var(--muted);margin-top:10px;}
|
||
.row{display:flex;gap:12px;flex-wrap:wrap;}
|
||
.row > *{flex:1;min-width:150px;}
|
||
button{background:var(--accent);color:#111;border:none;border-radius:6px;padding:9px 15px;
|
||
font-size:.9rem;font-weight:600;cursor:pointer;margin-top:14px;}
|
||
button:hover{background:var(--accent-dark);color:#fff;}
|
||
button.secondary{background:var(--panel-alt);color:var(--text);border:1px solid var(--border);}
|
||
button.secondary:hover{border-color:var(--accent);color:var(--accent);background:var(--panel-alt);}
|
||
button.small{padding:6px 12px;font-size:.82rem;margin-top:0;}
|
||
button:disabled{background:#3a3c40;color:#787c82;cursor:not-allowed;}
|
||
.hint{font-size:.78rem;color:var(--muted);margin-top:6px;}
|
||
.status{margin-top:12px;font-size:.87rem;}
|
||
.status.ok{color:var(--ok);}
|
||
.status.err{color:var(--err);}
|
||
#log{margin-top:14px;background:#0d0e0f;color:#c7cad0;border-radius:8px;padding:12px;
|
||
font-family:ui-monospace,Consolas,monospace;font-size:.8rem;max-height:200px;overflow-y:auto;
|
||
white-space:pre-wrap;display:none;border:1px solid var(--border);}
|
||
#log .l-ok{color:var(--ok);}
|
||
#log .l-err{color:var(--err);}
|
||
.progress-wrap{margin-top:10px;height:7px;border-radius:4px;background:#2c2e33;overflow:hidden;display:none;}
|
||
.progress-bar{height:100%;background:var(--accent);width:0%;transition:width .2s;}
|
||
fieldset{border:1px solid var(--border);border-radius:8px;padding:14px;margin-top:10px;}
|
||
fieldset legend{font-size:.8rem;color:var(--muted);padding:0 6px;}
|
||
.disabled-section{opacity:.45;pointer-events:none;}
|
||
details{margin-top:10px;font-size:.85rem;color:var(--muted);}
|
||
details summary{cursor:pointer;color:var(--text);}
|
||
#fileList{max-height:150px;overflow-y:auto;margin-top:8px;font-family:ui-monospace,Consolas,monospace;
|
||
font-size:.78rem;background:var(--panel-alt);border-radius:6px;padding:8px;border:1px solid var(--border);}
|
||
footer{display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;color:var(--muted);
|
||
font-size:.8rem;padding:18px 24px;border-top:1px solid var(--border);}
|
||
footer a{color:var(--muted);text-decoration:none;margin-right:16px;}
|
||
footer a:hover{color:var(--accent);}
|
||
.overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;align-items:center;
|
||
justify-content:center;z-index:50;}
|
||
.overlay.open{display:flex;}
|
||
.modal{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:22px 24px;
|
||
max-width:480px;width:90%;max-height:80vh;overflow-y:auto;}
|
||
.modal h3{margin-top:0;}
|
||
.modal .close{float:right;background:none;border:none;color:var(--muted);font-size:1.2rem;
|
||
cursor:pointer;margin:0;padding:0;}
|
||
.modal ol,.modal ul{padding-left:20px;font-size:.9rem;color:var(--text);}
|
||
.modal li{margin-bottom:8px;}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<div>
|
||
<span class="title">GITEA <span class="accent">UPLOAD</span></span>
|
||
<span class="subtitle">ZIP HOCHLADEN</span>
|
||
</div>
|
||
<div class="icons">
|
||
<button id="btnHelp" title="Anleitung">?</button>
|
||
<button id="btnSettings" title="Einstellungen">⚙</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
|
||
<div class="card">
|
||
<h2><span class="step-num">1</span>Gitea-Zugang</h2>
|
||
<label for="serverUrl">Gitea-Server</label>
|
||
<input type="url" id="serverUrl" placeholder="https://git.beispiel.de">
|
||
|
||
<div class="row">
|
||
<div>
|
||
<label for="username">Benutzername</label>
|
||
<input type="text" id="username" autocomplete="username">
|
||
</div>
|
||
<div>
|
||
<label for="password">Passwort oder Zugangstoken</label>
|
||
<input type="password" id="password" autocomplete="current-password">
|
||
</div>
|
||
</div>
|
||
<p class="hint">Ein Zugangstoken (Einstellungen → Anwendungen → Neues Token in Gitea) ist sicherer als das echte Passwort.</p>
|
||
|
||
<button id="btnLoadRepos">Verbinden & Repos laden</button>
|
||
<div id="authStatus" class="status"></div>
|
||
</div>
|
||
|
||
<div class="card disabled-section" id="repoCard">
|
||
<h2><span class="step-num">2</span>Ziel wählen</h2>
|
||
|
||
<label for="ownerSelect">Konto / Organisation</label>
|
||
<select id="ownerSelect"><option value="">– bitte zuerst verbinden –</option></select>
|
||
|
||
<label for="repoSelect">Vorhandenes Repo</label>
|
||
<select id="repoSelect"><option value="">– bitte zuerst verbinden –</option></select>
|
||
|
||
<fieldset>
|
||
<legend>Oder neues Repo anlegen</legend>
|
||
<div class="row">
|
||
<input type="text" id="newRepoName" placeholder="repo-name">
|
||
<label class="checkline" style="flex:0 0 auto;">
|
||
<input type="checkbox" id="newRepoPrivate" checked> Privat
|
||
</label>
|
||
</div>
|
||
<button class="secondary" id="btnCreateRepo">Repo erstellen & auswählen</button>
|
||
</fieldset>
|
||
<div id="repoStatus" class="status"></div>
|
||
</div>
|
||
|
||
<div class="card disabled-section" id="zipCard">
|
||
<h2><span class="step-num">3</span>ZIP-Datei wählen</h2>
|
||
<input type="file" id="zipFile" accept=".zip">
|
||
<label class="checkline"><input type="checkbox" id="stripRoot" checked> Obersten Ordner aus der ZIP entfernen</label>
|
||
<label class="checkline"><input type="checkbox" id="removeMissing"> Dateien im Repo löschen, die nicht mehr in der ZIP enthalten sind</label>
|
||
<div id="zipStatus" class="status"></div>
|
||
<details id="fileListDetails" style="display:none;">
|
||
<summary>Enthaltene Dateien anzeigen</summary>
|
||
<div id="fileList"></div>
|
||
</details>
|
||
</div>
|
||
|
||
<div class="card disabled-section" id="uploadCard">
|
||
<h2><span class="step-num">4</span>Hochladen</h2>
|
||
<label for="commitMsg">Commit-Nachricht</label>
|
||
<input type="text" id="commitMsg" value="Upload per ZIP-Tool">
|
||
|
||
<button id="btnUpload">Jetzt hochladen</button>
|
||
<div class="progress-wrap" id="progressWrap"><div class="progress-bar" id="progressBar"></div></div>
|
||
<div id="uploadStatus" class="status"></div>
|
||
<div id="log"></div>
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<footer>
|
||
<div>
|
||
<a href="/impressum">Impressum</a>
|
||
<a href="/datenschutz">Datenschutz</a>
|
||
<a href="./LICENSE">Lizenzen & Quellcode</a>
|
||
<a href="#" id="footerAnleitung">Anleitung</a>
|
||
</div>
|
||
<div>Alle Daten bleiben in deinem Browser · MIT-Lizenz</div>
|
||
</footer>
|
||
|
||
<!-- Anleitung -->
|
||
<div class="overlay" id="helpOverlay">
|
||
<div class="modal">
|
||
<button class="close" data-close="helpOverlay">×</button>
|
||
<h3>Anleitung</h3>
|
||
<ol>
|
||
<li>Gitea-Server-URL, Benutzername und Passwort oder Zugangstoken eingeben, dann "Verbinden & Repos laden".</li>
|
||
<li>Konto/Organisation wählen, dann ein vorhandenes Repo auswählen <strong>oder</strong> einen neuen Namen eingeben und "Repo erstellen".</li>
|
||
<li>ZIP-Datei auswählen. Enthält sie einen einzelnen umschließenden Ordner, wird der auf Wunsch automatisch entfernt.</li>
|
||
<li>Commit-Nachricht anpassen (optional) und "Jetzt hochladen" klicken.</li>
|
||
</ol>
|
||
<p>Dateien mit gleichem Pfad wie im Repo werden aktualisiert statt doppelt angelegt. Über
|
||
"Zugangsdaten in diesem Browser speichern" (Einstellungen, Zahnrad oben rechts) müssen Server,
|
||
Benutzername und Passwort nicht jedes Mal neu eingegeben werden.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Einstellungen -->
|
||
<div class="overlay" id="settingsOverlay">
|
||
<div class="modal">
|
||
<button class="close" data-close="settingsOverlay">×</button>
|
||
<h3>Einstellungen</h3>
|
||
<label class="checkline"><input type="checkbox" id="rememberCreds"> Zugangsdaten in diesem Browser speichern</label>
|
||
<p class="hint">Werden nur lokal in diesem Browser gespeichert, nirgendwo sonst übertragen. Auf einem
|
||
gemeinsam genutzten Rechner besser deaktiviert lassen.</p>
|
||
<button class="secondary small" id="btnClearCreds">Gespeicherte Zugangsdaten löschen</button>
|
||
|
||
<hr style="border-color:var(--border);margin:18px 0;">
|
||
|
||
<p class="hint" style="margin-top:0;">Zugangsdaten als Datei sichern oder auf einem anderen Gerät wieder einlesen:</p>
|
||
<button class="secondary small" id="btnExport">Export</button>
|
||
<button class="secondary small" id="btnImportTrigger">Import</button>
|
||
<input type="file" id="importFile" accept=".json" style="display:none;">
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||
<script>
|
||
(function(){
|
||
"use strict";
|
||
|
||
var STORAGE_KEY = "giteaUploaderCreds";
|
||
|
||
var state = {
|
||
server: "", authHeader: "", repos: [], owners: [], selectedOwner: "",
|
||
repo: null, // {owner, name, defaultBranch}
|
||
zipFiles: null
|
||
};
|
||
|
||
function el(id){ return document.getElementById(id); }
|
||
function setStatus(id, msg, cls){
|
||
var n = el(id);
|
||
n.textContent = msg;
|
||
n.className = "status" + (cls ? " " + cls : "");
|
||
}
|
||
function enable(id){ el(id).classList.remove("disabled-section"); }
|
||
function disable(id){ el(id).classList.add("disabled-section"); }
|
||
function b64utf8(str){ return btoa(unescape(encodeURIComponent(str))); }
|
||
function apiUrl(path){ return state.server.replace(/\/+$/,"") + path; }
|
||
function logLine(msg, isErr){
|
||
var log = el("log");
|
||
log.style.display = "block";
|
||
var line = document.createElement("div");
|
||
line.className = isErr ? "l-err" : "l-ok";
|
||
line.textContent = msg;
|
||
log.appendChild(line);
|
||
log.scrollTop = log.scrollHeight;
|
||
}
|
||
function openOverlay(id){ el(id).classList.add("open"); }
|
||
function closeOverlay(id){ el(id).classList.remove("open"); }
|
||
|
||
document.querySelectorAll("[data-close]").forEach(function(btn){
|
||
btn.addEventListener("click", function(){ closeOverlay(btn.getAttribute("data-close")); });
|
||
});
|
||
document.querySelectorAll(".overlay").forEach(function(ov){
|
||
ov.addEventListener("click", function(e){ if(e.target === ov) ov.classList.remove("open"); });
|
||
});
|
||
el("btnHelp").addEventListener("click", function(){ openOverlay("helpOverlay"); });
|
||
el("footerAnleitung").addEventListener("click", function(e){ e.preventDefault(); openOverlay("helpOverlay"); });
|
||
el("btnSettings").addEventListener("click", function(){ openOverlay("settingsOverlay"); });
|
||
|
||
async function apiFetch(path, opts){
|
||
opts = opts || {};
|
||
opts.headers = Object.assign({
|
||
"Authorization": state.authHeader,
|
||
"Content-Type": "application/json"
|
||
}, opts.headers || {});
|
||
var res = await fetch(apiUrl(path), opts);
|
||
if(!res.ok){
|
||
var msg = "HTTP " + res.status;
|
||
try{ var body = await res.json(); if(body && body.message) msg += ": " + body.message; }catch(e){}
|
||
throw new Error(msg);
|
||
}
|
||
if(res.status === 204) return null;
|
||
return res.json();
|
||
}
|
||
|
||
// ---- Zugangsdaten merken ----
|
||
function loadStoredCreds(){
|
||
try{
|
||
var raw = localStorage.getItem(STORAGE_KEY);
|
||
if(!raw) return;
|
||
var data = JSON.parse(raw);
|
||
el("serverUrl").value = data.server || "";
|
||
el("username").value = data.username || "";
|
||
el("password").value = data.password || "";
|
||
el("rememberCreds").checked = true;
|
||
}catch(e){}
|
||
}
|
||
function saveCredsIfWanted(){
|
||
if(el("rememberCreds").checked){
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||
server: el("serverUrl").value.trim(),
|
||
username: el("username").value.trim(),
|
||
password: el("password").value
|
||
}));
|
||
}else{
|
||
localStorage.removeItem(STORAGE_KEY);
|
||
}
|
||
}
|
||
el("btnClearCreds").addEventListener("click", function(){
|
||
localStorage.removeItem(STORAGE_KEY);
|
||
el("rememberCreds").checked = false;
|
||
el("password").value = "";
|
||
setStatus("authStatus", "Gespeicherte Zugangsdaten gelöscht.", "ok");
|
||
});
|
||
loadStoredCreds();
|
||
|
||
// ---- Export / Import ----
|
||
el("btnExport").addEventListener("click", function(){
|
||
var data = {
|
||
server: el("serverUrl").value.trim(),
|
||
username: el("username").value.trim(),
|
||
password: el("password").value
|
||
};
|
||
var blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||
var a = document.createElement("a");
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = "gitea-uploader-zugangsdaten.json";
|
||
a.click();
|
||
URL.revokeObjectURL(a.href);
|
||
});
|
||
el("btnImportTrigger").addEventListener("click", function(){ el("importFile").click(); });
|
||
el("importFile").addEventListener("change", function(){
|
||
var file = this.files[0];
|
||
if(!file) return;
|
||
var reader = new FileReader();
|
||
reader.onload = function(){
|
||
try{
|
||
var data = JSON.parse(reader.result);
|
||
el("serverUrl").value = data.server || "";
|
||
el("username").value = data.username || "";
|
||
el("password").value = data.password || "";
|
||
closeOverlay("settingsOverlay");
|
||
setStatus("authStatus", "Zugangsdaten importiert. Jetzt verbinden.", "ok");
|
||
}catch(e){
|
||
alert("Datei konnte nicht gelesen werden: " + e.message);
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
});
|
||
|
||
// ---- Schritt 1: Verbinden & Repos laden ----
|
||
el("btnLoadRepos").addEventListener("click", async function(){
|
||
state.server = el("serverUrl").value.trim();
|
||
var user = el("username").value.trim();
|
||
var pass = el("password").value;
|
||
if(!state.server || !user || !pass){
|
||
setStatus("authStatus", "Bitte Server, Benutzername und Passwort/Token ausfüllen.", "err");
|
||
return;
|
||
}
|
||
state.authHeader = "Basic " + b64utf8(user + ":" + pass);
|
||
setStatus("authStatus", "Verbinde...", "");
|
||
try{
|
||
var all = [];
|
||
for(var page = 1; page <= 5; page++){
|
||
var batch = await apiFetch("/api/v1/user/repos?limit=50&page=" + page);
|
||
all = all.concat(batch);
|
||
if(batch.length < 50) break;
|
||
}
|
||
state.repos = all;
|
||
|
||
var orgs = [];
|
||
try{ orgs = await apiFetch("/api/v1/user/orgs?limit=50"); }catch(e){}
|
||
state.owners = [user].concat(orgs.map(function(o){ return o.username || o.name; }));
|
||
|
||
var ownerSel = el("ownerSelect");
|
||
ownerSel.innerHTML = state.owners.map(function(o){
|
||
return "<option value='" + o + "'>" + o + (o === user ? " (persönlich)" : "") + "</option>";
|
||
}).join("");
|
||
state.selectedOwner = state.owners[0];
|
||
refreshRepoOptions();
|
||
|
||
saveCredsIfWanted();
|
||
enable("repoCard");
|
||
setStatus("authStatus", "Verbunden. " + all.length + " Repo(s) gefunden.", "ok");
|
||
}catch(e){
|
||
setStatus("authStatus", "Login fehlgeschlagen: " + e.message, "err");
|
||
}
|
||
});
|
||
|
||
function refreshRepoOptions(){
|
||
var sel = el("repoSelect");
|
||
var filtered = state.repos.filter(function(r){ return r.owner.login === state.selectedOwner; });
|
||
if(filtered.length === 0){
|
||
sel.innerHTML = "<option value=''>– keine Repos in diesem Konto –</option>";
|
||
}else{
|
||
sel.innerHTML = "<option value=''>– bitte auswählen –</option>" +
|
||
filtered.map(function(r){
|
||
var idx = state.repos.indexOf(r);
|
||
return "<option value='" + idx + "'>" + r.name + (r.private ? " (privat)" : "") + "</option>";
|
||
}).join("");
|
||
}
|
||
state.repo = null;
|
||
disable("zipCard");
|
||
}
|
||
|
||
el("ownerSelect").addEventListener("change", function(){
|
||
state.selectedOwner = this.value;
|
||
refreshRepoOptions();
|
||
});
|
||
|
||
el("repoSelect").addEventListener("change", function(){
|
||
var i = this.value;
|
||
if(i === ""){ state.repo = null; disable("zipCard"); return; }
|
||
var r = state.repos[i];
|
||
state.repo = { owner: r.owner.login, name: r.name, defaultBranch: r.default_branch || "main" };
|
||
setStatus("repoStatus", "Ausgewählt: " + r.full_name + " (Branch: " + state.repo.defaultBranch + ")", "ok");
|
||
enable("zipCard");
|
||
});
|
||
|
||
// ---- Schritt 2: Neues Repo erstellen ----
|
||
el("btnCreateRepo").addEventListener("click", async function(){
|
||
var name = el("newRepoName").value.trim();
|
||
if(!name){ setStatus("repoStatus", "Bitte einen Repo-Namen eingeben.", "err"); return; }
|
||
setStatus("repoStatus", "Erstelle Repo...", "");
|
||
try{
|
||
var priv = el("newRepoPrivate").checked;
|
||
var isPersonal = state.selectedOwner === el("username").value.trim();
|
||
var path = isPersonal ? "/api/v1/user/repos" : "/api/v1/orgs/" + state.selectedOwner + "/repos";
|
||
var r = await apiFetch(path, {
|
||
method: "POST",
|
||
body: JSON.stringify({ name: name, private: priv, auto_init: true })
|
||
});
|
||
state.repos.push(r);
|
||
refreshRepoOptions();
|
||
var sel = el("repoSelect");
|
||
sel.value = state.repos.length - 1;
|
||
sel.dispatchEvent(new Event("change"));
|
||
setStatus("repoStatus", "Repo '" + r.full_name + "' erstellt und ausgewählt.", "ok");
|
||
}catch(e){
|
||
setStatus("repoStatus", "Erstellen fehlgeschlagen: " + e.message, "err");
|
||
}
|
||
});
|
||
|
||
// ---- Schritt 3: ZIP einlesen ----
|
||
el("zipFile").addEventListener("change", async function(){
|
||
var file = this.files[0];
|
||
if(!file) return;
|
||
setStatus("zipStatus", "Lese ZIP...", "");
|
||
try{
|
||
var zip = await JSZip.loadAsync(file);
|
||
var entries = [];
|
||
var names = Object.keys(zip.files);
|
||
for(var i = 0; i < names.length; i++){
|
||
var entry = zip.files[names[i]];
|
||
if(entry.dir) continue;
|
||
var b64 = await entry.async("base64");
|
||
entries.push({ path: entry.name, base64: b64 });
|
||
}
|
||
if(entries.length === 0){
|
||
setStatus("zipStatus", "ZIP enthält keine Dateien.", "err");
|
||
return;
|
||
}
|
||
if(el("stripRoot").checked){
|
||
var firstSlash = entries[0].path.indexOf("/");
|
||
if(firstSlash > 0){
|
||
var prefix = entries[0].path.substring(0, firstSlash + 1);
|
||
var allShare = entries.every(function(e){ return e.path.indexOf(prefix) === 0; });
|
||
if(allShare){
|
||
entries = entries.map(function(e){ return { path: e.path.substring(prefix.length), base64: e.base64 }; });
|
||
}
|
||
}
|
||
}
|
||
state.zipFiles = entries;
|
||
enable("uploadCard");
|
||
setStatus("zipStatus", entries.length + " Datei(en) gefunden, bereit zum Hochladen.", "ok");
|
||
el("fileList").textContent = entries.map(function(e){ return e.path; }).join("\n");
|
||
el("fileListDetails").style.display = "block";
|
||
}catch(e){
|
||
setStatus("zipStatus", "ZIP konnte nicht gelesen werden: " + e.message, "err");
|
||
}
|
||
});
|
||
|
||
// ---- Schritt 4: Hochladen ----
|
||
el("btnUpload").addEventListener("click", async function(){
|
||
if(!state.repo || !state.zipFiles){
|
||
setStatus("uploadStatus", "Bitte erst Repo wählen und ZIP einlesen.", "err");
|
||
return;
|
||
}
|
||
var btn = el("btnUpload");
|
||
btn.disabled = true;
|
||
el("log").innerHTML = "";
|
||
el("log").style.display = "block";
|
||
el("progressWrap").style.display = "block";
|
||
setStatus("uploadStatus", "Ermittle aktuellen Stand im Repo...", "");
|
||
|
||
try{
|
||
var existing = {};
|
||
try{
|
||
var tree = await apiFetch(
|
||
"/api/v1/repos/" + state.repo.owner + "/" + state.repo.name +
|
||
"/git/trees/" + encodeURIComponent(state.repo.defaultBranch) + "?recursive=true"
|
||
);
|
||
(tree.tree || []).forEach(function(t){ if(t.type === "blob") existing[t.path] = t.sha; });
|
||
}catch(e){
|
||
logLine("Hinweis: Konnte bestehenden Stand nicht lesen (evtl. leeres Repo) - " + e.message, false);
|
||
}
|
||
|
||
var newPaths = {};
|
||
var operations = state.zipFiles.map(function(f){
|
||
newPaths[f.path] = true;
|
||
var op = { path: f.path, content: f.base64 };
|
||
if(existing.hasOwnProperty(f.path)){ op.operation = "update"; op.sha = existing[f.path]; }
|
||
else{ op.operation = "create"; }
|
||
return op;
|
||
});
|
||
|
||
if(el("removeMissing").checked){
|
||
Object.keys(existing).forEach(function(p){
|
||
if(!newPaths[p]) operations.push({ path: p, operation: "delete", sha: existing[p] });
|
||
});
|
||
}
|
||
|
||
var batches = [];
|
||
var current = [];
|
||
var currentSize = 0;
|
||
var MAX_FILES = 20;
|
||
var MAX_BYTES = 3 * 1024 * 1024;
|
||
operations.forEach(function(op){
|
||
var size = op.content ? op.content.length : 0;
|
||
if(current.length >= MAX_FILES || (currentSize + size) > MAX_BYTES){
|
||
batches.push(current); current = []; currentSize = 0;
|
||
}
|
||
current.push(op);
|
||
currentSize += size;
|
||
});
|
||
if(current.length) batches.push(current);
|
||
|
||
var msg = el("commitMsg").value.trim() || "Upload per ZIP-Tool";
|
||
var done = 0, total = operations.length;
|
||
|
||
for(var b = 0; b < batches.length; b++){
|
||
setStatus("uploadStatus", "Lade Batch " + (b+1) + " von " + batches.length + " hoch...", "");
|
||
await apiFetch(
|
||
"/api/v1/repos/" + state.repo.owner + "/" + state.repo.name + "/contents",
|
||
{ method: "POST", body: JSON.stringify({
|
||
files: batches[b], message: msg + " (" + (b+1) + "/" + batches.length + ")",
|
||
branch: state.repo.defaultBranch
|
||
}) }
|
||
);
|
||
done += batches[b].length;
|
||
el("progressBar").style.width = Math.round(done / total * 100) + "%";
|
||
logLine("Batch " + (b+1) + "/" + batches.length + " ok (" + batches[b].length + " Datei(en))", false);
|
||
}
|
||
|
||
setStatus("uploadStatus", "Fertig! " + total + " Datei(en) hochgeladen/aktualisiert.", "ok");
|
||
}catch(e){
|
||
setStatus("uploadStatus", "Fehler: " + e.message, "err");
|
||
logLine("Abgebrochen: " + e.message, true);
|
||
}finally{
|
||
btn.disabled = false;
|
||
}
|
||
});
|
||
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|