From ac8734b4771c05ac2fc1bbbd81e883fd330dec83 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 8 Jan 2025 15:18:04 -0800 Subject: [PATCH 01/11] Extract to a consistent folder that is not based on the ZIP filename --- src/account_x.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/account_x.ts b/src/account_x.ts index cd028541..0f0f552c 100644 --- a/src/account_x.ts +++ b/src/account_x.ts @@ -1823,12 +1823,14 @@ export class XAccountController { // Unzip twitter archive to the account data folder using unzipper // Return unzipped path if success, else null. async unzipXArchive(archiveZipPath: string): Promise { - const archiveZip = await unzipper.Open.file(archiveZipPath); if (!this.account) { return null; } - const unzippedPath = path.join(getAccountDataPath("X", this.account.username), path.parse(archiveZipPath).name) + const unzippedPath = path.join(getAccountDataPath("X", this.account.username), "official-x-archive"); + + const archiveZip = await unzipper.Open.file(archiveZipPath); await archiveZip.extract({ path: unzippedPath }); + return unzippedPath } From f27076cbfbfb7e2ddc20ba1f3c2d5a2538c6c75f Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 8 Jan 2025 15:18:46 -0800 Subject: [PATCH 02/11] Do error handling on the unzip and validate steps --- .../src/views/x/XWizardImportingPage.vue | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/views/x/XWizardImportingPage.vue b/src/renderer/src/views/x/XWizardImportingPage.vue index 416adaf8..e3046352 100644 --- a/src/renderer/src/views/x/XWizardImportingPage.vue +++ b/src/renderer/src/views/x/XWizardImportingPage.vue @@ -55,19 +55,35 @@ const startClicked = async () => { importStarted.value = true; // Unarchive the zip + let unzippedPath: string | null = null; statusValidating.value = ImportStatus.Active; - const unzippedPath: string | null = await window.electron.X.unzipXArchive(props.model.account.id, importFromArchivePath.value); + try { + unzippedPath = await window.electron.X.unzipXArchive(props.model.account.id, importFromArchivePath.value); + } catch (e) { + statusValidating.value = ImportStatus.Failed; + errorMessages.value.push(`${e}`); + importFailed.value = true; + return; + } if (unzippedPath === null) { statusValidating.value = ImportStatus.Failed; errorMessages.value.push(unzippedPath); importFailed.value = true; return; } - statusValidating.value = ImportStatus.Finished; // Verify that the archive is valid statusValidating.value = ImportStatus.Active; - const verifyResp: string | null = await window.electron.X.verifyXArchive(props.model.account.id, unzippedPath); + let verifyResp: string | null = null; + try { + verifyResp = await window.electron.X.verifyXArchive(props.model.account.id, unzippedPath); + } catch (e) { + statusValidating.value = ImportStatus.Failed; + errorMessages.value.push(`${e}`); + importFailed.value = true; + await window.electron.X.deleteUnzippedXArchive(props.model.account.id, unzippedPath); + return; + } if (verifyResp !== null) { statusValidating.value = ImportStatus.Failed; errorMessages.value.push(verifyResp); From d05eca04105e5d17a3779e620124cf7df0c265e9 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 8 Jan 2025 15:21:40 -0800 Subject: [PATCH 03/11] Actually, call temp folder to extract archive to "tmp", and log it in output --- src/account_x.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/account_x.ts b/src/account_x.ts index 0f0f552c..2fbfa3ae 100644 --- a/src/account_x.ts +++ b/src/account_x.ts @@ -1826,12 +1826,14 @@ export class XAccountController { if (!this.account) { return null; } - const unzippedPath = path.join(getAccountDataPath("X", this.account.username), "official-x-archive"); + const unzippedPath = path.join(getAccountDataPath("X", this.account.username), "tmp"); const archiveZip = await unzipper.Open.file(archiveZipPath); await archiveZip.extract({ path: unzippedPath }); - return unzippedPath + log.info(`XAccountController.unzipXArchive: unzipped to ${unzippedPath}`); + + return unzippedPath; } // Delete the unzipped X archive once the build is completed From 91ff88028ad00b20890982bf968dd5dd391123fb Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 8 Jan 2025 09:02:29 -0800 Subject: [PATCH 04/11] Fix typescript annotations --- src/account_x.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/account_x.ts b/src/account_x.ts index 2fbfa3ae..5527824e 100644 --- a/src/account_x.ts +++ b/src/account_x.ts @@ -2607,10 +2607,10 @@ export const defineIPCX = () => { } }); - ipcMain.handle('X:deleteUnzippedXArchive', async (_, accountID: number, archivePath: string): Promise => { + ipcMain.handle('X:deleteUnzippedXArchive', async (_, accountID: number, archivePath: string): Promise => { try { const controller = getXAccountController(accountID); - return await controller.deleteUnzippedXArchive(archivePath); + await controller.deleteUnzippedXArchive(archivePath); } catch (error) { throw new Error(packageExceptionForReport(error as Error)); } From 0a36a15dba9f7e5c50de912ce5ac93bb3ad833aa Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 8 Jan 2025 15:54:24 -0800 Subject: [PATCH 05/11] Add a debugXArchive function that lists files in the unzipped archive path, and make it alert those files when importing an X archive --- src/account_x.ts | 20 +++++++++++++++++++ src/preload.ts | 3 +++ src/renderer/src/main.ts | 1 + .../src/views/x/XWizardImportingPage.vue | 5 +++++ 4 files changed, 29 insertions(+) diff --git a/src/account_x.ts b/src/account_x.ts index 5527824e..bbd51528 100644 --- a/src/account_x.ts +++ b/src/account_x.ts @@ -1845,6 +1845,17 @@ export class XAccountController { }); } + async debugXArchive(archivePath: string): Promise { + const filenames = await glob(path.join(archivePath, "*")); + + // Stripe archivePath from the beginning of each filename + for (let i = 0; i < filenames.length; i++) { + filenames[i] = filenames[i].slice(archivePath.length + 1); + } + + return filenames; + } + // Return null on success, and a string (error message) on error async verifyXArchive(archivePath: string): Promise { const foldersToCheck = [ @@ -2616,6 +2627,15 @@ export const defineIPCX = () => { } }); + ipcMain.handle('X:debugXArchive', async (_, accountID: number, archivePath: string): Promise => { + try { + const controller = getXAccountController(accountID); + return await controller.debugXArchive(archivePath); + } catch (error) { + throw new Error(packageExceptionForReport(error as Error)); + } + }); + ipcMain.handle('X:verifyXArchive', async (_, accountID: number, archivePath: string): Promise => { try { const controller = getXAccountController(accountID); diff --git a/src/preload.ts b/src/preload.ts index 42b69584..cc1d2d12 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -251,6 +251,9 @@ contextBridge.exposeInMainWorld('electron', { deleteUnzippedXArchive: (accountID: number, archivePath: string): Promise => { return ipcRenderer.invoke('X:deleteUnzippedXArchive', accountID, archivePath); }, + debugXArchive: (accountID: number, archivePath: string): Promise => { + return ipcRenderer.invoke('X:debugXArchive', accountID, archivePath); + }, verifyXArchive: (accountID: number, archivePath: string): Promise => { return ipcRenderer.invoke('X:verifyXArchive', accountID, archivePath); }, diff --git a/src/renderer/src/main.ts b/src/renderer/src/main.ts index dbb42904..76be39b1 100644 --- a/src/renderer/src/main.ts +++ b/src/renderer/src/main.ts @@ -106,6 +106,7 @@ declare global { deleteDMsScrollToBottom: (accountID: number) => Promise; unzipXArchive: (accountID: number, archivePath: string) => Promise; deleteUnzippedXArchive: (accountID: number, archivePath: string) => Promise; + debugXArchive: (accountID: number, archivePath: string) => Promise; verifyXArchive: (accountID: number, archivePath: string) => Promise; importXArchive: (accountID: number, archivePath: string, dataType: string) => Promise; getCookie: (accountID: number, name: string) => Promise; diff --git a/src/renderer/src/views/x/XWizardImportingPage.vue b/src/renderer/src/views/x/XWizardImportingPage.vue index e3046352..11539035 100644 --- a/src/renderer/src/views/x/XWizardImportingPage.vue +++ b/src/renderer/src/views/x/XWizardImportingPage.vue @@ -72,6 +72,11 @@ const startClicked = async () => { return; } + // Debugging + const debugResp = await window.electron.X.debugXArchive(props.model.account.id, unzippedPath); + console.log(debugResp); + await window.electron.showMessage(`Path: ${unzippedPath}\n\nFilenames:\n${debugResp.join('\n')}`); + // Verify that the archive is valid statusValidating.value = ImportStatus.Active; let verifyResp: string | null = null; From 25f8639ac6b913f8f6acc4bfc1f60a613660bb20 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Wed, 8 Jan 2025 16:01:22 -0800 Subject: [PATCH 06/11] Version bump to 1.0.23-dev --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 756f179f..3d226e6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyd", - "version": "1.0.22", + "version": "1.0.23-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cyd", - "version": "1.0.22", + "version": "1.0.23-dev", "license": "proprietary", "dependencies": { "@fortawesome/fontawesome-free": "^6.5.2", diff --git a/package.json b/package.json index 1e93d14b..fd065623 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cyd", "private": true, - "version": "1.0.22", + "version": "1.0.23-dev", "main": ".vite/build/main.js", "description": "Automatically delete your data from tech platforms, except for what you want to keep", "license": "proprietary", From 099c2be157aa494cd00b315d2b7c3f9df5aa93b2 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 12 Jan 2025 15:12:11 -0800 Subject: [PATCH 07/11] Revert "Add a debugXArchive function that lists files in the unzipped archive path, and make it alert those files when importing an X archive" This reverts commit 0a36a15dba9f7e5c50de912ce5ac93bb3ad833aa. --- src/account_x.ts | 20 ------------------- src/preload.ts | 3 --- src/renderer/src/main.ts | 1 - .../src/views/x/XWizardImportingPage.vue | 5 ----- 4 files changed, 29 deletions(-) diff --git a/src/account_x.ts b/src/account_x.ts index bbd51528..5527824e 100644 --- a/src/account_x.ts +++ b/src/account_x.ts @@ -1845,17 +1845,6 @@ export class XAccountController { }); } - async debugXArchive(archivePath: string): Promise { - const filenames = await glob(path.join(archivePath, "*")); - - // Stripe archivePath from the beginning of each filename - for (let i = 0; i < filenames.length; i++) { - filenames[i] = filenames[i].slice(archivePath.length + 1); - } - - return filenames; - } - // Return null on success, and a string (error message) on error async verifyXArchive(archivePath: string): Promise { const foldersToCheck = [ @@ -2627,15 +2616,6 @@ export const defineIPCX = () => { } }); - ipcMain.handle('X:debugXArchive', async (_, accountID: number, archivePath: string): Promise => { - try { - const controller = getXAccountController(accountID); - return await controller.debugXArchive(archivePath); - } catch (error) { - throw new Error(packageExceptionForReport(error as Error)); - } - }); - ipcMain.handle('X:verifyXArchive', async (_, accountID: number, archivePath: string): Promise => { try { const controller = getXAccountController(accountID); diff --git a/src/preload.ts b/src/preload.ts index cc1d2d12..42b69584 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -251,9 +251,6 @@ contextBridge.exposeInMainWorld('electron', { deleteUnzippedXArchive: (accountID: number, archivePath: string): Promise => { return ipcRenderer.invoke('X:deleteUnzippedXArchive', accountID, archivePath); }, - debugXArchive: (accountID: number, archivePath: string): Promise => { - return ipcRenderer.invoke('X:debugXArchive', accountID, archivePath); - }, verifyXArchive: (accountID: number, archivePath: string): Promise => { return ipcRenderer.invoke('X:verifyXArchive', accountID, archivePath); }, diff --git a/src/renderer/src/main.ts b/src/renderer/src/main.ts index 76be39b1..dbb42904 100644 --- a/src/renderer/src/main.ts +++ b/src/renderer/src/main.ts @@ -106,7 +106,6 @@ declare global { deleteDMsScrollToBottom: (accountID: number) => Promise; unzipXArchive: (accountID: number, archivePath: string) => Promise; deleteUnzippedXArchive: (accountID: number, archivePath: string) => Promise; - debugXArchive: (accountID: number, archivePath: string) => Promise; verifyXArchive: (accountID: number, archivePath: string) => Promise; importXArchive: (accountID: number, archivePath: string, dataType: string) => Promise; getCookie: (accountID: number, name: string) => Promise; diff --git a/src/renderer/src/views/x/XWizardImportingPage.vue b/src/renderer/src/views/x/XWizardImportingPage.vue index 11539035..e3046352 100644 --- a/src/renderer/src/views/x/XWizardImportingPage.vue +++ b/src/renderer/src/views/x/XWizardImportingPage.vue @@ -72,11 +72,6 @@ const startClicked = async () => { return; } - // Debugging - const debugResp = await window.electron.X.debugXArchive(props.model.account.id, unzippedPath); - console.log(debugResp); - await window.electron.showMessage(`Path: ${unzippedPath}\n\nFilenames:\n${debugResp.join('\n')}`); - // Verify that the archive is valid statusValidating.value = ImportStatus.Active; let verifyResp: string | null = null; From 04626322989890ed7dd3dc5e363299753f6b827e Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Sun, 12 Jan 2025 15:41:37 -0800 Subject: [PATCH 08/11] Refactor IPC functions showSelectZIPFileDialog and showSelectFolderDialog into a single function, showOpenDialog, with settings to allow for selecting folders, selecting files, and filtering files. Make the two places that used these functions (advanced settings modal, and importing X archive) use the new function. When importing the X archive, if the path selected is not a ZIP file, treat it like an already unzipped folder. --- src/main.ts | 35 ++++++---------- src/preload.ts | 9 ++-- src/renderer/src/main.ts | 5 ++- .../src/modals/AdvancedSettingsModal.vue | 2 +- src/renderer/src/test_util.ts | 2 +- .../src/views/x/XWizardImportingPage.vue | 42 +++++++++++-------- 6 files changed, 44 insertions(+), 51 deletions(-) diff --git a/src/main.ts b/src/main.ts index b758a087..73a5489b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,7 +14,8 @@ import { nativeImage, autoUpdater, powerSaveBlocker, - powerMonitor + powerMonitor, + FileFilter } from 'electron'; import { updateElectronApp, UpdateSourceType } from 'update-electron-app'; @@ -343,34 +344,22 @@ async function createWindow() { } }); - ipcMain.handle('showSelectZIPFileDialog', async (_): Promise => { + ipcMain.handle('showOpenDialog', async (_, selectFolders: boolean, selectFiles: boolean, fileFilters: FileFilter[] | undefined = undefined): Promise => { const dataPath = database.getConfig('dataPath'); - const options: Electron.OpenDialogSyncOptions = { - filters: [{ name: 'Archive', extensions: ['zip'] }], - properties: ['openFile'], - }; - - if (dataPath) { - options.defaultPath = dataPath; + const properties: ("openFile" | "openDirectory" | "multiSelections" | "showHiddenFiles" | "createDirectory" | "promptToCreate" | "noResolveAliases" | "treatPackageAsDirectory" | "dontAddToRecent")[] = []; + if (selectFolders) { + properties.push('openDirectory'); + properties.push('createDirectory'); + properties.push('promptToCreate'); } - - try { - const result = dialog.showOpenDialogSync(win, options); - if (result && result.length > 0) { - return result[0]; - } - return null; - } catch (error) { - throw new Error(packageExceptionForReport(error as Error)); + if (selectFiles) { + properties.push('openFile'); } - }); - - ipcMain.handle('showSelectFolderDialog', async (_): Promise => { - const dataPath = database.getConfig('dataPath'); const options: Electron.OpenDialogSyncOptions = { - properties: ['openDirectory', 'createDirectory', 'promptToCreate'], + properties: properties, + filters: fileFilters, }; if (dataPath) { options.defaultPath = dataPath; diff --git a/src/preload.ts b/src/preload.ts index 42b69584..b0cdf5f7 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,4 +1,4 @@ -import { contextBridge, ipcRenderer } from 'electron' +import { contextBridge, ipcRenderer, FileFilter } from 'electron' import { ErrorReport, Account, @@ -51,11 +51,8 @@ contextBridge.exposeInMainWorld('electron', { showQuestion: (message: string, trueText: string, falseText: string): Promise => { return ipcRenderer.invoke('showQuestion', message, trueText, falseText) }, - showSelectZIPFileDialog: (): Promise => { - return ipcRenderer.invoke('showSelectZIPFileDialog') - }, - showSelectFolderDialog: (): Promise => { - return ipcRenderer.invoke('showSelectFolderDialog') + showOpenDialog: (selectFolders: boolean, selectFiles: boolean, fileFilters: FileFilter[] | undefined = undefined): Promise => { + return ipcRenderer.invoke('showOpenDialog', selectFolders, selectFiles, fileFilters) }, openURL: (url: string) => { ipcRenderer.invoke('openURL', url) diff --git a/src/renderer/src/main.ts b/src/renderer/src/main.ts index dbb42904..e6e57619 100644 --- a/src/renderer/src/main.ts +++ b/src/renderer/src/main.ts @@ -24,6 +24,8 @@ import type { } from "../../shared_types"; import App from "./App.vue"; +import { FileFilter } from "electron"; + declare global { interface Window { electron: { @@ -38,8 +40,7 @@ declare global { showMessage: (message: string) => void; showError: (message: string) => void; showQuestion: (message: string, trueText: string, falseText: string) => Promise; - showSelectZIPFileDialog: () => Promise; - showSelectFolderDialog: () => Promise; + showOpenDialog: (selectFolders: boolean, selectFiles: boolean, fileFilters: FileFilter[] | undefined) => Promise; openURL: (url: string) => void; loadFileInWebview: (webContentsId: number, filename: string) => void; getAccountDataPath: (accountID: number, filename: string) => Promise, diff --git a/src/renderer/src/modals/AdvancedSettingsModal.vue b/src/renderer/src/modals/AdvancedSettingsModal.vue index 198b0ab2..cab59ce3 100644 --- a/src/renderer/src/modals/AdvancedSettingsModal.vue +++ b/src/renderer/src/modals/AdvancedSettingsModal.vue @@ -13,7 +13,7 @@ let modalInstance: Modal | null = null; const dataPath = ref(''); const browseClicked = async () => { - const newDataPath = await window.electron.showSelectFolderDialog(); + const newDataPath = await window.electron.showOpenDialog(true, false, undefined); if (newDataPath) { dataPath.value = newDataPath; await window.electron.database.setConfig('dataPath', newDataPath); diff --git a/src/renderer/src/test_util.ts b/src/renderer/src/test_util.ts index 3c6e56f5..7f925ceb 100644 --- a/src/renderer/src/test_util.ts +++ b/src/renderer/src/test_util.ts @@ -11,7 +11,7 @@ export const stubElectron = () => { showMessage: cy.stub(), showError: cy.stub(), showQuestion: cy.stub(), - showSelectFolderDialog: cy.stub(), + showOpenDialog: cy.stub(), openURL: cy.stub(), loadFileInWebview: cy.stub(), getAccountDataPath: cy.stub(), diff --git a/src/renderer/src/views/x/XWizardImportingPage.vue b/src/renderer/src/views/x/XWizardImportingPage.vue index e3046352..7eea1c14 100644 --- a/src/renderer/src/views/x/XWizardImportingPage.vue +++ b/src/renderer/src/views/x/XWizardImportingPage.vue @@ -53,23 +53,28 @@ const createCountString = (importCount: number, skipCount: number) => { const startClicked = async () => { errorMessages.value = []; importStarted.value = true; - - // Unarchive the zip let unzippedPath: string | null = null; - statusValidating.value = ImportStatus.Active; - try { - unzippedPath = await window.electron.X.unzipXArchive(props.model.account.id, importFromArchivePath.value); - } catch (e) { - statusValidating.value = ImportStatus.Failed; - errorMessages.value.push(`${e}`); - importFailed.value = true; - return; - } - if (unzippedPath === null) { - statusValidating.value = ImportStatus.Failed; - errorMessages.value.push(unzippedPath); - importFailed.value = true; - return; + + // Does importFromArchivePath end with .zip? + if (!importFromArchivePath.value.endsWith('.zip')) { + unzippedPath = importFromArchivePath.value; + } else { + // Unarchive the zip + statusValidating.value = ImportStatus.Active; + try { + unzippedPath = await window.electron.X.unzipXArchive(props.model.account.id, importFromArchivePath.value); + } catch (e) { + statusValidating.value = ImportStatus.Failed; + errorMessages.value.push(`${e}`); + importFailed.value = true; + return; + } + if (unzippedPath === null) { + statusValidating.value = ImportStatus.Failed; + errorMessages.value.push(unzippedPath); + importFailed.value = true; + return; + } } // Verify that the archive is valid @@ -145,7 +150,7 @@ const startClicked = async () => { }; const importFromArchiveBrowserClicked = async () => { - const path = await window.electron.showSelectZIPFileDialog(); + const path = await window.electron.showOpenDialog(true, true, [{ name: 'ZIP Archive', extensions: ['zip'] }]); if (path) { importFromArchivePath.value = path; } @@ -195,7 +200,8 @@ const iconFromStatus = (status: ImportStatus) => {