Skip to content

Commit 0721ee7

Browse files
author
Erika Perugachi
authored
Merge pull request #1088 from JulianAdams4/backup-restore
Mailbox Backup: Export & Restore ☠
2 parents 069efc2 + e1a2308 commit 0721ee7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2461
-102
lines changed

electron_app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
"line-by-line": "^0.1.6",
148148
"moment": "^2.22.2",
149149
"os-locale": "^3.0.1",
150+
"recursive-copy": "^2.0.10",
150151
"rimraf": "^2.6.3",
151152
"sqlite3": "4.0.2",
152153
"unused-filename": "^2.1.0",

electron_app/src/BackupManager.js

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const zlib = require('zlib');
4+
const { app } = require('electron');
5+
const copy = require('recursive-copy');
6+
const myAccount = require('./Account');
7+
const { databasePath } = require('./models');
8+
const { APP_DOMAIN } = require('./utils/const');
9+
const { createPathRecursive, getUserEmailsPath } = require('./utils/FileUtils');
10+
const {
11+
decryptStreamFileWithPassword,
12+
encryptStreamFile,
13+
exportDatabaseToFile,
14+
generateKeyAndIvFromPassword,
15+
importDatabaseFromFile
16+
} = require('./dbExporter');
17+
18+
const STREAM_SIZE = 512 * 1024;
19+
20+
/* Folders & Paths
21+
----------------------------- */
22+
const TempBackupFolderName = 'BackupData';
23+
const TempDatabaseBackupFileName = 'CriptextBackup.db';
24+
const TempEmailsBackupFolderName = 'EmailsBackup';
25+
const TempBackupDirectory = path.join(databasePath, '..', TempBackupFolderName);
26+
const TempDatabaseBackupPath = path.join(
27+
TempBackupDirectory,
28+
TempDatabaseBackupFileName
29+
);
30+
const TempEmailsBackupPath = path.join(
31+
TempBackupDirectory,
32+
TempEmailsBackupFolderName
33+
);
34+
const ExportUnencryptedFilename = path.join(
35+
TempBackupDirectory,
36+
'unencrypt.exp'
37+
);
38+
const ExportZippedFilename = path.join(TempBackupDirectory, 'zipped.exp');
39+
const ExportEncryptedFilename = path.join(TempBackupDirectory, 'encrypted.exp');
40+
41+
const RestoreUnzippedFilename = path.join(TempBackupDirectory, 'unzipped.res');
42+
const RestoreUnencryptedFilename = path.join(
43+
TempBackupDirectory,
44+
'unencryp.res'
45+
);
46+
47+
/* Temp Directory
48+
----------------------------- */
49+
const checkTempBackupDirectory = () => {
50+
try {
51+
if (fs.existsSync(TempBackupDirectory)) {
52+
removeTempBackupDirectoryRecursive(TempBackupDirectory);
53+
}
54+
fs.mkdirSync(TempBackupDirectory);
55+
} catch (e) {
56+
throw new Error('Unable to create temp folder');
57+
}
58+
};
59+
60+
const removeTempBackupDirectoryRecursive = pathToDelete => {
61+
if (fs.existsSync(pathToDelete)) {
62+
fs.readdirSync(pathToDelete).forEach(file => {
63+
const currentPath = path.join(pathToDelete, file);
64+
if (fs.lstatSync(currentPath).isDirectory()) {
65+
removeTempBackupDirectoryRecursive(currentPath);
66+
} else {
67+
fs.unlinkSync(currentPath);
68+
}
69+
});
70+
fs.rmdirSync(pathToDelete);
71+
}
72+
};
73+
74+
/* Default Directory
75+
----------------------------- */
76+
function getUsername() {
77+
return myAccount.recipientId.includes('@')
78+
? myAccount.recipientId
79+
: `${myAccount.recipientId}@${APP_DOMAIN}`;
80+
}
81+
82+
const getDefaultBackupFolder = () => {
83+
return path.join(
84+
app.getPath('documents'),
85+
'Criptext',
86+
getUsername(),
87+
'backups'
88+
);
89+
};
90+
91+
const createDefaultBackupFolder = () => {
92+
const backupPath = getDefaultBackupFolder();
93+
return createPathRecursive(backupPath);
94+
};
95+
96+
/* Methods
97+
----------------------------- */
98+
const createTempDatabaseBackup = () => {
99+
return new Promise((resolve, reject) => {
100+
fs.copyFile(databasePath, TempDatabaseBackupPath, cpErr => {
101+
if (cpErr) reject({ error: 'Preparing backup error' });
102+
resolve();
103+
});
104+
});
105+
};
106+
107+
const createTempEmailsBackup = () => {
108+
const EmailsFolder = getUserEmailsPath(process.env.NODE_ENV, getUsername());
109+
return new Promise((resolve, reject) => {
110+
try {
111+
copy(EmailsFolder, TempEmailsBackupPath);
112+
resolve();
113+
} catch (error) {
114+
reject({ error: 'Preparing backup error' });
115+
}
116+
});
117+
};
118+
119+
const zipStreamFile = ({ inputFile, outputFile }) => {
120+
return new Promise((resolve, reject) => {
121+
const highWaterMark = STREAM_SIZE;
122+
const reader = fs.createReadStream(inputFile, { highWaterMark });
123+
const writer = fs.createWriteStream(outputFile);
124+
reader
125+
.pipe(zlib.createGzip())
126+
.pipe(writer)
127+
.on('error', reject)
128+
.on('finish', resolve);
129+
});
130+
};
131+
132+
const unzipStreamFile = ({ inputFile, outputFile }) => {
133+
return new Promise((resolve, reject) => {
134+
const highWaterMark = STREAM_SIZE;
135+
const reader = fs.createReadStream(inputFile, { highWaterMark });
136+
const writer = fs.createWriteStream(outputFile);
137+
reader
138+
.pipe(zlib.createGunzip())
139+
.pipe(writer)
140+
.on('error', reject)
141+
.on('finish', resolve);
142+
});
143+
};
144+
145+
const prepareBackupFiles = async ({ backupPrevFiles = true }) => {
146+
try {
147+
checkTempBackupDirectory();
148+
if (backupPrevFiles) {
149+
await createTempDatabaseBackup();
150+
await createTempEmailsBackup();
151+
}
152+
} catch (error) {
153+
removeTempBackupDirectoryRecursive(TempBackupDirectory);
154+
return error;
155+
}
156+
};
157+
158+
/* Export Backup
159+
----------------------------- */
160+
const exportBackupUnencrypted = async ({ backupPath }) => {
161+
try {
162+
try {
163+
await exportDatabaseToFile({
164+
databasePath: TempDatabaseBackupPath,
165+
outputPath: ExportUnencryptedFilename
166+
});
167+
} catch (dbErr) {
168+
throw new Error('Failed to export database');
169+
}
170+
// Compress backup file
171+
try {
172+
await zipStreamFile({
173+
inputFile: ExportUnencryptedFilename,
174+
outputFile: ExportZippedFilename
175+
});
176+
} catch (zipErr) {
177+
throw new Error('Failed to compress backup file');
178+
}
179+
// Move to destination
180+
try {
181+
fs.writeFileSync(backupPath, fs.readFileSync(ExportZippedFilename));
182+
} catch (fileErr) {
183+
throw new Error('Failed to move backup file');
184+
}
185+
} catch (exportBackupError) {
186+
throw exportBackupError;
187+
} finally {
188+
removeTempBackupDirectoryRecursive(TempBackupDirectory);
189+
}
190+
};
191+
192+
const exportBackupEncrypted = async ({ backupPath, password }) => {
193+
try {
194+
// Export database
195+
try {
196+
await exportDatabaseToFile({
197+
databasePath: TempDatabaseBackupPath,
198+
outputPath: ExportUnencryptedFilename
199+
});
200+
} catch (dbErr) {
201+
throw new Error('Failed to export database');
202+
}
203+
// GZip & Encrypt
204+
try {
205+
const { key, iv, salt } = generateKeyAndIvFromPassword(password);
206+
await encryptStreamFile({
207+
inputFile: ExportUnencryptedFilename,
208+
outputFile: ExportEncryptedFilename,
209+
key,
210+
iv,
211+
salt
212+
});
213+
} catch (encryptErr) {
214+
throw new Error('Failed to encrypt backup');
215+
}
216+
// Move to destination
217+
try {
218+
fs.writeFileSync(backupPath, fs.readFileSync(ExportEncryptedFilename));
219+
} catch (fileErr) {
220+
throw new Error('Failed to create backup file');
221+
}
222+
} catch (exportBackupError) {
223+
throw exportBackupError;
224+
} finally {
225+
removeTempBackupDirectoryRecursive(TempBackupDirectory);
226+
}
227+
};
228+
229+
/* Restore Backup
230+
----------------------------- */
231+
const restoreUnencryptedBackup = async ({ filePath }) => {
232+
try {
233+
// Unzip file
234+
try {
235+
await unzipStreamFile({
236+
inputFile: filePath,
237+
outputFile: RestoreUnzippedFilename
238+
});
239+
} catch (unzipError) {
240+
throw new Error('Failed to unzip backup file');
241+
}
242+
// Import to database
243+
try {
244+
await importDatabaseFromFile({
245+
filepath: RestoreUnzippedFilename,
246+
databasePath: databasePath
247+
});
248+
} catch (importError) {
249+
throw new Error('Failed to import into database');
250+
}
251+
} catch (restoreBackupError) {
252+
throw restoreBackupError;
253+
} finally {
254+
removeTempBackupDirectoryRecursive(TempBackupDirectory);
255+
}
256+
};
257+
258+
const restoreEncryptedBackup = async ({ filePath, password }) => {
259+
try {
260+
// Decrypt & Unzip
261+
try {
262+
await await decryptStreamFileWithPassword({
263+
inputFile: filePath,
264+
outputFile: RestoreUnencryptedFilename,
265+
password
266+
});
267+
} catch (decryptErr) {
268+
throw new Error('Failed to decrypt backup file');
269+
}
270+
// Import to database
271+
try {
272+
await importDatabaseFromFile({
273+
filepath: RestoreUnencryptedFilename,
274+
databasePath: databasePath
275+
});
276+
} catch (importError) {
277+
throw new Error('Failed to import into database');
278+
}
279+
} catch (restoreBackupError) {
280+
throw restoreBackupError;
281+
} finally {
282+
removeTempBackupDirectoryRecursive(TempBackupDirectory);
283+
}
284+
};
285+
286+
module.exports = {
287+
createDefaultBackupFolder,
288+
getDefaultBackupFolder,
289+
prepareBackupFiles,
290+
exportBackupUnencrypted,
291+
exportBackupEncrypted,
292+
restoreUnencryptedBackup,
293+
restoreEncryptedBackup
294+
};

electron_app/src/DBManager.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1234,7 +1234,11 @@ const getSettings = async () => {
12341234
};
12351235

12361236
const updateSettings = async ({ language, opened, theme }) => {
1237-
const params = noNulls({ language, opened, theme });
1237+
const params = noNulls({
1238+
language,
1239+
opened,
1240+
theme
1241+
});
12381242
if (Object.keys(params).length < 1) {
12391243
return Promise.resolve(true);
12401244
}

electron_app/src/__integrations__/DbExporter.integration.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const fs = require('fs');
2323
const myAccount = require('../Account');
2424
const { APP_DOMAIN } = require('../utils/const');
2525

26+
jest.mock('./../Account.js');
27+
2628
const DATABASE_PATH = `${__dirname}/test.db`;
2729
const PARSED_SAMPLE_FILEPATH = `${__dirname}/parsed_sample_file.txt`;
2830
const TEMP_DIRECTORY = '/tmp/criptext-tests';

electron_app/src/__integrations__/parsed_sample_file.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{"fileVersion":"5","recipientId":"test","domain":"criptext.com"}
12
{"table":"contact","object":{"id":1,"email":"[email protected]","name":"Alice","isTrusted":false,"spamScore":0}}
23
{"table":"contact","object":{"id":2,"email":"[email protected]","name":"Bob","isTrusted":false,"spamScore":0}}
34
{"table":"contact","object":{"id":3,"email":"[email protected]","name":"Charlie","isTrusted":false,"spamScore":0}}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
recipientId: 'test'
3+
};

electron_app/src/dataTransferClient.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,17 @@ const checkDataTransferDirectory = () => {
2727
}
2828
};
2929

30-
const removeDataTransferDirectoryRecursive = () => {
31-
if (fs.existsSync(dataTransferDirectory)) {
32-
fs.readdirSync(dataTransferDirectory).forEach(file => {
33-
const currentPath = dataTransferDirectory + '/' + file;
30+
const removeDataTransferDirectoryRecursive = pathToDelete => {
31+
if (fs.existsSync(pathToDelete)) {
32+
fs.readdirSync(pathToDelete).forEach(file => {
33+
const currentPath = path.join(pathToDelete, file);
3434
if (fs.lstatSync(currentPath).isDirectory()) {
3535
removeDataTransferDirectoryRecursive(currentPath);
3636
} else {
3737
fs.unlinkSync(currentPath);
3838
}
3939
});
40-
fs.rmdirSync(dataTransferDirectory);
40+
fs.rmdirSync(pathToDelete);
4141
}
4242
};
4343

@@ -147,7 +147,7 @@ const exportDatabase = async () => {
147147
};
148148

149149
const clearSyncData = () => {
150-
return removeDataTransferDirectoryRecursive();
150+
return removeDataTransferDirectoryRecursive(dataTransferDirectory);
151151
};
152152

153153
module.exports = {

0 commit comments

Comments
 (0)