Sieht hier dann so aus:
Stimmt!
Sieht hier dann so aus:
Stimmt!
das funktioniert hier mit einer kleinen Änderung so, wie ich es haben möchte
Prima, dann passt es.![]()
Ich nutze das folgende Script, wenn du dort die Zeilen 174 bis 183 auskommentierst, dann hast du das Ergebnis:

// basiert auf Sript von BrokenHeart
// Firefox 154+
(function() {
if (!window.gBrowser)
return;
setTimeout(function() {
setFunction();
}, 50);
// Custom icons in profile/chrome/icons folder
let ProfilePath = PathUtils.toFileURI(PathUtils.join(PathUtils.profileDir, 'chrome', 'icons'));
let icon1 = "Ordner.png";
let icon2 = "Bild36.png";
let cs_font_size = 12;
let cs_font_weight = 700;
let cs_font_color = "blue";
// Fix: feste Spaltenbreite (anpassen, wenn du mehrstellig erwartest)
let colWidthCh = 4;
function setFunction() {
const css = `
#bmContent {
font-family: "Courier New", monospace !important;
font-size: ${cs_font_size}px !important;
font-weight: ${cs_font_weight} !important;
display: flex !important;
align-items: center !important;
justify-content: flex-end !important;
/* nach rechts drücken innerhalb von menu-text */
margin-left: auto !important;
height: 16px !important;
white-space: nowrap !important;
margin-top: -14px !important;
}
.bmIcon1 {
width: 16px;
height: 16px;
background: url("${ProfilePath}/${icon1}") no-repeat center;
background-size: 16px 16px;
/* Icon kommt NACH dem Wert => Abstand links ist das relevante */
margin-left: 6px;
margin-right: 0;
margin-top: -2px;
flex: 0 0 auto;
}
.bmIcon2 {
width: 16px;
height: 16px;
background: url("${ProfilePath}/${icon2}") no-repeat center;
background-size: 12px 12px;
margin-left: 6px;
margin-right: 0;
margin-top: -2px;
flex: 0 0 auto;
}
.bmValue {
color: ${cs_font_color} !important;
flex: 0 0 auto;
min-width: ${colWidthCh}ch; /* gleiche Flucht über Zeilen */
text-align: right;
}
.bmSlash {
color: ${cs_font_color} !important;
margin-left: 3px !important;
margin-right: -7px !important;
}
/* Hover-Farbwechsel */
menupopup[placespopup="true"] > menu.bookmark-item:hover > .menu-text > #bmContent .bmValue {
color: red !important;
fill: red !important;
}
menupopup[placespopup="true"] > menu.bookmark-item:hover > .menu-text > #bmContent .bmSlash {
color: #000 !important;
fill: #000 !important;
}
`;
const sss = Cc['@mozilla.org/content/style-sheet-service;1'].getService(Ci.nsIStyleSheetService);
const uri = Services.io.newURI('data:text/css,' + encodeURIComponent(css));
sss.loadAndRegisterSheet(uri, sss.AGENT_SHEET);
let bookMenu = document.getElementById('bookmarksMenu');
let persToolBar = document.getElementById('PersonalToolbar');
if (bookMenu) {
bookMenu.addEventListener('popupshown', onPopupShowing);
}
if (persToolBar) {
persToolBar.addEventListener('popupshowing', onPopupShowing);
}
}
function onPopupShowing(aEvent) {
let popup = aEvent.originalTarget;
for (let item of popup.children) {
if (item.localName != 'menu' || item.id?.startsWith('history'))
continue;
setTimeout(() => {
let itemPopup = item.menupopup;
itemPopup.hidden = true;
itemPopup.collapsed = true;
itemPopup.openPopup();
itemPopup.hidePopup();
let menuitemCount = 0;
let menuCount = 0;
for (let subitem of itemPopup.children) {
if (subitem.classList.contains('bookmark-item') && !subitem.disabled && !subitem.hidden) {
if (subitem.localName == 'menuitem') {
menuitemCount++;
} else if (subitem.localName == 'menu') {
menuCount++;
}
}
}
itemPopup.hidden = false;
itemPopup.collapsed = false;
let bmCounta = item.childNodes[1];
bmCounta.innerHTML = "";
let bmContent = document.createElement("bmContent");
bmContent.id = "bmContent";
let strCountOut1 = menuCount; // Ordner
let strCountOut2 = menuitemCount; // Lesezeichen
// DOM: value1 + Icon1 + "/" + value2 + Icon2
let v1 = document.createElement("span");
v1.className = "bmValue";
v1.textContent = strCountOut1;
let icon1El = document.createElement("span");
icon1El.className = "bmIcon1";
let slash = document.createElement("span");
slash.className = "bmSlash";
slash.textContent = "/"; // ohne Leerzeichen
let v2 = document.createElement("span");
v2.className = "bmValue";
v2.textContent = strCountOut2;
let icon2El = document.createElement("span");
icon2El.className = "bmIcon2";
// Ausblenden-Logik: Wenn ein Zähler 0 ist, alles rechts davon konsequent ausblenden
if (strCountOut1 === 0) {
v1.style.display = "none";
icon1El.style.display = "none";
slash.style.display = "none";
}
if (strCountOut2 === 0) {
v2.style.display = "none";
icon2El.style.display = "none";
slash.style.display = "none";
}
bmContent.appendChild(v1);
bmContent.appendChild(icon1El);
bmContent.appendChild(slash);
bmContent.appendChild(v2);
bmContent.appendChild(icon2El);
bmCounta.appendChild(bmContent);
}, 100);
}
}
})();
Alles anzeigen
In dem Skript von hier #375 wird beim Entpacken des Archivs der gesamte Inhalt des Profilordners aufgeführt. Das habe ich in dem folgenden Skript dahingehend geändert, dass beim Entpacken der vollständige Profilordner mit seinem richtigen Namen erstellt wird. Ist sehr viel einfacher, wenn ein Archiv mal für die Herstellung des Profils genutzt werden muss.
// ==UserScript==
// @name ProfileBackupRestore@Fu_combo_zipRoot_withlog.uc.js
// @description Navbar: Backup(Ordner1 + ZIP2) + Restore(Backup1->Profil) + Log in Ordner1
// @version 2026.07-combo-zip-root-log
// ==/UserScript==
(function () {
"use strict";
if (location.href !== "chrome://browser/content/browser.xhtml") return;
if (typeof CustomizableUI === "undefined") return;
if (!window.gBrowser) return;
const PROFILES = [
{
label: "Reserve 3",
profilePath: "C:\\Users\\Old Man\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\Reserve 3",
profileName: "Reserve 3",
backup1Path: "G:\\BackupRestore Firefox\\Reserve 3",
archiveRootPath: "E:\\BackupRestore Firefox\\Reserve 3",
iconPath: "file:///C:/FoxIcons2/Finale.png"
},
{
label: "Portable.Firefox.Updater.3_Stable ",
profilePath: "G:\\Portable.Firefox.Updater.3\\Firefox Stable x64\\profile",
profileName: "profile",
backup1Path: "G:\\BackupRestore Firefox\\Portable.Firefox.Updater.3_Stable",
archiveRootPath: "E:\\BackupRestore Firefox\\Portable.Firefox.Updater.3_Stable",
iconPath: "file:///C:/FoxIcons2/Finale.png"
},
{
label: "Portable.Firefox.Updater.3_Nightly ",
profilePath: "G:\\Portable.Firefox.Updater.3\\Firefox Nightly x64\\profile",
profileName: "profile",
backup1Path: "G:\\BackupRestore Firefox\\Portable.Firefox.Updater.3_Nightly",
archiveRootPath: "E:\\BackupRestore Firefox\\Portable.Firefox.Updater.3_Nightly",
iconPath: "file:///C:/FoxIcons2/Nightly.png"
}
];
const BTN_ID = "profilebackup_restore_combo_ziproot_withlog";
const POPUP_ID = BTN_ID + "_popup";
const btnIconPath = "file:///C:/FoxIcons2/backup.png";
// =========================
// Logging helpers
// =========================
function pad2(n) {
return String(n).padStart(2, "0");
}
function timeHHMM(d) {
return "[" + pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + "]";
}
function formatHeaderDateGermanLong(d) {
const weekdays = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
const months = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"];
const wd = weekdays[d.getDay()];
const mn = months[d.getMonth()];
return "am " + wd + ", " +
pad2(d.getDate()) + ". " + mn + " " + d.getFullYear() + ", " +
pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds()) + " Uhr";
}
function getLogFile(profile) {
const f = new FileUtils.File(profile.backup1Path);
f.append("ProfileBackupRestore.log");
return f;
}
function ensureParentForFile(file) {
const parent = file.parent;
if (!parent || !parent.exists()) {
// backup1Path ist bei dir der Parent; falls nicht existiert, versuche zu erstellen
// (best effort)
try { parent.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); } catch (_) {}
}
}
function appendTextToFile(file, text) {
try {
ensureParentForFile(file);
if (!file.exists()) {
file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
}
const fos = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
fos.init(file, 0x02 | 0x08 | 0x10, 0o644, 0); // write | create | append
const conv = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
conv.init(fos, "UTF-8", 0, 0);
conv.writeString(text);
conv.close();
try { fos.close(); } catch (_) {}
} catch (e) {
console.error("Log Fehler:", e);
}
}
function logRestore(profile, destProfilePath) {
const dStart = new Date();
const header = "Profil in alten Zustand versetzt " + formatHeaderDateGermanLong(dStart) + "\n\n";
const lines =
timeHHMM(dStart) + " Restore gestartet (Firefox-Erkennung deaktiviert).\n" +
timeHHMM(dStart) + " Lösche vorhandenen Profilordner: " + destProfilePath + "\n" +
timeHHMM(dStart) + " Kopiere Sicherung nach: " + destProfilePath + "\n" +
timeHHMM(new Date()) + " Restore erfolgreich abgeschlossen.\n";
appendTextToFile(getLogFile(profile), header + "\n" + lines + "\n");
}
function logBackup(profile, backupTargetPath, zipPath) {
const dStart = new Date();
const header = "Profil gesichert " + formatHeaderDateGermanLong(dStart) + "\n\n";
const lines =
timeHHMM(dStart) + " Backup gestartet (Firefox-Erkennung deaktiviert).\n" +
timeHHMM(dStart) + " Lösche vorhandenen Profilordner: " + backupTargetPath + "\n" +
timeHHMM(dStart) + " Kopiere Profil nach: " + backupTargetPath + "\n" +
timeHHMM(new Date()) + " Erzeuge Archiv ZIP: " + zipPath + "\n" +
timeHHMM(new Date()) + " Backup erfolgreich abgeschlossen.\n";
appendTextToFile(getLogFile(profile), header + "\n" + lines + "\n");
}
// =========================
// Alert
// =========================
function showAlert(text) {
try {
let w = null;
try { w = Services.wm.getMostRecentWindow("navigator:browser"); } catch (_) {}
Services.prompt.alert(w || null, "Profilsicherung", text);
} catch (e) {
console.error("Alert Fehler:", e);
}
}
// =========================
// Date/Zähler ZIP2
// =========================
function pad2Str(n) {
const x = parseInt(n, 10);
if (isNaN(x)) return "00";
return pad2(x);
}
function getDateStrYYYYMMDD(d) {
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
}
function escapeRegExp(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function getNextDailyZipIndex(archiveRootFile, profileName, dateStr) {
let max = 0;
const re = new RegExp("^" + escapeRegExp(profileName) + "_" + escapeRegExp(dateStr) + "_(\\d{2})\\.zip$");
let entries = archiveRootFile.directoryEntries;
while (entries.hasMoreElements()) {
const entry = entries.getNext().QueryInterface(Ci.nsIFile);
if (!entry.isFile()) continue;
const m = re.exec(entry.leafName);
if (m) {
const n = parseInt(m[1], 10);
if (!isNaN(n) && n > max) max = n;
}
}
return max + 1; // => startet bei 01
}
// =========================
// Skip-Dateien (WAL/SHM/Locks)
// =========================
function shouldSkipEntryByName(name) {
if (name === "parent.lock") return true;
if (name === "lock") return true;
if (name === "recovery.jsonlz4") return true;
if (name.endsWith(".sqlite-wal")) return true;
if (name.endsWith(".sqlite-shm")) return true;
return false;
}
// =========================
// Copy helpers
// =========================
function copyDirectoryContents(srcDir, destDir) {
let entries = srcDir.directoryEntries;
while (entries.hasMoreElements()) {
const entry = entries.getNext().QueryInterface(Ci.nsIFile);
if (shouldSkipEntryByName(entry.leafName)) continue;
const destEntry = destDir.clone();
destEntry.append(entry.leafName);
try {
if (entry.isDirectory()) {
if (!destEntry.exists()) destEntry.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
copyDirectoryContents(entry, destEntry);
} else {
if (destEntry.exists()) destEntry.remove(false);
entry.copyTo(destDir, entry.leafName);
}
} catch (e) {
console.warn("⚠️ Kopie fehlgeschlagen:", entry.path, e);
}
}
}
function clearDirectoryContents(dir) {
let entries = dir.directoryEntries;
while (entries.hasMoreElements()) {
const entry = entries.getNext().QueryInterface(Ci.nsIFile);
try { entry.remove(true); } catch (e) {
console.warn("⚠️ Clear fehlgeschlagen:", entry.path, e);
}
}
}
// =========================
// ZIP erstellen: Inhalt + Root = profile.profileName
// =========================
function zipDirectoryWithRoot(sourceDir, zipFile, rootNameInZip) {
const baseDirPath = sourceDir.path;
if (zipFile.exists()) zipFile.remove(true);
const zipWriter = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
const PR_WRONLY = 0x02;
const PR_CREATE_FILE = 0x08;
const PR_TRUNCATE = 0x20;
zipWriter.open(zipFile, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE);
function walk(dir) {
let entries = dir.directoryEntries;
while (entries.hasMoreElements()) {
const entry = entries.getNext().QueryInterface(Ci.nsIFile);
if (entry.path === zipFile.path) continue;
if (shouldSkipEntryByName(entry.leafName)) continue;
if (entry.isDirectory()) {
walk(entry);
continue;
}
// relPath relativ zum sourceDir, ohne führenden Slash
let relPath = entry.path.replace(baseDirPath, "");
if (!relPath) return;
if (relPath[0] === "\\" || relPath[0] === "/") relPath = relPath.substring(1);
const saveInZipAs = (rootNameInZip + "/" + relPath).replace(/\\/g, "/");
try {
zipWriter.addEntryFile(
saveInZipAs,
Ci.nsIZipWriter.COMPRESSION_FASTEST,
entry,
false
);
} catch (e) {
console.warn("⚠️ ZIP Eintrag fehlgeschlagen:", saveInZipAs, e);
}
}
}
walk(sourceDir);
zipWriter.close();
}
// =========================
// Backup (Ordner1 + ZIP2)
// =========================
function runBackup(profile) {
let backupTarget = null;
let zipFile = null;
try {
const now = new Date();
const profileDir = new FileUtils.File(profile.profilePath);
if (!profileDir.exists()) throw new Error("Profilordner existiert nicht: " + profile.profilePath);
const backupRoot = new FileUtils.File(profile.backup1Path);
if (!backupRoot.exists()) backupRoot.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
const archiveRoot = new FileUtils.File(profile.archiveRootPath);
if (!archiveRoot.exists()) archiveRoot.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
// BACKUP 1 Ziel: backupRoot/<profileName>
backupTarget = backupRoot.clone();
backupTarget.append(profile.profileName);
if (backupTarget.exists()) backupTarget.remove(true);
backupTarget.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
copyDirectoryContents(profileDir, backupTarget);
// BACKUP 2 ZIP: archiveRoot/<profileName>_YYYY-MM-DD_XX.zip (XX ab 01)
const dateStr = getDateStrYYYYMMDD(now);
const nextIdx = getNextDailyZipIndex(archiveRoot, profile.profileName, dateStr);
const idxStr = pad2Str(nextIdx);
const zipName = profile.profileName + "_" + dateStr + "_" + idxStr + ".zip";
zipFile = archiveRoot.clone();
zipFile.append(zipName);
// ZIP-Inhalt soll als Root den profile.profileName Ordner haben.
// Wir zippen den Inhalt von backupTarget, Root in der ZIP = profile.profileName.
zipDirectoryWithRoot(backupTarget, zipFile, profile.profileName);
if (!zipFile.exists()) throw new Error("ZIP wurde nicht erstellt: " + zipFile.path);
// Log in Ordner 1
try { logBackup(profile, backupTarget.path, zipFile.path); } catch (_) {}
setTimeout(() => {
showAlert(
"✅ Backup abgeschlossen!\n\n" +
"👤 Profil: " + profile.label + "\n\n" +
"📁 Backup Ordner 1: " + backupTarget.path + "\n" +
"📦 Archiv ZIP: " + zipFile.path
);
}, 50);
} catch (e) {
console.error("Backup Fehler:", e);
// Best effort Cleanup ZIP, falls vorhanden
try { if (zipFile && zipFile.exists()) zipFile.remove(false); } catch (_) {}
setTimeout(() => showAlert("❌ Backup Fehler: " + e.message), 0);
}
}
// =========================
// Restore (Backup1 -> Profil überschreibt; Wrapper fix!)
// =========================
function runRestore(profile) {
let profileDir = null;
let backupRootDir = null;
let backupProfileDir = null;
try {
profileDir = new FileUtils.File(profile.profilePath);
if (!profileDir.exists()) throw new Error("Zielprofil existiert nicht: " + profile.profilePath);
backupRootDir = new FileUtils.File(profile.backup1Path);
if (!backupRootDir.exists()) throw new Error("Backup1 Ordner existiert nicht: " + profile.backup1Path);
// Wrapper fix:
// Backup1 enthält i.d.R. <profileName>/... -> daraus restaurieren:
backupProfileDir = backupRootDir.clone();
backupProfileDir.append(profile.profileName);
// Fallback falls Backup1 doch direkt Root ist
if (!backupProfileDir.exists()) backupProfileDir = backupRootDir;
const ok = Services.prompt.confirm(
null,
"Profilsicherung (Restore)",
"Restore jetzt starten für: " + profile.label + "\n\n" +
"Quelle: " + backupProfileDir.path + "\n" +
"Ziel: " + profileDir.path + "\n\n" +
"WICHTIG: Firefox bitte vorher vollständig beenden.\nFortfahren?"
);
if (!ok) return;
clearDirectoryContents(profileDir);
copyDirectoryContents(backupProfileDir, profileDir);
try { logRestore(profile, profileDir.path); } catch (_) {}
setTimeout(() => {
showAlert(
"✅ Restore abgeschlossen!\n\n" +
"👤 Profil: " + profile.label + "\n\n" +
"📁 Quelle: " + backupProfileDir.path + "\n" +
"📁 Ziel: " + profileDir.path + "\n\n" +
"Hinweis: Starte Firefox danach neu."
);
}, 50);
} catch (e) {
console.error("Restore Fehler:", e);
setTimeout(() => showAlert("❌ Restore Fehler: " + e.message), 0);
}
}
// =========================
// Menü
// =========================
function createMenu(doc) {
const menupopup = doc.createXULElement("menupopup");
menupopup.setAttribute("id", POPUP_ID);
PROFILES.forEach((profile) => {
const backupItem = doc.createXULElement("menuitem");
backupItem.setAttribute("label", "Backup: " + profile.label);
backupItem.setAttribute("class", "menuitem-iconic");
if (profile.iconPath) backupItem.setAttribute("image", profile.iconPath);
backupItem.addEventListener("command", () => {
const ok = Services.prompt.confirm(
null,
"Profilsicherung",
"Backup jetzt starten für: " + profile.label + " ?"
);
if (ok) runBackup(profile);
});
menupopup.appendChild(backupItem);
});
const sep = doc.createXULElement("menuseparator");
menupopup.appendChild(sep);
PROFILES.forEach((profile) => {
const restoreItem = doc.createXULElement("menuitem");
restoreItem.setAttribute("label", "Restore: " + profile.label);
restoreItem.setAttribute("class", "menuitem-iconic");
if (profile.iconPath) restoreItem.setAttribute("image", profile.iconPath);
restoreItem.addEventListener("command", () => runRestore(profile));
menupopup.appendChild(restoreItem);
});
return menupopup;
}
// =========================
// Button Patch
// =========================
function patchButton(btn) {
if (!btn) return false;
const oldPopup = btn.querySelector("#" + POPUP_ID);
if (oldPopup) oldPopup.remove();
btn.setAttribute("type", "menu");
btn.setAttribute("aria-haspopup", "true");
btn.setAttribute("tooltiptext", "Backup & Restore");
btn.setAttribute("image", btnIconPath);
btn.appendChild(createMenu(btn.ownerDocument));
btn.addEventListener("click", (ev) => {
if (ev.button !== 0) return;
try {
const p = btn.querySelector("#" + POPUP_ID);
if (p) p.openPopup(btn, "after_start", 0, 0, false, false);
} catch (_) {}
}, false);
return true;
}
// =========================
// Widget erstellen
// =========================
try {
if (CustomizableUI.getWidget && CustomizableUI.getWidget(BTN_ID)) {
try { CustomizableUI.destroyWidget(BTN_ID); } catch (_) {}
}
} catch (_) {}
try {
const el = document.getElementById(BTN_ID);
if (el) el.remove();
} catch (_) {}
CustomizableUI.createWidget({
id: BTN_ID,
defaultArea: CustomizableUI.AREA_NAVBAR,
label: "Profilsicherung",
tooltiptext: "Backup & Restore (ZIP Root = profileName) + Log",
onCreated: (button) => setTimeout(() => patchButton(button), 0)
});
// =========================
// CSS (dein Layout)
// =========================
const css = `
#${BTN_ID} .toolbarbutton-icon {
width: 31px !important;
height: 31px !important;
padding: 5px !important;
}
#${BTN_ID}_popup {
max-width: 250px !important;
min-width: 250px !important;
}
#${BTN_ID}_popup menuitem.menuitem-iconic img.menu-icon {
margin-left: -28px !important;
}
#${BTN_ID}_popup menuitem.menuitem-iconic label.menu-text {
margin-left: 6px !important;
}
`;
const sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
const ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
const uri = ios.newURI("data:text/css;charset=utf-8," + encodeURIComponent(css), null, null);
sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
})();
Alles anzeigen
In dieser Version funktioniert Lesezeichen jetzt auch:
Danke!![]()
Erstell dir vorher aber zur Sicherheit ein Backup
Das hätte ich glatt vergessen, danke.![]()
![]()
Sicherung ja - Rücksicherung noch nicht getetstet
Ja, gut, dann werde ich mal probieren.![]()
Im Nightly gibt es auch das noch:
Habe ich mir angesehen, ist nicht so mein Ding. Schuster bleib bei deinen Leisten, so heißt es wohl.![]()
Einstellungen -> Konto und Synchronisation -> ganz unten...
Danke, das hatte ich noch nicht auf dem Schirm, muss ich gleich mal testen. Hast du das schon damit gemacht?
falls das unbemerkt geblieben sein sollte...
Ist es wirklich, kläre mich auf.![]()
aber es gibt schon ein Skript welches so gar im laufenden Betrieb das Profil sichert.
Ich habe nicht gesagt, dass es mit meinem Script nicht geht, Backup geht natürlich auch, aber ich würde es nicht bei laufendem Betrieb machen. Und Backup zurück spielen, da erübrigt sich ja wohl eine Kommentierung.
habe ich es bisher noch nicht geschafft,
ein "gutes" Restore-Skript zu zaubern.
Da musst du hier RE: BackupProfile.uc.js - div. Fragen dazu schauen. Und nur zur Kenntnis, ich arbeite nur mit PowerShell-Scripten.
Ich finde Deine Bemühungen um ein Skript zum Sichern des Profils ja echt klasse,
aber es gibt schon ein Skript
Danke, ja das kenne ich alles. Es wird niemand gezwungen mein Script zu nutzen, freie Entscheidung.![]()
Skript: ProfileBackupRestore@Fu_combo_zip2.uc.js (mit KI erstellt)
=====================================================================
Vorab ein wichtiger Hinweis, und zugleich als Empfehlung, das Script nur in dem Profil einzusetzen (starten), welches nicht selbst für ein Backup oder ein Restore vorgesehen ist. Der Grund ist der, dass für Restore der Firefox zwingend beendet sein muss, und für ein Backup es auch von Vorteil ist, um Komplikationen zu vermeiden.
==============================================================
Edit:
Ich bin immer von der Logik ausgegangen, dass es so sein muss, wegen Datenverlust, usw. Jetzt habe ich bei installierten, als auch bei portablen Profilen im laufenden Betrieb Backup getestet, läuft ohne Fehler durch. Und genau so verhält es sich bei Restore, nur einen Restart danach und alles läuft fehlerfrei.
==============================================================
Dieses Script ist eine Kombination für Backup und Restore (Profilordner zurück spielen). Vorteil hierbei ist, es werden für jedes Profil nur einmal die Pfade benötigt, die sorgfältig eingetragen werden sollten.
Vor Nutzung des Skriptes müssen zwei Ordner für die Sicherungen eingerichtet werden ( Beispiele aus dem Skript ersichtlich ). In den ersten Ordner wird dann der vollständige Profilordner hinein kopiert (kein ZIP), wobei ganz wichtig ist, den richtigen Namen des Profils in das Skript einzutragen (about:profiles aufrufen und Namen entnehmen, z.B. qtqwpmy7.default-release). Bei jedem neuen Backup wird
der Profilordner hier überschrieben. Auch portable können mit entsprechenden Angaben (Pfade) gesichert werden.
Der zweite Ordner gilt als doppelte Sicherung, hier wird auch der Profilordner hinein kopiert (als ZIP-Archiv), hat als Endung aber eine fortlaufende Nummerierung mit Datum und Zähler (wichtig wenn mehrmals gesichert wird).
Die Pfade zu diesen Ordnern sind bei jedem Profil in das Skript einzutragen.
Das Skript wird über den Button in der Navbar gestartet, es öffnet sich ein Menü (Grafik 1) über das eine Auswahl für Backup oder Restore getroffen werden kann, bei Klick darauf erscheint ein Bestätigungs-Dialog (Grafik 2), der bedient werden muss. Bei OK startet das Backup oder der Restore-Vorgang, bei Erfolg wird dann über Windows eine Benachrichtigung ausgegeben (Grafik 3). Außerdem wird eine Log-Datei im Sicherungsordner 1 angelegt, die bei jeder Aktion fortgeschrieben wird (Grafik 4).
// ==UserScript==
// @name ProfileBackupRestore@Fu_combo_zip2.uc.js
// @description Navbar: Backup (Backup1 Ordner + Archiv ZIP) + Restore aus Backup1 (überschreibt Profil) + Log in Backup1
// @version 2026.07-combo-zip2
// ==/UserScript==
(function () {
"use strict";
if (location.href !== "chrome://browser/content/browser.xhtml") return;
if (typeof CustomizableUI === "undefined") return;
if (!window.gBrowser) return;
// =========================
// Pro Profil konfigurieren
// =========================
const PROFILES = [
{
label: "Nightly2",
profilePath: "G:\\Firefox Test\\Nightly2\\Profilordner",
profileName: "Profilordner",
backup1Path: "G:\\Firefox Sicherung\\Nightly2",
archiveRootPath: "G:\\Sicherung2\\Nightly2",
iconPath: "file:///C:/FoxIcons2/Nightly.png"
},
{
label: "Beta1",
profilePath: "G:\\Firefox Test\\Beta1\\Profilordner",
profileName: "Profilordner",
backup1Path: "G:\\Firefox Sicherung\\Beta1",
archiveRootPath: "G:\\Sicherung2\\Beta1",
iconPath: "file:///C:/FoxIcons2/Beta.png"
},
{
label: "Reserve 3",
profilePath: "C:\\Users\\Old Man\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\Reserve 3",
profileName: "Reserve 3",
backup1Path: "G:\\Firefox Sicherung\\Reserve 3",
archiveRootPath: "G:\\Sicherung2\\Reserve 3",
iconPath: "file:///C:/FoxIcons2/Finale.png"
}
];
// =========================
// UI IDs
// =========================
const BTN_ID = "profilebackup_restore_combo_button_fixed_zip2_noroot_withlog";
const POPUP_ID = BTN_ID + "_popup";
const btnIconPath = "file:///C:/FoxIcons2/backup.png";
// =========================
// Helpers: Logging
// =========================
function pad2(n) {
return String(n).padStart(2, "0");
}
function timeHHMM(d) {
return "[" + pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + "]";
}
function formatHeaderDateGermanLong(d) {
// Ziel: "am Mittwoch, 08. Juli 2026, 09:26:01 Uhr"
try {
return "am " + d.toLocaleString("de-DE", {
weekday: "long",
year: "numeric",
month: "long",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
}) + " Uhr";
} catch (_) {
// Fallback, falls toLocaleString Optionen nicht sauber unterstützt werden
const weekdays = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
const wd = weekdays[d.getDay()] || "";
const day = pad2(d.getDate());
const monthNames = ["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"];
const mn = monthNames[d.getMonth()] || "";
return "am " + wd + ", " + day + ". " + mn + " " + d.getFullYear() + ", " +
pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds()) + " Uhr";
}
}
function getLogFile(profile) {
// Log-Datei im Ordner 1 (backup1Path)
let f = new FileUtils.File(profile.backup1Path);
f.append("ProfileBackupRestore.log");
return f;
}
function ensureLogFileExists(file) {
if (!file.exists()) {
// parent muss existieren; backup1Path sollte existieren
file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
}
}
function appendTextToFile(file, text) {
ensureLogFileExists(file);
const fos = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
// write (0x02) | create (0x08) | append (0x10)
fos.init(file, 0x02 | 0x08 | 0x10, 0o644, 0);
const conv = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
conv.init(fos, "UTF-8", 0, 0);
conv.writeString(text);
conv.close();
try { fos.close(); } catch (_) {}
}
function logBackup(profile, zipPath) {
const d = new Date();
const logFile = getLogFile(profile);
const header = "Profil gesichert " + formatHeaderDateGermanLong(d) + "\n\n";
const lines = [
timeHHMM(d) + " Backup gestartet (Firefox-Erkennung deaktiviert).\n",
timeHHMM(d) + " Kopiere Profil nach Backup1: " + profile.backup1Path + "\\" + profile.profileName + "\n",
timeHHMM(d) + " Erzeuge Archiv ZIP: " + (zipPath || "(unbekannt)") + "\n",
timeHHMM(new Date()) + " Backup erfolgreich abgeschlossen.\n"
].join("");
appendTextToFile(logFile, header + "\n" + lines + "\n");
}
function logRestore(profile, sourceDirPath, destDirPath) {
const d = new Date();
const logFile = getLogFile(profile);
const header = "Profil in alten Zustand versetzt " + formatHeaderDateGermanLong(d) + "\n\n";
const lines = [
timeHHMM(d) + " Restore gestartet (Firefox-Erkennung deaktiviert).\n",
timeHHMM(d) + " Lösche vorhandenen Profilordner: " + destDirPath + "\n",
timeHHMM(d) + " Kopiere Sicherung nach: " + destDirPath + "\n",
timeHHMM(new Date()) + " Restore erfolgreich abgeschlossen.\n"
].join("");
// Hinweis: sourceDirPath ist enthalten, aber nach deinem Muster stehen nur die drei Zeilen.
// Falls du es zusätzlich willst, sag kurz Bescheid.
appendTextToFile(logFile, header + lines + "\n");
}
// =========================
// UI Helpers: Alert
// =========================
function showAlert(text) {
try {
let w = null;
try { w = Services.wm.getMostRecentWindow("navigator:browser"); } catch (_) {}
Services.prompt.alert(w || null, "Profilsicherung", text);
} catch (e) {
console.error("Alert Fehler:", e);
}
}
// =========================
// Date/ZIP helpers
// =========================
function pad2Str(n) {
const x = parseInt(n, 10);
return pad2(isNaN(x) ? 0 : x);
}
function getDateStrYYYYMMDD(d) {
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
}
function escapeRegExp(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// =========================
// gesperrte Dateien ignorieren
// =========================
function shouldSkipEntryByName(name) {
if (
name === "parent.lock" ||
name === "lock" ||
name === "recovery.jsonlz4" ||
name.endsWith(".sqlite-wal") ||
name.endsWith(".sqlite-shm")
) return true;
return false;
}
// =========================
// Copy helpers (Ordnerkopie)
// =========================
function copyDirectoryContents(srcDir, destDir) {
let entries = srcDir.directoryEntries;
while (entries.hasMoreElements()) {
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
if (shouldSkipEntryByName(entry.leafName)) continue;
const destEntry = destDir.clone();
destEntry.append(entry.leafName);
try {
if (entry.isDirectory()) {
if (!destEntry.exists()) destEntry.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
copyDirectoryContents(entry, destEntry);
} else {
if (destEntry.exists()) destEntry.remove(false);
entry.copyTo(destDir, entry.leafName);
}
} catch (e) {
console.warn("⚠️ Datei konnte nicht kopiert werden:", entry.path, e);
}
}
}
function clearDirectoryContents(dir) {
let entries = dir.directoryEntries;
while (entries.hasMoreElements()) {
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
try { entry.remove(true); } catch (e) { console.warn("⚠️ Clear fehlgeschlagen:", entry.path, e); }
}
}
// =========================
// Backup2 ZIP-Index pro Tag (01..)
// Muster: <profileName>_YYYY-MM-DD_XX.zip
// =========================
function getNextDailyZipIndex(archiveRootFile, profileName, dateStr) {
let max = 0;
const re = new RegExp("^" + escapeRegExp(profileName) + "_" + escapeRegExp(dateStr) + "_(\\d{2})\\.zip$");
let entries = archiveRootFile.directoryEntries;
while (entries.hasMoreElements()) {
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
if (!entry.isFile()) continue;
const m = re.exec(entry.leafName);
if (m) {
const n = parseInt(m[1], 10);
if (!isNaN(n) && n > max) max = n;
}
}
return max + 1;
}
// =========================
// ZIP-Erstellung: Inhalt ohne Root-Ebene
// =========================
function zipDirectoryNoRoot(sourceDir, zipFile) {
// ZIP Inhalt: relativ zu sourceDir, ohne Extra Root-Ordner
const baseDirPath = sourceDir.path;
if (zipFile.exists()) zipFile.remove(true);
const zipWriter = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
const PR_WRONLY = 0x02;
const PR_CREATE_FILE = 0x08;
const PR_TRUNCATE = 0x20;
zipWriter.open(zipFile, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE);
function walk(dir) {
let entries = dir.directoryEntries;
while (entries.hasMoreElements()) {
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
if (entry.path === zipFile.path) continue;
if (shouldSkipEntryByName(entry.leafName)) continue;
let relPath = entry.path.replace(baseDirPath, "");
if (!relPath) continue;
if (relPath[0] === "\\" || relPath[0] === "/") relPath = relPath.substring(1);
const saveInZipAs = relPath.replace(/\\/g, "/");
try {
if (entry.isDirectory()) {
// Verzeichniseintrag ist optional; wir laufen rekursiv weiter
walk(entry);
} else {
zipWriter.addEntryFile(
saveInZipAs,
Ci.nsIZipWriter.COMPRESSION_FASTEST,
entry,
false
);
}
} catch (_) {
// gesperrte Dateien o.ä. ignorieren
}
}
}
walk(sourceDir);
zipWriter.close();
}
// =========================
// Backup & Restore
// =========================
function runBackup(profile) {
let backupTarget = null;
let zipFile = null;
try {
const now = new Date();
const profileDir = new FileUtils.File(profile.profilePath);
if (!profileDir.exists()) throw new Error("Profilordner existiert nicht: " + profile.profilePath);
const backupRoot = new FileUtils.File(profile.backup1Path);
if (!backupRoot.exists()) backupRoot.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
const archiveRoot = new FileUtils.File(profile.archiveRootPath);
if (!archiveRoot.exists()) archiveRoot.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
// BACKUP 1 (Ordner)
backupTarget = backupRoot.clone();
backupTarget.append(profile.profileName);
if (backupTarget.exists()) backupTarget.remove(true);
backupTarget.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
copyDirectoryContents(profileDir, backupTarget);
// BACKUP 2 (ZIP in archiveRootPath)
const dateStr = getDateStrYYYYMMDD(now);
const nextIdx = getNextDailyZipIndex(archiveRoot, profile.profileName, dateStr);
const idxStr = pad2Str(nextIdx);
const zipName = profile.profileName + "_" + dateStr + "_" + idxStr + ".zip";
zipFile = archiveRoot.clone();
zipFile.append(zipName);
// ZIP-Inhalt aus backupTarget (ohne Root-Ebene)
zipDirectoryNoRoot(backupTarget, zipFile);
if (!zipFile.exists()) {
throw new Error("ZIP wurde nicht erstellt: " + zipFile.path);
}
// Log schreiben
try { logBackup(profile, zipFile.path); } catch (_) {}
setTimeout(() => {
showAlert(
"✅ Backup abgeschlossen!\n\n" +
"👤 Profil: " + profile.label + "\n\n" +
"📁 Backup Ordner 1: " + backupTarget.path + "\n" +
"📦 Archiv ZIP: " + zipFile.path
);
}, 50);
} catch (e) {
console.error("Backup Fehler:", e);
try {
if (zipFile && zipFile.exists()) zipFile.remove(false);
} catch (_) {}
setTimeout(() => showAlert("❌ Backup Fehler: " + e.message), 0);
}
}
function runRestore(profile) {
let profileDir = null;
let backupRootDir = null;
let backupProfileDir = null;
try {
profileDir = new FileUtils.File(profile.profilePath);
if (!profileDir.exists()) throw new Error("Zielprofil existiert nicht: " + profile.profilePath);
backupRootDir = new FileUtils.File(profile.backup1Path);
if (!backupRootDir.exists()) throw new Error("Backup1 Ordner existiert nicht: " + profile.backup1Path);
// Backup1 enthält typischerweise <profileName>/... -> backupProfileDir ist diese Ebene
backupProfileDir = backupRootDir.clone();
backupProfileDir.append(profile.profileName);
// Fallback falls Backup1 doch schon direkt der Root ist
if (!backupProfileDir.exists()) backupProfileDir = backupRootDir;
const ok = Services.prompt.confirm(
null,
"Profilsicherung (Restore)",
"Restore jetzt starten für: " + profile.label + "\n\n" +
"Quelle: " + backupProfileDir.path + "\n" +
"Ziel: " + profileDir.path + "\n\n" +
"WICHTIG: Firefox bitte vorher vollständig beenden.\nFortfahren?"
);
if (!ok) return;
// Ziel leeren
clearDirectoryContents(profileDir);
// Zurückkopieren
copyDirectoryContents(backupProfileDir, profileDir);
// Log schreiben (wie im Muster: alte Zustand...)
try { logRestore(profile, backupProfileDir.path, profileDir.path); } catch (_) {}
setTimeout(() => {
showAlert(
"✅ Restore abgeschlossen!\n\n" +
"👤 Profil: " + profile.label + "\n\n" +
"📁 Quelle: " + backupProfileDir.path + "\n" +
"📁 Ziel: " + profileDir.path + "\n\n" +
"Hinweis: Starte Firefox danach neu."
);
}, 50);
} catch (e) {
console.error("Restore Fehler:", e);
setTimeout(() => showAlert("❌ Restore Fehler: " + e.message), 0);
}
}
// =========================
// Menü erstellen
// =========================
function createMenu(doc) {
const menupopup = doc.createXULElement("menupopup");
menupopup.setAttribute("id", POPUP_ID);
// Backup Gruppe
PROFILES.forEach((profile) => {
const menuitem = doc.createXULElement("menuitem");
menuitem.setAttribute("label", "Backup: " + profile.label);
menuitem.setAttribute("class", "menuitem-iconic");
if (profile.iconPath) menuitem.setAttribute("image", profile.iconPath);
menuitem.addEventListener("command", () => {
const ok = Services.prompt.confirm(
null,
"Profilsicherung",
"Backup jetzt starten für: " + profile.label + " ?"
);
if (ok) runBackup(profile);
});
menupopup.appendChild(menuitem);
});
const sep = doc.createXULElement("menuseparator");
menupopup.appendChild(sep);
// Restore Gruppe
PROFILES.forEach((profile) => {
const menuitem = doc.createXULElement("menuitem");
menuitem.setAttribute("label", "Restore: " + profile.label);
menuitem.setAttribute("class", "menuitem-iconic");
if (profile.iconPath) menuitem.setAttribute("image", profile.iconPath);
menuitem.addEventListener("command", () => runRestore(profile));
menupopup.appendChild(menuitem);
});
return menupopup;
}
// =========================
// Button patchen
// =========================
function patchButton(btn) {
if (!btn) return false;
const oldPopup = btn.querySelector("#" + POPUP_ID);
if (oldPopup) oldPopup.remove();
btn.setAttribute("type", "menu");
btn.setAttribute("aria-haspopup", "true");
btn.setAttribute("tooltiptext", "Backup & Restore");
btn.setAttribute("image", btnIconPath);
btn.appendChild(createMenu(btn.ownerDocument));
btn.addEventListener("click", (ev) => {
if (ev.button !== 0) return;
try {
const p = btn.querySelector("#" + POPUP_ID);
if (p) p.openPopup(btn, "after_start", 0, 0, false, false);
} catch (_) {}
}, false);
return true;
}
// =========================
// Widget erstellen
// =========================
try {
if (CustomizableUI.getWidget && CustomizableUI.getWidget(BTN_ID)) {
try { CustomizableUI.destroyWidget(BTN_ID); } catch (_) {}
}
} catch (_) {}
try {
const el = document.getElementById(BTN_ID);
if (el) el.remove();
} catch (_) {}
CustomizableUI.createWidget({
id: BTN_ID,
defaultArea: CustomizableUI.AREA_NAVBAR,
label: "Profilsicherung",
tooltiptext: "Backup & Restore (mit Log)",
onCreated: (button) => setTimeout(() => patchButton(button), 0)
});
// =========================
// CSS (wie bei dir)
// =========================
const css = `
#${BTN_ID} .toolbarbutton-icon {
width: 31px !important;
height: 31px !important;
padding: 5px !important;
}
#${POPUP_ID} {
max-width: 250px !important;
min-width: 250px !important;
}
#${POPUP_ID} menuitem.menuitem-iconic img.menu-icon {
margin-left: -28px !important;
}
#${POPUP_ID} menuitem.menuitem-iconic label.menu-text {
margin-left: 6px !important;
}
`;
const sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
const ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
const uri = ios.newURI("data:text/css;charset=utf-8," + encodeURIComponent(css), null, null);
sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
})();
Alles anzeigen
Das war die Lösung. Habe meinen Profilpfad nun auch ganz angegeben und damit geht es.
Alles wird gut.![]()
Im ersten Backup Ordner erstellt das Script keine Zip Datei, kopiert nur
die Dateien rein.
Ja, Endor, das ist so gewollt, und zwar aus folgendem Grund. Schon lange nutze ich PowerShell-Scripte, um ein erstelltes Backup, um schnell wieder den Profilordner zurück zu bringen. Und da muss der Ordner genau so zur Verfügung stehen, wie er im System vorhanden ist. Ich gebe dir mal ein Beispiel aus dem Skript aus #362 für das Profil Reserve 3 profilePath: "C:\\Users\\Old Man\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\Reserve 3",, dafür würde das ps1-Script dann so aussehen:
# Reserve 3 Retour.ps1
# =========================
# Einstellungen (Restore)
# =========================
# 1. Ziel-Profilordner (wird überschrieben)
$ProfilePath = "C:\\Users\\Old Man\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\Reserve 3"
# 2. Quelle: gesicherter Profilordner
$BackupProfile = "G:\\Firefox Sicherung\\Reserve 3\\Reserve 3"
# 3. Pfad für Log-Datei
$LogPfad = "G:\\Firefox Sicherung\\Reserve 3"
# =============================
# Ab hier besser nichts ändern
# =============================
Add-Type -AssemblyName System.Windows.Forms
function Show-Info {
param([string]$Msg)
Write-Host ("{0} - {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Msg)
}
# Feste Log-Datei für Restore
$LogFile = Join-Path $LogPfad "Retour.txt"
# Alte Log-Datei löschen, falls vorhanden
if (Test-Path -Path $LogFile) {
Remove-Item -Path $LogFile -Force
}
Start-Sleep -Seconds 1
# neue Log-Datei anlegen
$LogFile = (New-Item $LogFile -ItemType File -Force).FullName
# Überschrift für das LogFile
Add-Content $LogFile ("Profil in alten Zustand versetzt am {0} Uhr`n" -f (Get-Date -Format "dddd, dd. MMMM yyyy, HH:mm:ss"))
Add-Content $LogFile "`n`n"
function Write-Log {
param([string]$Text)
Add-Content -Path $LogFile -Value ("[{0}] {1}" -f (Get-Date -Format "HH:mm"), $Text)
}
Show-Info "Starte Restore des Firefox-Profils (Firefox-Erkennung deaktiviert)."
Write-Log "Restore gestartet (Firefox-Erkennung deaktiviert)."
# =========================
# Restore-Teil
# =========================
# prüfen, ob Sicherungsprofil existiert
if (-not (Test-Path -LiteralPath $BackupProfile)) {
Show-Info "Sicherungsprofil nicht gefunden: $BackupProfile"
Write-Log "FEHLER: Sicherungsprofil nicht gefunden: $BackupProfile"
[System.Windows.Forms.MessageBox]::Show(
"Sicherungsprofil wurde nicht gefunden:`n$BackupProfile",
"Firefox-Restore",
[System.Windows.Forms.MessageBoxButtons]::OK,
[System.Windows.Forms.MessageBoxIcon]::Error
) | Out-Null
exit 1
}
# vorhandenes Ziel-Profil löschen
if (Test-Path -LiteralPath $ProfilePath) {
Show-Info "Lösche vorhandenen Profilordner: $ProfilePath"
Write-Log "Lösche vorhandenen Profilordner: $ProfilePath"
Remove-Item -LiteralPath $ProfilePath -Recurse -Force
Start-Sleep -Seconds 2
} else {
Show-Info "Zielprofil existiert noch nicht – nichts zu löschen."
Write-Log "Hinweis: Zielprofil existiert noch nicht – nichts zu löschen."
}
# Sicherungsprofil zurückkopieren
Show-Info "Kopiere Sicherung nach: $ProfilePath"
Write-Log "Kopiere Sicherung nach: $ProfilePath"
Copy-Item -LiteralPath $BackupProfile -Destination $ProfilePath -Recurse -Force
# Erfolgsmeldung
Show-Info "Restore erfolgreich abgeschlossen."
Write-Log "Restore erfolgreich abgeschlossen."
[System.Windows.Forms.MessageBox]::Show(
"Firefox-Profil erfolgreich zurückgespielt.`n`nZielprofil: $ProfilePath`nLog: $LogFile`n`n⚠️ Hinweis: Firefox-Erkennung deaktiviert - manuell schließen!",
"Firefox-Restore",
[System.Windows.Forms.MessageBoxButtons]::OK,
[System.Windows.Forms.MessageBoxIcon]::Information
) | Out-Null
Alles anzeigen
Diese ps1-Datei kann von überall aus gestartet werden. Oberstes Gebot ist, der Firefox muss beendet sein ( sollte besser auch bei der Sicherung sein). Wenn das Backup dann wieder zurück übertragen wurde wird eine Log-Datei geschrieben ( Ordner muss angegeben werden, wo sie abgelegt werden soll), die das zum Inhalt hat.
Aber wenn die Profilbezeichnung ein Leerzeichen enthält funktioniert es nicht.
zbs. Firefox 152 dann kommt nur die Meldung :
In meinem Script habe ich z.B das stehen: profilePath: "C:\\Users\\Old Man\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\Reserve 3",
Name lautet "Reserve 3", und hat ein Leerzeichen, funktioniert aber ohne Fehler. Ich werde aber trotzdem noch mit anderen testen, melde mich wieder.
Edit:
Und auch hiermit profilePath: "C:\\Users\\Old Man\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\ooaxewl1.Reserve-Profil 2", keine Probleme, weder mit Script aus #362 noch aus #367. Es sind im Pfad ja insgesamt 2 Leerzeichen enthalten, deswegen ja zwingend vorgeschrieben die Anführungszeichen. Heißt der Profilordner bei dir wirklich "Firefox 152"?
Eventuell hat ja noch jemand getestet, dann bitte kommentieren.
Wer dann doch lieber ein ZIP-Archiv im zweiten Sicherungsordner haben möchte, der kann dann dafür dieses Script nutzen. Ansonsten hat sich gegenüber dem Script aus #362 nichts geändert.
// wurde mit KI erstellt
// ==UserScript==
// @name ProfileBackup@Fu_ZIP.uc.js
// @description Firefox Profil sichern: Backup (Ordner) + Archiv (ZIP pro Tag/01..), gesperrte Dateien ignoriert
// @version 2026.07-zip-nsizipwriter
// ==/UserScript==
(function () {
"use strict";
if (location.href !== "chrome://browser/content/browser.xhtml") return;
if (typeof CustomizableUI === "undefined") return;
if (!window.gBrowser) return;
// =========================
// Pro Profil konfigurieren
// =========================
const PROFILES = [
{
label: "Reserve 3",
profilePath: "C:\\Users\\Old Man\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\Reserve 3",
profileName: "Reserve 3",
backup1Path: "G:\\Firefox Sicherung\\Reserve 3",
archiveRootPath: "G:\\Sicherung2\\Reserve 3",
iconPath: "file:///C:/FoxIcons2/Finale.png"
},
{
label: "Nightly2",
profilePath: "G:\\Firefox Test\\Nightly2\\Profilordner",
profileName: "Profilordner",
backup1Path: "G:\\Firefox Sicherung\\Nightly2",
archiveRootPath: "G:\\Sicherung2\\Nightly2",
iconPath: "file:///C:/FoxIcons2/Nightly.png"
},
{
label: "Beta1",
profilePath: "G:\\Firefox Test\\Beta1\\Profilordner",
profileName: "Profilordner",
backup1Path: "G:\\Firefox Sicherung\\Beta1",
archiveRootPath: "G:\\Sicherung2\\Beta1",
iconPath: "file:///C:/FoxIcons2/Beta.png"
}
];
// =========================
// UI IDs
// =========================
const BTN_ID = "profilebackup_menu_button_icons_final";
const POPUP_ID = BTN_ID + "_popup";
// =========================
// Button-Icon
// =========================
const btnIconPath = "file:///C:/FoxIcons2/backup.png";
// =========================
// UI Helpers
// =========================
function showAlert(text) {
try {
let w = null;
try { w = Services.wm.getMostRecentWindow("navigator:browser"); } catch (_) {}
Services.prompt.alert(w || null, "Profilsicherung", text);
} catch (e) {
console.error("Alert Fehler:", e);
}
}
function pad2(n) {
return String(n).padStart(2, "0");
}
function getDateStrYYYYMMDD(d) {
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
}
function escapeRegExp(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// =========================
// gesperrte Dateien + WAL/SHM ignorieren
// =========================
function shouldSkipEntry(entry) {
const name = entry.leafName;
if (name === "parent.lock") return true;
if (name === "lock") return true;
if (name === "recovery.jsonlz4") return true;
if (name.endsWith(".sqlite-wal")) return true;
if (name.endsWith(".sqlite-shm")) return true;
return false;
}
// =========================
// Backup 1: Ordnerkopie (wie bisher)
// =========================
function copyDirectoryContents(srcDir, destDir) {
let entries = srcDir.directoryEntries;
while (entries.hasMoreElements()) {
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
if (shouldSkipEntry(entry)) continue;
const newFile = destDir.clone();
newFile.append(entry.leafName);
try {
if (entry.isDirectory()) {
if (!newFile.exists()) newFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
copyDirectoryContents(entry, newFile);
} else {
entry.copyTo(destDir, entry.leafName);
}
} catch (e) {
console.warn("⚠️ Datei konnte nicht kopiert werden (gesperrt?): " + entry.path, e);
}
}
}
// =========================
// ZIP-Name-Zähler pro Tag (01..)
// =========================
function getNextDailyZipIndex(archiveRootFile, profileName, dateStr) {
let max = 0;
const re = new RegExp(
"^" + escapeRegExp(profileName) + "_" + escapeRegExp(dateStr) + "_(\\d{2})\\.zip$"
);
let entries = archiveRootFile.directoryEntries;
while (entries.hasMoreElements()) {
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
if (!entry.isFile()) continue;
const m = re.exec(entry.leafName);
if (m) {
const n = parseInt(m[1], 10);
if (!isNaN(n) && n > max) max = n;
}
}
return max + 1;
}
// =========================
// ZIP-Erstellung mit nsIZipWriter
// Packt den Inhalt des Profilordners relativ ein (ohne zusätzlichen Root-Ordner)
// =========================
function zipProfileFolder(profileDir, zipFile, baseDirPathForRel) {
// Basis: Wir brauchen relativ zu baseDirPathForRel
// baseDirPathForRel = profileDir.path
const zipWriter = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
const PR_WRONLY = 0x02;
const PR_CREATE_FILE = 0x08;
const PR_TRUNCATE = 0x20;
if (zipFile.exists()) {
zipFile.remove(true);
}
zipWriter.open(zipFile, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE);
// Rekursiv: Dateien hinzufügen
function addRecursive(currentDir) {
let entries = currentDir.directoryEntries;
while (entries.hasMoreElements()) {
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
if (shouldSkipEntry(entry)) continue;
if (entry.path === zipFile.path) continue;
if (entry.isDirectory()) {
addRecursive(entry);
} else {
// Relativer Pfad im ZIP
let rel = entry.path.replace(baseDirPathForRel, "");
rel = rel.replace(/^([\\\/])/, ""); // führenden Slash entfernen
// ZIP verlangt forward slashes
rel = rel.replace(/\\/g, "/");
try {
zipWriter.addEntryFile(
rel,
Ci.nsIZipWriter.COMPRESSION_FASTEST,
entry,
false
);
} catch (e) {
// gesperrt → ignorieren (wie im Vorlage-Script)
}
}
}
}
addRecursive(profileDir);
zipWriter.close();
}
// =========================
// runBackup
// =========================
function runBackup(profile) {
let backupTarget = null;
let archiveRootFile = null;
try {
const now = new Date();
const profileDir = new FileUtils.File(profile.profilePath);
if (!profileDir.exists()) throw new Error("Profilordner existiert nicht: " + profile.profilePath);
const backupRoot = new FileUtils.File(profile.backup1Path);
if (!backupRoot.exists()) backupRoot.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
archiveRootFile = new FileUtils.File(profile.archiveRootPath);
if (!archiveRootFile.exists()) archiveRootFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
// -------------------------
// BACKUP 1 (Ordner)
// -------------------------
backupTarget = backupRoot.clone();
backupTarget.append(profile.profileName);
if (backupTarget.exists()) backupTarget.remove(true);
backupTarget.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
copyDirectoryContents(profileDir, backupTarget);
// -------------------------
// BACKUP 2 (ZIP, täglich + 01..)
// -------------------------
const dateStr = getDateStrYYYYMMDD(now);
const nextIdx = getNextDailyZipIndex(archiveRootFile, profile.profileName, dateStr);
const idxStr = pad2(nextIdx);
const zipFileName = profile.profileName + "_" + dateStr + "_" + idxStr + ".zip";
const zipFile = archiveRootFile.clone();
zipFile.append(zipFileName);
zipProfileFolder(profileDir, zipFile, profileDir.path);
if (!zipFile.exists()) {
throw new Error("ZIP wurde nicht erstellt: " + zipFile.path);
}
setTimeout(() => {
showAlert(
"✅ Backup abgeschlossen!\n\n" +
"👤 Profil: " + profile.label + "\n\n" +
"📁 Backup Ordner 1: " + backupTarget.path + "\n" +
"📦 Archiv ZIP: " + zipFile.path
);
}, 50);
} catch (e) {
console.error("Backup Fehler:", e);
try {
// optional: staging o.ä. gäbe es hier nicht; also best-effort cleanup nicht nötig
} catch (_) {}
setTimeout(() => showAlert("❌ Fehler: " + e.message), 0);
}
}
// =========================
// Menü erstellen
// =========================
function createMenu(doc) {
const menupopup = doc.createXULElement("menupopup");
menupopup.setAttribute("id", POPUP_ID);
PROFILES.forEach((profile) => {
const menuitem = doc.createXULElement("menuitem");
menuitem.setAttribute("label", "Backup: " + profile.label);
menuitem.setAttribute("class", "menuitem-iconic");
if (profile.iconPath) {
menuitem.setAttribute("image", profile.iconPath);
}
menuitem.addEventListener("command", () => {
let ok = false;
try {
ok = Services.prompt.confirm(
null,
"Profilsicherung",
"Backup jetzt starten für: " + profile.label + " ?"
);
} catch (_) {
ok = true; // Fallback
}
if (ok) runBackup(profile);
});
menupopup.appendChild(menuitem);
});
return menupopup;
}
// =========================
// Button patchen (öffnet Popup)
// =========================
function patchButton(btn) {
if (!btn) return false;
const oldPopup = btn.querySelector("#" + POPUP_ID);
if (oldPopup) oldPopup.remove();
btn.setAttribute("type", "menu");
btn.setAttribute("aria-haspopup", "true");
btn.setAttribute("tooltiptext", "Profile auswählen (Backup starten)");
btn.setAttribute("image", btnIconPath);
btn.appendChild(createMenu(btn.ownerDocument));
btn.addEventListener("click", (ev) => {
if (ev.button !== 0) return;
try {
const p = btn.querySelector("#" + POPUP_ID);
if (p) p.openPopup(btn, "after_start", 0, 0, false, false);
} catch (_) {}
}, false);
return true;
}
// =========================
// Alte Instanz entfernen + Widget erstellen
// =========================
try {
if (CustomizableUI.getWidget && CustomizableUI.getWidget(BTN_ID)) {
try { CustomizableUI.destroyWidget(BTN_ID); } catch (_) {}
}
} catch (_) {}
try {
const el = document.getElementById(BTN_ID);
if (el) el.remove();
} catch (_) {}
CustomizableUI.createWidget({
id: BTN_ID,
defaultArea: CustomizableUI.AREA_NAVBAR,
label: "Profilsicherung",
tooltiptext: "Profile auswählen (Backup starten)",
onCreated: (button) => setTimeout(() => patchButton(button), 0)
});
// =========================
// CSS Styles (Button & Popup) – wie bei dir
// =========================
const css = `
#${BTN_ID} .toolbarbutton-icon {
width: 31px !important;
height: 31px !important;
padding: 5px !important;
}
#${POPUP_ID} {
max-width: 250px !important;
min-width: 250px !important;
}
#${POPUP_ID} menuitem.menuitem-iconic img.menu-icon {
margin-left: -28px !important;
}
#${POPUP_ID} menuitem.menuitem-iconic label.menu-text {
margin-left: 6px !important;
}
`;
const sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
const ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
const uri = ios.newURI("data:text/css;charset=utf-8," + encodeURIComponent(css), null, null);
sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
})();
Alles anzeigen
Script in #362 getauscht, Button wird nun auch in Version 152 und 153 gezeigt.
Fehler in der Konsole, Schaltfläche wird nicht angezeigt, funktioniert nicht, v152, v153beta9
Das kann durchaus richtig sein, denn ich habe es nur in der 154 aufgebaut und getestet. Sorry , das werde ich nacharbeiten, stelle es dann hier wieder rein.
Skript ProfileBackup@Fu.uc.js
=============================
// wurde mit KI erstellt
Vor Nutzung des Skriptes müssen zwei Ordner für die Sicherungen eingerichtet werden ( Beispiele aus dem Skript ersichtlich ). In den ersten Ordner wird dann der vollständige Profilordner hinein kopiert (kein ZIP), wobei ganz wichtig ist, den richtigen Namen des Profils in das Skript einzutragen (about:profiles aufrufen und Namen entnehmen, z.B. qtqwpmy7.default-release). Bei jedem neuen Backup wird
der Profilordner hier überschrieben. Auch portable können mit entsprechenden Angaben (Pfade) gesichert werden.
Der zweite Ordner gilt als doppelte Sicherung, hier wird auch der Profilordner hinein kopiert, hat als Endung aber eine fortlaufende Nummerierung mit Datum und Uhrzeit (wichtig wenn mehrmals gesichert wird).
Die Pfade zu diesen Ordnern sind bei jedem Profil in das Skript einzutragen.
Das Skript wird über den Button in der Navbar gestartet, es öffnet sich ein Menü (Grafik 1) über das eine Auswahl für ein Backup getroffen werden kann, bei Klick darauf erscheint ein Bestätigungs-Dialog (Grafik 2), der bedient werden muss. Bei OK startet das Backup, bei Erfolg wird dann über Windows eine Benachrichtigung ausgegeben (Grafik 3).
// ==UserScript==
// @name ProfileBackup@Fu.uc.js
// @description Firefox Profil sichern (Backup + Archiv als Ordner, gesperrte Dateien ignoriert)
// @version 2026.07-icons-css-ordner-fixed
// ==/UserScript==
(function () {
"use strict";
if (location.href !== "chrome://browser/content/browser.xhtml") return;
if (typeof CustomizableUI === "undefined") return;
if (!window.gBrowser) return;
// =========================
// Pro Profil konfigurieren
// =========================
const PROFILES = [
{
label: "Reserve 3",
profilePath: "C:\\Users\\Old Man\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\Reserve 3",
profileName: "Reserve 3",
backup1Path: "G:\\Firefox Sicherung\\Reserve 3",
archiveRootPath: "G:\\Sicherung2\\Reserve 3",
iconPath: "file:///C:/FoxIcons2/Finale.png"
},
{
label: "Nightly2",
profilePath: "G:\\Firefox Test\\Nightly2\\Profilordner",
profileName: "Profilordner",
backup1Path: "G:\\Firefox Sicherung\\Nightly2",
archiveRootPath: "G:\\Sicherung2\\Nightly2",
iconPath: "file:///C:/FoxIcons2/Nightly.png"
},
{
label: "Beta1",
profilePath: "G:\\Firefox Test\\Beta1\\Profilordner",
profileName: "Profilordner",
backup1Path: "G:\\Firefox Sicherung\\Beta1",
archiveRootPath: "G:\\Sicherung2\\Beta1",
iconPath: "file:///C:/FoxIcons2/Beta.png"
}
// Weitere Profile hier ergänzen:
// {
// label: "Stable",
// profilePath: "D:\\Firefox\\Stable\\Profilordner",
// profileName: "Stable_Profil",
// backup1Path: "D:\\Firefox Sicherung\\Stable",
// archiveRootPath: "D:\\Sicherung2\\Stable",
// iconPath: "file:///C:/FoxIcons2/002.png"
// }
];
// =========================
// UI IDs
// =========================
const BTN_ID = "profilebackup_menu_button_icons_final";
const POPUP_ID = BTN_ID + "_popup";
// =========================
// Button-Icon
// =========================
const btnIconPath = "file:///C:/FoxIcons2/backup.png";
// =========================
// Helpers
// =========================
function showAlert(text) {
try {
let w = null;
try {
w = Services.wm.getMostRecentWindow("navigator:browser");
} catch (_) {}
Services.prompt.alert(w || null, "Profilsicherung", text);
} catch (e) {
console.error("Alert Fehler:", e);
}
}
function pad(n) {
return String(n).padStart(2, "0");
}
// =========================
// Backup-Logik
// =========================
function getArchiveFolder(root, profileName) {
const d = new Date();
const timestamp =
pad(d.getMonth() + 1) +
pad(d.getDate()) + "_" +
pad(d.getHours()) +
pad(d.getMinutes());
const archiveTarget = root.clone();
archiveTarget.append(profileName + "_" + timestamp);
return archiveTarget;
}
function copyDirectoryContents(srcDir, destDir) {
let entries = srcDir.directoryEntries;
while (entries.hasMoreElements()) {
let entry = entries.getNext().QueryInterface(Ci.nsIFile);
// gesperrte / WAL / shm ignorieren
if (
entry.leafName === "parent.lock" ||
entry.leafName === "lock" ||
entry.leafName === "recovery.jsonlz4" ||
entry.leafName.endsWith(".sqlite-wal") ||
entry.leafName.endsWith(".sqlite-shm")
) {
continue;
}
const newFile = destDir.clone();
newFile.append(entry.leafName);
try {
if (entry.isDirectory()) {
if (!newFile.exists()) newFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
copyDirectoryContents(entry, newFile);
} else {
entry.copyTo(destDir, entry.leafName);
}
} catch (e) {
console.warn("⚠️ Datei konnte nicht kopiert werden (gesperrt?): " + entry.path, e);
}
}
}
function runBackup(profile) {
try {
const profileDir = new FileUtils.File(profile.profilePath);
if (!profileDir.exists()) {
throw new Error("Profilordner existiert nicht: " + profile.profilePath);
}
const backupRoot = new FileUtils.File(profile.backup1Path);
if (!backupRoot.exists()) backupRoot.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
const archiveRoot = new FileUtils.File(profile.archiveRootPath);
if (!archiveRoot.exists()) archiveRoot.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
// BACKUP 1: immer gleicher Zielordner
const backupTarget = backupRoot.clone();
backupTarget.append(profile.profileName);
if (backupTarget.exists()) backupTarget.remove(true);
backupTarget.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
copyDirectoryContents(profileDir, backupTarget);
// BACKUP 2: Zeitstempel-Ordner im Archivroot
const archiveTarget = getArchiveFolder(archiveRoot, profile.profileName);
archiveTarget.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
copyDirectoryContents(profileDir, archiveTarget);
// Alert nachträglich (robuster)
setTimeout(() => {
showAlert(
"✅ Backup abgeschlossen!\n\n" +
"👤 Profil: " + profile.label + "\n\n" +
"📁 Backup Ordner 1: " + backupTarget.path + "\n" +
"📁 Archiv Ordner 2: " + archiveTarget.path
);
}, 50);
} catch (e) {
console.error("Backup Fehler:", e);
setTimeout(() => showAlert("❌ Fehler: " + e.message), 0);
}
}
// =========================
// Menü bauen
// =========================
function createMenu(doc) {
const menupopup = doc.createXULElement("menupopup");
menupopup.setAttribute("id", POPUP_ID);
PROFILES.forEach((profile) => {
const menuitem = doc.createXULElement("menuitem");
menuitem.setAttribute("label", "Backup: " + profile.label);
menuitem.setAttribute("class", "menuitem-iconic");
if (profile.iconPath) {
menuitem.setAttribute("image", profile.iconPath);
}
menuitem.addEventListener("command", () => {
let ok = false;
try {
ok = Services.prompt.confirm(
null,
"Profilsicherung",
"Backup jetzt starten für: " + profile.label + " ?"
);
} catch (_) {
ok = true; // Fallback
}
if (ok) runBackup(profile);
});
menupopup.appendChild(menuitem);
});
return menupopup;
}
// =========================
// Button patchen
// =========================
function patchButton(btn) {
if (!btn) return false;
const oldPopup = btn.querySelector("#" + POPUP_ID);
if (oldPopup) oldPopup.remove();
btn.setAttribute("type", "menu");
btn.setAttribute("aria-haspopup", "true");
btn.setAttribute("tooltiptext", "Profile auswählen (Backup starten)");
btn.setAttribute("image", btnIconPath);
btn.appendChild(createMenu(btn.ownerDocument));
btn.addEventListener("click", (ev) => {
if (ev.button !== 0) return;
try {
const p = btn.querySelector("#" + POPUP_ID);
if (p) p.openPopup(btn, "after_start", 0, 0, false, false);
} catch (_) {}
}, false);
return true;
}
// =========================
// Alte Instanz entfernen + Widget erstellen
// =========================
try {
if (CustomizableUI.getWidget && CustomizableUI.getWidget(BTN_ID)) {
try { CustomizableUI.destroyWidget(BTN_ID); } catch (_) {}
}
} catch (_) {}
try {
const el = document.getElementById(BTN_ID);
if (el) el.remove();
} catch (_) {}
CustomizableUI.createWidget({
id: BTN_ID,
defaultArea: CustomizableUI.AREA_NAVBAR,
label: "Profilsicherung",
tooltiptext: "Profile auswählen (Backup starten)",
onCreated: (button) => setTimeout(() => patchButton(button), 0)
});
// =========================
// CSS Styles (Button & Popup)
// =========================
const css = `
#${BTN_ID} .toolbarbutton-icon {
width: 31px !important;
height: 31px !important;
padding: 5px !important;
}
#${POPUP_ID} {
max-width: 250px !important;
min-width: 250px !important;
}
#${POPUP_ID} menuitem.menuitem-iconic img.menu-icon {
margin-left: -28px !important;
}
#${POPUP_ID} menuitem.menuitem-iconic label.menu-text {
margin-left: 6px !important;
}
`;
const sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
const ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
const uri = ios.newURI("data:text/css;charset=utf-8," + encodeURIComponent(css), null, null);
sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
})();
Alles anzeigen
Das Script TabMoveOnly.uc.js funktionierte auch nicht mehr (jetzt erst festgestellt). Hier dann die bereinigte Version zur Verfügung.
// ==UserScript==
// @name TabMoveOnly.uc.js
// @namespace local
// @description Nur das Verschieben von Tabs per Drag & Drop
// @include main
// @compatibility Firefox152+
// @version 2026-07-05
// ==/UserScript==
"use strict";
TabMoveOnly();
function TabMoveOnly() {
if (!window.gBrowser) return;
const TAB_DROP_TYPE = window.TAB_DROP_TYPE || "application/x-moz-tabbrowser-tab";
// =========================================================
// FEINEINSTELLUNGEN FÜR DIE MARKER
// =========================================================
// Links: Marker horizontal verschieben in px
const LEFT_MARKER_SHIFT = 0;
// Rechts: Marker horizontal verschieben in px
const RIGHT_MARKER_SHIFT = 0;
// Marker vertikal leicht nach oben/unten korrigieren in px
const MARKER_TOP = -1;
// =========================================================
// MARKER-CSS
// =========================================================
const css = `
.tabbrowser-tab[dnd-marker]{
position: relative !important;
}
.tab-dnd-marker{
position: absolute !important;
pointer-events: none !important;
z-index: 1000 !important;
content: "" !important;
width: 14px !important;
height: 18px !important;
border-radius: 50% !important;
background:
radial-gradient(circle at center,
#00ffff 0 35%,
#1dacd6 36% 60%,
rgba(255,255,255,0) 61%) !important;
box-shadow: 0 0 3px rgba(255, 90, 82, 0.35) !important;
}
`;
// =========================================================
// CSS EINBINDEN
// =========================================================
const sss = Cc["@mozilla.org/content/style-sheet-service;1"]
.getService(Ci.nsIStyleSheetService);
const uri = Services.io.newURI(
"data:text/css;charset=UTF-8," + encodeURIComponent(css)
);
if (!sss.sheetRegistered(uri, sss.USER_SHEET)) {
sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
}
const tabContainer = gBrowser.tabContainer;
// Listener nur einmal aktivieren
let listenersActive = false;
// Reserviert für Gruppen-/Drop-Zustand
let lastGroupStart = null;
let lastGroupEnd = null;
// =========================================================
// MARKER-VERWALTUNG
// =========================================================
function clearDndMarkers() {
for (const tab of gBrowser.tabs) {
tab.removeAttribute("dnd-marker");
tab.style.removeProperty("--tab-dnd-marker-top");
if (tab._dndMarkerEl) {
tab._dndMarkerEl.remove();
tab._dndMarkerEl = null;
}
}
}
function setDndMarker(tab, side) {
clearDndMarkers();
if (!tab || !side) return;
// Nur zur Erkennung / CSS-Selektoren
tab.setAttribute("dnd-marker", side);
// Vertikale Feinkorrektur als Variable direkt am Tab
tab.style.setProperty("--tab-dnd-marker-top", `${MARKER_TOP}px`);
// Echtes Marker-Element, damit dein CSS sicher greift
const marker = tab.ownerDocument.createXULElement("box");
marker.className = `tab-dnd-marker ${side}`;
// Horizontale Feineinstellung pro Seite
if (side === "left") {
marker.style.setProperty("left", `${1 + LEFT_MARKER_SHIFT}px`, "important");
} else if (side === "right") {
marker.style.setProperty("right", `${1 + RIGHT_MARKER_SHIFT}px`, "important");
}
tab.appendChild(marker);
tab._dndMarkerEl = marker;
}
// =========================================================
// HILFSFUNKTIONEN FÜR TAB / DROP
// =========================================================
function getTabFromEventTarget(event, ignoreTabSides = false) {
let { target } = event;
if (!target) return null;
if (target.nodeType !== Node.ELEMENT_NODE) {
target = target.parentElement;
}
let tab = target?.closest("tab") || target?.closest("tab-group");
const selectedTab = gBrowser.selectedTab;
// Falls der Cursor nur im Randbereich ist, nimm den aktuell ausgewählten Tab
if (tab && ignoreTabSides) {
const { width, height } = tab.getBoundingClientRect();
if (
event.screenX < tab.screenX + width * 0.25 ||
event.screenX > tab.screenX + width * 0.75 ||
((event.screenY < tab.screenY + height * 0.25 ||
event.screenY > tab.screenY + height * 0.75) &&
gBrowser.tabContainer.verticalMode)
) {
return selectedTab;
}
}
return tab;
}
// Firefox-interne Drop-Index-Berechnung für Multirow / normales Tab-Layout
gBrowser.tabContainer._getDropIndex = function (event) {
let tabToDropAt = getTabFromEventTarget(event, false);
if (tabToDropAt?.localName == "tab-group") {
tabToDropAt = tabToDropAt.previousSibling;
if (!tabToDropAt) {
tabToDropAt = gBrowser.visibleTabs[0];
}
}
if (!tabToDropAt) {
tabToDropAt = gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1];
}
if (!tabToDropAt) return null;
const tabPos = gBrowser.tabContainer.getIndexOfItem(tabToDropAt);
const rect = tabToDropAt.getBoundingClientRect();
const ltr = window.getComputedStyle(this).direction == "ltr";
if (ltr) {
return event.clientX < rect.x + rect.width / 2 ? tabPos : tabPos + 1;
}
return event.clientX > rect.x + rect.width / 2 ? tabPos : tabPos + 1;
};
// Prüft, ob der Drag wirklich ein Tab-Drag ist
function orig_getDropEffectForTabDrag(event) {
const dt = event.dataTransfer;
let isMovingTabs = dt.mozItemCount > 0;
for (let i = 0; i < dt.mozItemCount; i++) {
const types = dt.mozTypesAt(0);
if (types[0] != TAB_DROP_TYPE) {
isMovingTabs = false;
break;
}
}
if (isMovingTabs) {
const sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
if (
XULElement.isInstance(sourceNode) &&
sourceNode.localName == "tab" &&
sourceNode.documentGlobal.isChromeWindow &&
sourceNode.ownerDocument.documentElement.getAttribute("windowtype") == "navigator:browser" &&
sourceNode.documentGlobal.gBrowser.tabContainer == sourceNode.container
) {
if (
PrivateBrowsingUtils.isWindowPrivate(window) !=
PrivateBrowsingUtils.isWindowPrivate(sourceNode.documentGlobal)
) {
return "none";
}
if (window.gMultiProcessBrowser != sourceNode.documentGlobal.gMultiProcessBrowser) return "none";
if (window.gFissionBrowser != sourceNode.documentGlobal.gFissionBrowser) return "none";
return dt.dropEffect == "copy" ? "copy" : "move";
}
}
if (Services.droppedLinkHandler.canDropLink(event, true)) return "link";
return "none";
}
function getMarkerInfo(event) {
let tabToDropAt = getTabFromEventTarget(event, false);
if (tabToDropAt?.localName == "tab-group") {
tabToDropAt = tabToDropAt.previousSibling;
if (!tabToDropAt) {
tabToDropAt = gBrowser.visibleTabs[0];
}
}
if (!tabToDropAt) {
const lastTab = gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1];
return { tab: lastTab, side: "right" };
}
const rect = tabToDropAt.getBoundingClientRect();
const ltr = window.getComputedStyle(gBrowser.tabContainer).direction == "ltr";
const side = ltr
? (event.clientX < rect.x + rect.width / 2 ? "left" : "right")
: (event.clientX > rect.x + rect.width / 2 ? "left" : "right");
return { tab: tabToDropAt, side };
}
// =========================================================
// DRAG OVER
// =========================================================
function performTabDragOver(event) {
const effects = orig_getDropEffectForTabDrag(event);
// Nur echte Tab-Drags zulassen
if (effects == "none" || effects == "link") {
clearDndMarkers();
return;
}
event.preventDefault();
event.stopPropagation();
const tab = getTabFromEventTarget(event, true);
// Kleine Starthilfe für Gruppen-Merker
if (tab && tab.nodeName == "tab-group" && !lastGroupStart) {
const first = tab.querySelector("tab:first-of-type");
const last = tab.querySelector("tab:last-of-type");
if (first && last) {
lastGroupStart = first._tPos;
lastGroupEnd = last._tPos;
}
}
const newIndex = gBrowser.tabContainer._getDropIndex(event);
if (newIndex == null) {
clearDndMarkers();
return;
}
const markerInfo = getMarkerInfo(event);
setDndMarker(markerInfo.tab, markerInfo.side);
}
// =========================================================
// DROP
// =========================================================
function performTabDropEvent(event) {
clearDndMarkers();
const dt = event.dataTransfer;
const dropEffect = dt.dropEffect;
let draggedTab;
if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) {
draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
if (!draggedTab) return;
}
if (draggedTab && dropEffect != "copy" && draggedTab.container == gBrowser.tabContainer) {
let newIndex = gBrowser.tabContainer._getDropIndex(event);
if (newIndex == null) return;
const selectedTabs = gBrowser.selectedTabs.length > 1 ? gBrowser.selectedTabs : [draggedTab];
let tabToMoveAt = gBrowser.tabContainer.getItemAtIndex(newIndex);
const tab = getTabFromEventTarget(event, false);
const tabgroup = tab?.closest("tab-group");
if (!tab) {
newIndex = gBrowser.tabs.length;
tabToMoveAt = null;
}
if (tabgroup && !tab.previousSibling) {
newIndex = 0;
selectedTabs.forEach(t => {
gBrowser.moveTabTo(t, { tabIndex: newIndex++, forceUngrouped: true });
});
} else if (
!tab ||
(!tabgroup && !tabToMoveAt?.group) ||
(tabgroup && tabToMoveAt?.group)
) {
selectedTabs.forEach(t => {
gBrowser.moveTabBefore(t, tabToMoveAt);
});
} else {
tabToMoveAt = gBrowser.tabContainer.getItemAtIndex(newIndex - 1);
selectedTabs.forEach(t => {
gBrowser.moveTabAfter(t, tabToMoveAt);
});
}
lastGroupStart = null;
lastGroupEnd = null;
}
}
// =========================================================
// EINMALIGE INITIALISIERUNG
// =========================================================
tabContainer.addEventListener("dragstart", () => {
if (listenersActive) return;
tabContainer.on_dragover = e => performTabDragOver(e);
tabContainer._onDragOver = e => performTabDragOver(e);
tabContainer.ondrop = e => performTabDropEvent(e);
tabContainer.addEventListener("dragend", clearDndMarkers, true);
tabContainer.addEventListener("drop", clearDndMarkers, true);
listenersActive = true;
});
}
Alles anzeigen