Skip to content

Commit

Permalink
Merge pull request #365 from lockdown-systems/364-x-archives
Browse files Browse the repository at this point in the history
Fix importing X archive
  • Loading branch information
SaptakS authored Jan 13, 2025
2 parents 48ec0ea + 9c80a6a commit 238130d
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 58 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "cyd",
"private": true,
"version": "1.0.22",
"version": "1.0.24-dev",
"main": ".vite/build/main.js",
"description": "Automatically delete your data from tech platforms, except for what you want to keep",
"license": "proprietary",
Expand Down
10 changes: 7 additions & 3 deletions src/account_x/x_account_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1718,13 +1718,17 @@ 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<string | null> {
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), "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
Expand Down
35 changes: 12 additions & 23 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
nativeImage,
autoUpdater,
powerSaveBlocker,
powerMonitor
powerMonitor,
FileFilter
} from 'electron';
import { updateElectronApp, UpdateSourceType } from 'update-electron-app';

Expand Down Expand Up @@ -343,34 +344,22 @@ async function createWindow() {
}
});

ipcMain.handle('showSelectZIPFileDialog', async (_): Promise<string | null> => {
ipcMain.handle('showOpenDialog', async (_, selectFolders: boolean, selectFiles: boolean, fileFilters: FileFilter[] | undefined = undefined): Promise<string | null> => {
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<string | null> => {
const dataPath = database.getConfig('dataPath');

const options: Electron.OpenDialogSyncOptions = {
properties: ['openDirectory', 'createDirectory', 'promptToCreate'],
properties: properties,
filters: fileFilters,
};
if (dataPath) {
options.defaultPath = dataPath;
Expand Down
9 changes: 3 additions & 6 deletions src/preload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { contextBridge, ipcRenderer } from 'electron'
import { contextBridge, ipcRenderer, FileFilter } from 'electron'
import {
ErrorReport,
Account,
Expand Down Expand Up @@ -51,11 +51,8 @@ contextBridge.exposeInMainWorld('electron', {
showQuestion: (message: string, trueText: string, falseText: string): Promise<boolean> => {
return ipcRenderer.invoke('showQuestion', message, trueText, falseText)
},
showSelectZIPFileDialog: (): Promise<string | null> => {
return ipcRenderer.invoke('showSelectZIPFileDialog')
},
showSelectFolderDialog: (): Promise<string | null> => {
return ipcRenderer.invoke('showSelectFolderDialog')
showOpenDialog: (selectFolders: boolean, selectFiles: boolean, fileFilters: FileFilter[] | undefined = undefined): Promise<string | null> => {
return ipcRenderer.invoke('showOpenDialog', selectFolders, selectFiles, fileFilters)
},
openURL: (url: string) => {
ipcRenderer.invoke('openURL', url)
Expand Down
5 changes: 3 additions & 2 deletions src/renderer/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import type {
} from "../../shared_types";
import App from "./App.vue";

import { FileFilter } from "electron";

declare global {
interface Window {
electron: {
Expand All @@ -38,8 +40,7 @@ declare global {
showMessage: (message: string) => void;
showError: (message: string) => void;
showQuestion: (message: string, trueText: string, falseText: string) => Promise<boolean>;
showSelectZIPFileDialog: () => Promise<string | null>;
showSelectFolderDialog: () => Promise<string | null>;
showOpenDialog: (selectFolders: boolean, selectFiles: boolean, fileFilters: FileFilter[] | undefined) => Promise<string | null>;
openURL: (url: string) => void;
loadFileInWebview: (webContentsId: number, filename: string) => void;
getAccountDataPath: (accountID: number, filename: string) => Promise<string | null>,
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/modals/AdvancedSettingsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 2 additions & 3 deletions src/renderer/src/test_util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ export const stubElectron = () => {
shouldOpenDevtools: cy.stub().resolves(false),
showMessage: cy.stub(),
showError: cy.stub(),
showQuestion: cy.stub().resolves(true),
showSelectZIPFileDialog: cy.stub().resolves(null),
showSelectFolderDialog: cy.stub().resolves(null),
showQuestion: cy.stub(),
showOpenDialog: cy.stub(),
openURL: cy.stub(),
loadFileInWebview: cy.stub(),
getAccountDataPath: cy.stub().resolves(null),
Expand Down
89 changes: 72 additions & 17 deletions src/renderer/src/views/x/XWizardImportingPage.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script setup lang="ts">
import {
ref,
getCurrentInstance
getCurrentInstance,
onMounted
} from 'vue';
import {
AccountXViewModel,
Expand All @@ -25,6 +26,10 @@ const emit = defineEmits<{
setState: [value: State]
}>()
// Keep track of platform
// Mac users can browse for both ZIP or unzipped folder at once, but Windows and Linux users need two separate buttons
const platform = ref('');
// Buttons
const backClicked = async () => {
emit('setState', State.WizardImportStart);
Expand Down Expand Up @@ -53,21 +58,42 @@ const createCountString = (importCount: number, skipCount: number) => {
const startClicked = async () => {
errorMessages.value = [];
importStarted.value = true;
let unzippedPath: string | null = null;
// 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;
}
}
// Unarchive the zip
// Verify that the archive is valid
statusValidating.value = ImportStatus.Active;
const unzippedPath: string | null = await window.electron.X.unzipXArchive(props.model.account.id, importFromArchivePath.value);
if (unzippedPath === null) {
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(unzippedPath);
errorMessages.value.push(`${e}`);
importFailed.value = true;
await window.electron.X.deleteUnzippedXArchive(props.model.account.id, unzippedPath);
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);
if (verifyResp !== null) {
statusValidating.value = ImportStatus.Failed;
errorMessages.value.push(verifyResp);
Expand Down Expand Up @@ -128,8 +154,22 @@ const startClicked = async () => {
};
const importFromArchiveBrowserClicked = async () => {
const path = await window.electron.showSelectZIPFileDialog();
const importFromArchiveBrowseClicked = async () => {
const path = await window.electron.showOpenDialog(true, true, [{ name: 'ZIP Archive', extensions: ['zip'] }]);
if (path) {
importFromArchivePath.value = path;
}
};
const importFromArchiveBrowseZipClicked = async () => {
const path = await window.electron.showOpenDialog(false, true, [{ name: 'ZIP Archive', extensions: ['zip'] }]);
if (path) {
importFromArchivePath.value = path;
}
};
const importFromArchiveBrowseFolderClicked = async () => {
const path = await window.electron.showOpenDialog(true, false, undefined);
if (path) {
importFromArchivePath.value = path;
}
Expand Down Expand Up @@ -170,6 +210,10 @@ const iconFromStatus = (status: ImportStatus) => {
}
};
onMounted(async () => {
platform.value = await window.electron.getPlatform();
});
</script>

<template>
Expand All @@ -179,7 +223,8 @@ const iconFromStatus = (status: ImportStatus) => {
</h2>
<p class="text-muted">
<template v-if="!importStarted">
Browse for the ZIP file of the X archive you downloaded.
Browse for the ZIP file of the X archive you downloaded, or the folder where you have already extracted
it.
</template>
<template v-else>
Importing your archive...
Expand All @@ -190,9 +235,19 @@ const iconFromStatus = (status: ImportStatus) => {
<div class="input-group">
<input v-model="importFromArchivePath" type="text" class="form-control"
placeholder="Import your X archive" readonly>
<button class="btn btn-secondary" @click="importFromArchiveBrowserClicked">
Browse for Archive
</button>
<template v-if="platform == 'darwin'">
<button class="btn btn-secondary" @click="importFromArchiveBrowseClicked">
Browse for Archive
</button>
</template>
<template v-else>
<button class="btn btn-secondary me-1" @click="importFromArchiveBrowseZipClicked">
Browse for ZIP
</button>
<button class="btn btn-secondary" @click="importFromArchiveBrowseFolderClicked">
Browse for Unzipped Folder
</button>
</template>
</div>

<div class="buttons">
Expand All @@ -216,7 +271,7 @@ const iconFromStatus = (status: ImportStatus) => {
<i v-else>
<RunningIcon />
</i>
Unzipping and validating X archive
Validating X archive
</li>
<li :class="statusImportingTweets == ImportStatus.Pending ? 'text-muted' : ''">
<i v-if="statusImportingTweets != ImportStatus.Active"
Expand Down

0 comments on commit 238130d

Please sign in to comment.