Last active
March 2, 2025 12:58
-
-
Save yyogo/42f131c2c89bb3d8550e334ec38d6e2d to your computer and use it in GitHub Desktop.
Extract file recovery snapshots from Obsidian
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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