mirror of
https://github.com/agoramachina/claude-exporter.git
synced 2026-07-09 15:59:52 +00:00
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:
parent
59280d058d
commit
c61d8c8d53
8 changed files with 191 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
6
TODO.md
6
TODO.md
|
|
@ -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 🔴
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -175,6 +175,15 @@
|
|||
<div id="testStatus" class="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Backup & 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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -175,6 +175,15 @@
|
|||
<div id="testStatus" class="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Backup & 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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue