Skip to content

Instantly share code, notes, and snippets.

@yyogo
Last active March 2, 2025 12:58
Show Gist options
  • Save yyogo/42f131c2c89bb3d8550e334ec38d6e2d to your computer and use it in GitHub Desktop.
Save yyogo/42f131c2c89bb3d8550e334ec38d6e2d to your computer and use it in GitHub Desktop.
Extract file recovery snapshots from Obsidian
/**
* Obsidian Snapshot Exporter
* --------------------------
*
* This script helps you export snapshots (version history) from Obsidian's IndexedDB storage.
* It creates a ZIP file containing all your snapshots with proper file naming and organization.
*
* === HOW TO USE ===
*
* Step 1: Open Obsidian's Developer Tools
* - In Obsidian, press Ctrl+Shift+I (Windows/Linux) or Cmd+Option+I (Mac)
* - Click on the Console tab in the developer tools pane
*
* Step 2: Run the Script
* - Copy this entire script
* - Paste it into the Console tab of the Developer Tools
* + if you see a warning, type "allow pasting" and press Enter and then paste the script
* - Press Enter to load the script
*
* Step 3: Export Your Snapshots
* Run this command in the console:
* await exportSnapshots(app.appId);
*
* This command will export all snapshots from the current vault.
* You can also extract snapshots for specific files only by providing an array of file paths:
* await exportSnapshots(app.appId, ['Note1.md', 'Folder/Note2.md']);
*
* Or you can export snapshots from a differnet vault by providing the vault ID:
* await exportSnapshots('YOUR_VAULT_ID');
*
* (To list all available vault IDs, run: `await listVaultIds();`)
*
* Step 4: Download the ZIP File
* The script will automatically generate and download a ZIP file containing all your snapshots.
* Each snapshot will be saved as a Markdown file with the pattern: {vaultId}/{note_name}_{timestamp}.md
*
* === TROUBLESHOOTING ===
*
* - If you don't see any vault IDs, make sure you have the File Recovery core plugin enabled in Obsidian
* - If the ZIP download doesn't start, check the console for any error messages
* - Make sure you're running the script in Obsidian's Developer Tools, not in a regular browser
*
* === PRIVACY NOTE ===
*
* This script runs entirely in your browser and doesn't send any data over the network
* (except to download the JSZip library from a CDN). Your notes and snapshots remain private.
*/
async function exportSnapshots(vaultId, filePaths=null) {
// First, we need to load the JSZip library dynamically
if (typeof JSZip === 'undefined') {
console.log("Loading JSZip library...");
return new Promise((resolve) => {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
script.onload = function() {
console.log("JSZip loaded successfully");
// Continue with the export once JSZip is loaded
performExport(vaultId, filePaths).then(resolve);
};
script.onerror = function() {
console.error("Failed to load JSZip. Please check your internet connection.");
};
document.head.appendChild(script);
});
} else {
return performExport(vaultId, filePaths);
}
// Main export function (will be called after JSZip is loaded)
async function performExport(vaultId, filePaths) {
const zip = new JSZip();
// Open the correct database
const dbName = `${vaultId}-backup`;
console.log(`Opening database: ${dbName}`);
return new Promise((resolve, reject) => {
const dbRequest = indexedDB.open(dbName);
// Handle errors
dbRequest.onerror = function(event) {
console.error(`Error opening database ${dbName}:`, event.target.error);
reject(event.target.error);
};
// Handle successful connection
dbRequest.onsuccess = async function(event) {
const db = event.target.result;
console.log(`Successfully opened database: ${dbName}`);
// Check if 'backups' store exists
if (!Array.from(db.objectStoreNames).includes('backups')) {
console.error("No 'backups' store found in this database");
reject(new Error("No 'backups' store found"));
return;
}
// Start a transaction on the 'backups' store
const transaction = db.transaction('backups', 'readonly');
const backupsStore = transaction.objectStore('backups');
console.log("Accessing 'backups' store");
// Get all snapshot records
const getAllRequest = backupsStore.getAll();
getAllRequest.onsuccess = async function() {
const allRecords = getAllRequest.result;
// Filter records for the specified files
const records = allRecords.filter(record =>
!filePaths || record.path && filePaths.includes(record.path)
);
if (records.length === 0) {
console.warn("No matching records found. Note that filenames must include extension (.md).");
return;
}
console.log(`Found ${records.length} matching records`);
// Process each record
for (const record of records) {
if (record.path && record.ts) {
// Clean up the file name
const timestamp = new Date(record.ts).toISOString().replace(/:/g, '-');
// Create file name for the snapshot
const fileName = `${vaultId}/${record.path}_${timestamp}.md`;
// Add file content to the zip
// If the record has 'data' property, use that as content
const content = record.data || `# Empty snapshot for ${record.path}\nTimestamp: ${record.ts}`;
zip.file(fileName, content);
console.log(`Added snapshot: ${fileName}`);
}
}
// Generate the zip file
console.log("Generating ZIP file...");
const zipBlob = await zip.generateAsync({type: 'blob'});
// Create a download link
const url = URL.createObjectURL(zipBlob);
const a = document.createElement('a');
a.href = url;
a.download = `obsidian-snapshots-${vaultId}-${new Date().toISOString().replace(/:/g, '-')}.zip`;
document.body.appendChild(a);
a.click();
// Clean up
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
console.log("Export complete!");
resolve();
};
getAllRequest.onerror = function(event) {
console.error("Error fetching records:", event.target.error);
reject(event.target.error);
};
};
});
}
}
// Function to list all available vault IDs in IndexedDB
async function listVaultIds() {
console.log("Checking for available vault backups...");
try {
const databases = await indexedDB.databases();
// Filter for backup databases
const backupDbs = databases.filter(db => db.name && db.name.includes('-backup'));
console.log("Found the following vault backups:");
backupDbs.forEach(db => {
const vaultId = db.name.replace('-backup', '');
console.log(`- Vault ID: ${vaultId}`);
});
if (backupDbs.length === 0) {
console.log("No vault backups found");
}
return backupDbs.map(db => db.name.replace('-backup', ''));
} catch (error) {
console.error("Error listing databases:", error);
return [];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment