Last active 1769045220

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

ntfy-browser-import-export.js Raw
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
9javascript:(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
14async 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
34Promise.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
71javascript:(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});