import { WebApplication } from '@/ui_models/application'; import { EncryptionIntent, ProtectedAction, SNItem, ContentType, SNNote } from 'snjs'; export class ArchiveManager { private readonly application: WebApplication private textFile?: string constructor(application: WebApplication) { this.application = application; } public async downloadBackup(encrypted: boolean) { return this.downloadBackupOfItems(this.application.allItems(), encrypted); } public async downloadBackupOfItems(items: SNItem[], encrypted: boolean) { const run = async () => { // download in Standard Notes format const intent = encrypted ? EncryptionIntent.FileEncrypted : EncryptionIntent.FileDecrypted; this.itemsData(items, intent).then((data) => { const modifier = encrypted ? 'Encrypted' : 'Decrypted'; this.downloadData( data!, `Standard Notes ${modifier} Backup - ${this.formattedDate()}.txt` ); // download as zipped plain text files if (!encrypted) { this.downloadZippedItems(items); } }); }; if ( await this.application.privilegesService! .actionRequiresPrivilege(ProtectedAction.ManageBackups) ) { this.application.presentPrivilegesModal( ProtectedAction.ManageBackups, () => { run(); }); } else { run(); } } private formattedDate() { const string = `${new Date()}`; // Match up to the first parenthesis, i.e do not include '(Central Standard Time)' const matches = string.match(/^(.*?) \(/); if (matches && matches.length >= 2) { return matches[1]; } return string; } private async itemsData(items: SNItem[], intent: EncryptionIntent) { const data = await this.application.createBackupFile(items, intent); if (!data) { return undefined; } const blobData = new Blob([data], { type: 'text/json' }); return blobData; } private get zip() { return (window as any).zip; } private async loadZip() { if (this.zip) { return; } const scriptTag = document.createElement('script'); scriptTag.src = '/assets/zip/zip.js'; scriptTag.async = false; const headTag = document.getElementsByTagName('head')[0]; headTag.appendChild(scriptTag); return new Promise((resolve) => { scriptTag.onload = () => { this.zip.workerScriptsPath = 'assets/zip/'; resolve(); }; }); } private async downloadZippedItems(items: SNItem[]) { await this.loadZip(); this.zip.createWriter( new this.zip.BlobWriter('application/zip'), (zipWriter: any) => { let index = 0; const nextFile = () => { const item = items[index]; let name, contents; if (item.content_type === ContentType.Note) { const note = item as SNNote; name = note.title; contents = note.text; } else { name = item.content_type; contents = JSON.stringify(item.content, null, 2); } if (!name) { name = ''; } const blob = new Blob([contents], { type: 'text/plain' }); let filePrefix = name.replace(/\//g, '').replace(/\\+/g, ''); const fileSuffix = `-${item.uuid.split('-')[0]}.txt`; // Standard max filename length is 255. Slice the note name down to allow filenameEnd filePrefix = filePrefix.slice(0, (255 - fileSuffix.length)); const fileName = `${item.content_type}/${filePrefix}${fileSuffix}`; zipWriter.add(fileName, new this.zip.BlobReader(blob), () => { index++; if (index < items.length) { nextFile(); } else { zipWriter.close((blob: any) => { this.downloadData( blob, `Standard Notes Backup - ${this.formattedDate()}.zip` ); zipWriter = null; }); } }); }; nextFile(); }, onerror); } private hrefForData(data: Blob) { // If we are replacing a previously generated file we need to // manually revoke the object URL to avoid memory leaks. if (this.textFile) { window.URL.revokeObjectURL(this.textFile); } this.textFile = window.URL.createObjectURL(data); // returns a URL you can use as a href return this.textFile; } private downloadData(data: Blob, fileName: string) { const link = document.createElement('a'); link.setAttribute('download', fileName); link.href = this.hrefForData(data); document.body.appendChild(link); link.click(); link.remove(); } }