Last active 1769045220

A set of scripts to backup and restore your ntfy subscriptions, settings, users and messages from a browser session

fabio's Avatar fabio revised this gist 1769045220. Go to revision

1 file changed, 170 insertions

ntfy-browser-import-export.js(file created)

@@ -0,0 +1,170 @@
1 + /**
2 + * ntfy exporter
3 + *
4 + * This saves the ntfy state in your current browser session to a JSON file that
5 + * can be imported by another browser session or (only partly tested) the Android app.
6 + *
7 + * Save the following one-liner to your bookmarks to run it on the fly:
8 +
9 + javascript:(async()=>{async function dumpStore(dbName,storeName){const db=await new Promise((resolve,reject)=>{const r=indexedDB.open(dbName);r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);});const tx=db.transaction(storeName,"readonly");const store=tx.objectStore(storeName);const all=await new Promise((resolve,reject)=>{const r=store.getAll();r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);});db.close();return all;}const [subscriptions,users,notifications,settings]=await Promise.all([dumpStore("ntfy","subscriptions"),dumpStore("ntfy","users"),dumpStore("ntfy","notifications"),dumpStore("ntfy","prefs")]);const data={subscriptions,users,notifications,settings};const json=JSON.stringify(data,null,2);const blob=new Blob([json],{type:"application/json"});const url=URL.createObjectURL(blob);const a=document.createElement("a");a.href=url;a.download=`ntfy-backup-${location.host}-${new Date().toISOString().replace(/[:.]/g,"-")}.json`;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url);})().catch(e=>alert(e&&e.message?e.message:String(e)));
10 +
11 + * The full implementation follows
12 + */
13 +
14 + async function dumpStore(dbName, storeName) {
15 + const db = await new Promise((resolve, reject) => {
16 + const r = indexedDB.open(dbName);
17 + r.onsuccess = () => resolve(r.result);
18 + r.onerror = () => reject(r.error);
19 + });
20 +
21 + const tx = db.transaction(storeName, "readonly");
22 + const store = tx.objectStore(storeName);
23 +
24 + const all = await new Promise((resolve, reject) => {
25 + const r = store.getAll();
26 + r.onsuccess = () => resolve(r.result);
27 + r.onerror = () => reject(r.error);
28 + });
29 +
30 + db.close();
31 + return all;
32 + }
33 +
34 + Promise.all(
35 + [
36 + dumpStore("ntfy", "subscriptions"),
37 + dumpStore("ntfy", "users"),
38 + dumpStore("ntfy", "notifications"),
39 + dumpStore("ntfy", "prefs"),
40 + ]
41 + ).then(([subscriptions, users, notifications, settings]) => {
42 + const data = {
43 + subscriptions,
44 + users,
45 + notifications,
46 + settings,
47 + };
48 +
49 + const json = JSON.stringify(data, null, 2);
50 + const blob = new Blob([json], { type: "application/json" });
51 + const url = URL.createObjectURL(blob);
52 +
53 + const a = document.createElement("a");
54 + a.href = url;
55 + a.download = `ntfy-backup-${location.host}-${new Date()
56 + .toISOString()
57 + .replace(/[:.]/g, "-")}.json`;
58 + document.body.appendChild(a);
59 + a.click();
60 + a.remove();
61 +
62 + URL.revokeObjectURL(url);
63 + });
64 +
65 + /**
66 + * ntfy importer
67 + *
68 + * A backup file generated through the script above can be restored into
69 + * another browser session. Create a bookmark with the following JS:
70 +
71 + javascript:(async()=>{const DB_NAME="ntfy";const openDb=()=>new Promise((res,rej)=>{const r=indexedDB.open(DB_NAME);r.onsuccess=()=>res(r.result);r.onerror=()=>rej(r.error);r.onblocked=()=>rej(new Error("IndexedDB open blocked (close other tabs using this origin)."));});const pickFile=()=>new Promise((res,rej)=>{const i=document.createElement("input");i.type="file";i.accept="application/json";i.onchange=()=>{const f=i.files&&i.files[0];f?res(f):rej(new Error("No file selected"));};i.click();});const readJson=f=>new Promise((res,rej)=>{const fr=new FileReader();fr.onload=()=>{try{res(JSON.parse(fr.result));}catch(e){rej(e);}};fr.onerror=()=>rej(fr.error);fr.readAsText(f);});const clearAndImport=(db,storeName,rows)=>new Promise((res,rej)=>{const tx=db.transaction(storeName,"readwrite");const store=tx.objectStore(storeName);const c=store.clear();c.onerror=()=>rej(c.error);c.onsuccess=()=>{for(const row of (rows||[])) store.put(row);};tx.oncomplete=()=>res();tx.onerror=()=>rej(tx.error);tx.onabort=()=>rej(tx.error||new Error("Transaction aborted"));});try{const file=await pickFile();const data=await readJson(file);const db=await openDb();const stores=Array.from(db.objectStoreNames);const mapping=[["subscriptions",data.subscriptions],["users",data.users],["notifications",data.notifications],["prefs",data.settings]];for(const [storeName,rows] of mapping){if(!stores.includes(storeName)){console.warn("Skipping missing store:",storeName);continue;}await clearAndImport(db,storeName,rows);console.log(`Imported ${(rows||[]).length} into ${storeName}`);}db.close();location.reload();}catch(e){console.error(e);alert(e&&e.message?e.message:String(e));}})();
72 +
73 + * Upon click it will open a file dialog - prompt a previously created backup.
74 + * The full implementation follows.
75 + */
76 +
77 + // Run on the ntfy Web UI origin (same host) in the NEW browser/session.
78 + // 1) paste this into DevTools Console
79 + // 2) pick the JSON export file when prompted
80 + (async () => {
81 + const DB_NAME = "ntfy";
82 +
83 + function openDb() {
84 + return new Promise((resolve, reject) => {
85 + const r = indexedDB.open(DB_NAME);
86 + r.onsuccess = () => resolve(r.result);
87 + r.onerror = () => reject(r.error);
88 + r.onblocked = () => reject(new Error("IndexedDB open blocked (close other tabs using this origin)."));
89 + // No onupgradeneeded: we assume the DB already exists with the right schema.
90 + });
91 + }
92 +
93 + function pickFile() {
94 + return new Promise((resolve, reject) => {
95 + const input = document.createElement("input");
96 + input.type = "file";
97 + input.accept = "application/json";
98 + input.onchange = () => {
99 + const f = input.files && input.files[0];
100 + if (!f) reject(new Error("No file selected"));
101 + else resolve(f);
102 + };
103 + input.click();
104 + });
105 + }
106 +
107 + function readJson(file) {
108 + return new Promise((resolve, reject) => {
109 + const fr = new FileReader();
110 + fr.onload = () => {
111 + try { resolve(JSON.parse(fr.result)); }
112 + catch (e) { reject(e); }
113 + };
114 + fr.onerror = () => reject(fr.error);
115 + fr.readAsText(file);
116 + });
117 + }
118 +
119 + async function clearAndImportStore(db, storeName, rows) {
120 + return new Promise((resolve, reject) => {
121 + const tx = db.transaction(storeName, "readwrite");
122 + const store = tx.objectStore(storeName);
123 +
124 + // Clear then put all rows back.
125 + const clearReq = store.clear();
126 + clearReq.onerror = () => reject(clearReq.error);
127 +
128 + clearReq.onsuccess = () => {
129 + // Use put() so existing keys are overwritten if any remain.
130 + for (const row of (rows || [])) store.put(row);
131 + };
132 +
133 + tx.oncomplete = () => resolve();
134 + tx.onerror = () => reject(tx.error);
135 + tx.onabort = () => reject(tx.error || new Error("Transaction aborted"));
136 + });
137 + }
138 +
139 + const file = await pickFile();
140 + const data = await readJson(file);
141 +
142 + const db = await openDb();
143 + const stores = Array.from(db.objectStoreNames);
144 + console.log("Found stores:", stores);
145 +
146 + // The export you created uses these top-level keys:
147 + const mapping = [
148 + ["subscriptions", data.subscriptions],
149 + ["users", data.users],
150 + ["notifications", data.notifications],
151 + ["prefs", data.settings], // export called it "settings", store is "prefs"
152 + ];
153 +
154 + for (const [storeName, rows] of mapping) {
155 + if (!stores.includes(storeName)) {
156 + console.warn(`Skipping missing store: ${storeName}`);
157 + continue;
158 + }
159 + await clearAndImportStore(db, storeName, rows);
160 + console.log(`Imported ${rows ? rows.length : 0} records into ${storeName}`);
161 + }
162 +
163 + db.close();
164 +
165 + // Optional: reload so UI picks up changes immediately.
166 + location.reload();
167 + })().catch(e => {
168 + console.error(e);
169 + alert(e && e.message ? e.message : String(e));
170 + });
Newer Older