Add Backup & Restore for extension data in options page

Download all chrome.storage.local + chrome.storage.sync data to a structured
JSON file ({_meta, local, sync}) and restore it later. Solves uninstall/
reinstall data loss and migration between separate extension builds (store
vs. GitHub) that have separate storage. Restore validates _meta.app and
confirms before overwriting.
This commit is contained in:
tux tucker 2026-05-14 21:55:33 -04:00
parent 59280d058d
commit c61d8c8d53
8 changed files with 191 additions and 2 deletions

View file

@ -1,5 +1,10 @@
# Changelog
## [1.9.5]
- Added Backup & Restore to the options page — download all extension data (model snapshots, export history, preferences) to a JSON file and restore it later
- Survives uninstall/reinstall, and lets you move data between browsers, devices, or extension builds (e.g. store version ↔ GitHub build)
## [1.9.4]
- Browse table's Model column now shows each chat's original (first-seen) model from the snapshot data, falling back to the current/inferred model when no snapshot exists

View file

@ -128,6 +128,12 @@
- Moved `formatModelName`, `getModelBadgeClass`, `DEFAULT_MODEL_TIMELINE` out of `content.js`/`browse.js` into shared `utils.js`
- Doc-linked the Anthropic model-ID schema in code comments
- **Backup & Restore for extension data** (v1.9.5)
- Options page can download all `chrome.storage.local` + `chrome.storage.sync` data to a JSON file and restore it
- Solves uninstall/reinstall data loss, and migration between separate extension builds (store vs. GitHub) which have separate storage
- Backup file is structured `{ _meta, local, sync }`; restore validates `_meta.app` and confirms before overwriting
- Future enhancement: smart per-key merge on restore (e.g. union `modelSnapshots`, keep earliest `firstSeen`) instead of overwrite
## Pending 🔄
### Critical Priority 🔴

View file

@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Claude Exporter Beta",
"version": "1.9.4",
"version": "1.9.5",
"description": "Export conversations and artifacts from Claude.ai",
"permissions": [
"activeTab",

View file

@ -175,6 +175,15 @@
<div id="testStatus" class="status"></div>
</div>
<div class="section">
<h3>Backup &amp; Restore</h3>
<p>Save the extension's data — model snapshots, export history, and preferences — to a file you can restore later. Useful before uninstalling, or to move your data to another browser, device, or extension build.</p>
<button id="backupBtn" style="margin-right: 8px;">Download Backup</button>
<button id="restoreBtn">Restore from File</button>
<input type="file" id="restoreFile" accept="application/json,.json" style="display: none;">
<div id="backupStatus" class="status"></div>
</div>
<script src="options.js"></script>
</body>
</html>

View file

@ -64,6 +64,86 @@ document.getElementById('testBtn').addEventListener('click', async () => {
}
});
// Backup all extension data to a file
document.getElementById('backupBtn').addEventListener('click', () => {
chrome.storage.local.get(null, (local) => {
chrome.storage.sync.get(null, (sync) => {
const backup = {
_meta: {
app: 'claude-exporter',
backupVersion: 1,
extensionVersion: chrome.runtime.getManifest().version,
createdAt: new Date().toISOString()
},
local: local || {},
sync: sync || {}
};
const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `claude-exporter-backup-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
const snapCount = Object.keys(backup.local.modelSnapshots || {}).length;
const exportCount = Object.keys(backup.local.exportTimestamps || {}).length;
showStatus('backupStatus', `Backup downloaded — ${snapCount} model snapshot(s), ${exportCount} export record(s).`, 'success');
});
});
});
// Restore extension data from a backup file
document.getElementById('restoreBtn').addEventListener('click', () => {
document.getElementById('restoreFile').click();
});
document.getElementById('restoreFile').addEventListener('change', (event) => {
const file = event.target.files[0];
event.target.value = ''; // allow re-selecting the same file later
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
let backup;
try {
backup = JSON.parse(e.target.result);
} catch (err) {
showStatus('backupStatus', 'Restore failed: the file is not valid JSON.', 'error');
return;
}
// Make sure this is actually one of our backup files
if (!backup || typeof backup !== 'object' || !backup._meta ||
backup._meta.app !== 'claude-exporter' || typeof backup.local !== 'object') {
showStatus('backupStatus', 'Restore failed: this does not look like a Claude Exporter backup file.', 'error');
return;
}
const snapCount = Object.keys(backup.local.modelSnapshots || {}).length;
const exportCount = Object.keys(backup.local.exportTimestamps || {}).length;
const proceed = confirm(
`Restore this backup?\n\n` +
`It contains ${snapCount} model snapshot(s) and ${exportCount} export record(s), ` +
`created ${backup._meta.createdAt || 'an unknown date'}.\n\n` +
`This overwrites the extension's current data with the backup's contents.`
);
if (!proceed) {
showStatus('backupStatus', 'Restore cancelled.', 'error');
return;
}
chrome.storage.local.set(backup.local, () => {
const syncData = (backup.sync && typeof backup.sync === 'object') ? backup.sync : {};
chrome.storage.sync.set(syncData, () => {
showStatus('backupStatus', `Restore complete — ${snapCount} model snapshot(s), ${exportCount} export record(s) restored. Reload any open Claude pages and the browse page to see the changes.`, 'success');
});
});
};
reader.readAsText(file);
});
// Helper functions
function showStatus(elementId, message, type) {
const statusEl = document.getElementById(elementId);

View file

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Claude Exporter Beta",
"version": "1.9.4",
"version": "1.9.5",
"description": "Export conversations and artifacts from Claude.ai",
"permissions": [
"activeTab",

View file

@ -175,6 +175,15 @@
<div id="testStatus" class="status"></div>
</div>
<div class="section">
<h3>Backup &amp; Restore</h3>
<p>Save the extension's data — model snapshots, export history, and preferences — to a file you can restore later. Useful before uninstalling, or to move your data to another browser, device, or extension build.</p>
<button id="backupBtn" style="margin-right: 8px;">Download Backup</button>
<button id="restoreBtn">Restore from File</button>
<input type="file" id="restoreFile" accept="application/json,.json" style="display: none;">
<div id="backupStatus" class="status"></div>
</div>
<script src="options.js"></script>
</body>
</html>

View file

@ -64,6 +64,86 @@ document.getElementById('testBtn').addEventListener('click', async () => {
}
});
// Backup all extension data to a file
document.getElementById('backupBtn').addEventListener('click', () => {
chrome.storage.local.get(null, (local) => {
chrome.storage.sync.get(null, (sync) => {
const backup = {
_meta: {
app: 'claude-exporter',
backupVersion: 1,
extensionVersion: chrome.runtime.getManifest().version,
createdAt: new Date().toISOString()
},
local: local || {},
sync: sync || {}
};
const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `claude-exporter-backup-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
const snapCount = Object.keys(backup.local.modelSnapshots || {}).length;
const exportCount = Object.keys(backup.local.exportTimestamps || {}).length;
showStatus('backupStatus', `Backup downloaded — ${snapCount} model snapshot(s), ${exportCount} export record(s).`, 'success');
});
});
});
// Restore extension data from a backup file
document.getElementById('restoreBtn').addEventListener('click', () => {
document.getElementById('restoreFile').click();
});
document.getElementById('restoreFile').addEventListener('change', (event) => {
const file = event.target.files[0];
event.target.value = ''; // allow re-selecting the same file later
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
let backup;
try {
backup = JSON.parse(e.target.result);
} catch (err) {
showStatus('backupStatus', 'Restore failed: the file is not valid JSON.', 'error');
return;
}
// Make sure this is actually one of our backup files
if (!backup || typeof backup !== 'object' || !backup._meta ||
backup._meta.app !== 'claude-exporter' || typeof backup.local !== 'object') {
showStatus('backupStatus', 'Restore failed: this does not look like a Claude Exporter backup file.', 'error');
return;
}
const snapCount = Object.keys(backup.local.modelSnapshots || {}).length;
const exportCount = Object.keys(backup.local.exportTimestamps || {}).length;
const proceed = confirm(
`Restore this backup?\n\n` +
`It contains ${snapCount} model snapshot(s) and ${exportCount} export record(s), ` +
`created ${backup._meta.createdAt || 'an unknown date'}.\n\n` +
`This overwrites the extension's current data with the backup's contents.`
);
if (!proceed) {
showStatus('backupStatus', 'Restore cancelled.', 'error');
return;
}
chrome.storage.local.set(backup.local, () => {
const syncData = (backup.sync && typeof backup.sync === 'object') ? backup.sync : {};
chrome.storage.sync.set(syncData, () => {
showStatus('backupStatus', `Restore complete — ${snapCount} model snapshot(s), ${exportCount} export record(s) restored. Reload any open Claude pages and the browse page to see the changes.`, 'success');
});
});
};
reader.readAsText(file);
});
// Helper functions
function showStatus(elementId, message, type) {
const statusEl = document.getElementById(elementId);