2
0
mirror of https://github.com/frappe/books.git synced 2024-11-08 14:50:56 +00:00

fix: create backup on migrate or patch

This commit is contained in:
18alantom 2023-07-12 14:58:14 +05:30
parent 23916a3b1d
commit 823739f564
7 changed files with 180 additions and 68 deletions

View File

@ -1,4 +1,4 @@
import fs from 'fs/promises'; import fs from 'fs-extra';
import { DatabaseError } from 'fyo/utils/errors'; import { DatabaseError } from 'fyo/utils/errors';
import path from 'path'; import path from 'path';
import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types'; import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types';
@ -9,6 +9,9 @@ import { BespokeQueries } from './bespoke';
import DatabaseCore from './core'; import DatabaseCore from './core';
import { runPatches } from './runPatch'; import { runPatches } from './runPatch';
import { BespokeFunction, Patch } from './types'; import { BespokeFunction, Patch } from './types';
import BetterSQLite3 from 'better-sqlite3';
import { getMapFromList } from 'utils/index';
import { Version } from 'utils/version';
export class DatabaseManager extends DatabaseDemuxBase { export class DatabaseManager extends DatabaseDemuxBase {
db?: DatabaseCore; db?: DatabaseCore;
@ -50,25 +53,12 @@ export class DatabaseManager extends DatabaseDemuxBase {
return; return;
} }
const isFirstRun = await this.#getIsFirstRun(); const isFirstRun = this.#getIsFirstRun();
if (isFirstRun) { if (isFirstRun) {
await this.db!.migrate(); await this.db!.migrate();
} }
/** await this.#executeMigration();
* This needs to be supplimented with transactions
* TODO: Add transactions in core.ts
*/
const dbPath = this.db!.dbPath;
const copyPath = await this.#makeTempCopy();
try {
await this.#runPatchesAndMigrate();
} catch (error) {
await this.#handleFailedMigration(error, dbPath, copyPath);
} finally {
await unlinkIfExists(copyPath);
}
} }
async #handleFailedMigration( async #handleFailedMigration(
@ -89,32 +79,67 @@ export class DatabaseManager extends DatabaseDemuxBase {
throw error; throw error;
} }
async #runPatchesAndMigrate() { async #executeMigration() {
const patchesToExecute = await this.#getPatchesToExecute(); const version = this.#getAppVersion();
const patches = await this.#getPatchesToExecute(version);
patchesToExecute.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); const hasPatches = !!patches.pre.length || !!patches.post.length;
const preMigrationPatches = patchesToExecute.filter( if (hasPatches) {
(p) => p.patch.beforeMigrate await this.#createBackup();
);
const postMigrationPatches = patchesToExecute.filter(
(p) => !p.patch.beforeMigrate
);
await runPatches(preMigrationPatches, this);
await this.db!.migrate();
await runPatches(postMigrationPatches, this);
} }
async #getPatchesToExecute(): Promise<Patch[]> { await runPatches(patches.pre, this, version);
await this.db!.migrate({
pre: async () => {
if (hasPatches) {
return;
}
await this.#createBackup();
},
});
await runPatches(patches.post, this, version);
}
async #getPatchesToExecute(
version: string
): Promise<{ pre: Patch[]; post: Patch[] }> {
if (this.db === undefined) { if (this.db === undefined) {
return []; return { pre: [], post: [] };
} }
const query: { name: string }[] = await this.db.knex!('PatchRun').select( const query = (await this.db.knex!('PatchRun').select()) as {
'name' name: string;
); version?: string;
const executedPatches = query.map((q) => q.name); failed?: boolean;
return patches.filter((p) => !executedPatches.includes(p.name)); }[];
const runPatchesMap = getMapFromList(query, 'name');
/**
* A patch is run only if:
* - it hasn't run and was added in a future version
* i.e. app version is before patch added version
* - it ran but failed in some other version (i.e fixed)
*/
const filtered = patches
.filter((p) => {
const exec = runPatchesMap[p.name];
if (!exec && Version.lte(version, p.version)) {
return true;
}
if (exec?.failed && exec?.version !== version) {
return true;
}
return false;
})
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
return {
pre: filtered.filter((p) => p.patch.beforeMigrate),
post: filtered.filter((p) => !p.patch.beforeMigrate),
};
} }
async call(method: DatabaseMethod, ...args: unknown[]) { async call(method: DatabaseMethod, ...args: unknown[]) {
@ -149,33 +174,86 @@ export class DatabaseManager extends DatabaseDemuxBase {
return await queryFunction(this.db!, ...args); return await queryFunction(this.db!, ...args);
} }
async #getIsFirstRun(): Promise<boolean> { #getIsFirstRun(): boolean {
if (!this.#isInitialized) { const db = this.getDriver();
if (!db) {
return true; return true;
} }
const tableList: unknown[] = await this.db!.knex!.raw( const noPatchRun =
"SELECT name FROM sqlite_master WHERE type='table'" db
); .prepare(
return tableList.length === 0; `select name from sqlite_master
where
type = 'table' and
name = 'PatchRun'`
)
.all().length === 0;
db.close();
return noPatchRun;
} }
async #makeTempCopy() { async #createBackup() {
const src = this.db!.dbPath; const { dbPath } = this.db ?? {};
if (src === ':memory:') { if (!dbPath) {
return;
}
const backupPath = this.#getBackupFilePath();
if (!backupPath) {
return;
}
const db = this.getDriver();
await db?.backup(backupPath);
db?.close();
}
#getBackupFilePath() {
const { dbPath } = this.db ?? {};
if (dbPath === ':memory:' || !dbPath) {
return null; return null;
} }
const dir = path.parse(src).dir; let fileName = path.parse(dbPath).name;
const dest = path.join(dir, '__premigratory_temp.db'); if (fileName.endsWith('.books')) {
fileName = fileName.slice(0, -6);
}
try { const backupFolder = path.join(path.dirname(dbPath), 'backups');
await fs.copyFile(src, dest); const date = new Date().toISOString().split('.')[0];
} catch (err) { const version = this.#getAppVersion();
const backupFile = `${fileName}-${version}-${date}.books.db`;
fs.ensureDirSync(backupFolder);
return path.join(backupFolder, backupFile);
}
#getAppVersion() {
const db = this.getDriver();
if (!db) {
return '0.0.0';
}
const query = db
.prepare(
`select value from SingleValue
where
fieldname = 'version' and
parent = 'SystemSettings'`
)
.get() as undefined | { value: string };
db.close();
return query?.value || '0.0.0';
}
getDriver() {
const { dbPath } = this.db ?? {};
if (!dbPath) {
return null; return null;
} }
return dest; return BetterSQLite3(dbPath, { readonly: true });
} }
} }

View File

@ -2,34 +2,50 @@ import { emitMainProcessError, getDefaultMetaFieldValueMap } from '../helpers';
import { DatabaseManager } from './manager'; import { DatabaseManager } from './manager';
import { FieldValueMap, Patch } from './types'; import { FieldValueMap, Patch } from './types';
export async function runPatches(patches: Patch[], dm: DatabaseManager) { export async function runPatches(
patches: Patch[],
dm: DatabaseManager,
version: string
) {
const list: { name: string; success: boolean }[] = []; const list: { name: string; success: boolean }[] = [];
for (const patch of patches) { for (const patch of patches) {
const success = await runPatch(patch, dm); const success = await runPatch(patch, dm, version);
list.push({ name: patch.name, success }); list.push({ name: patch.name, success });
} }
return list; return list;
} }
async function runPatch(patch: Patch, dm: DatabaseManager): Promise<boolean> { async function runPatch(
patch: Patch,
dm: DatabaseManager,
version: string
): Promise<boolean> {
let failed = false;
try { try {
await patch.patch.execute(dm); await patch.patch.execute(dm);
} catch (error) { } catch (error) {
if (!(error instanceof Error)) { failed = true;
return false; if (error instanceof Error) {
}
error.message = `Patch Failed: ${patch.name}\n${error.message}`; error.message = `Patch Failed: ${patch.name}\n${error.message}`;
emitMainProcessError(error, { patchName: patch.name, notifyUser: false }); emitMainProcessError(error, { patchName: patch.name, notifyUser: false });
return false; }
} }
await makeEntry(patch.name, dm); await makeEntry(patch.name, version, failed, dm);
return true; return true;
} }
async function makeEntry(patchName: string, dm: DatabaseManager) { async function makeEntry(
patchName: string,
version: string,
failed: boolean,
dm: DatabaseManager
) {
const defaultFieldValueMap = getDefaultMetaFieldValueMap() as FieldValueMap; const defaultFieldValueMap = getDefaultMetaFieldValueMap() as FieldValueMap;
defaultFieldValueMap.name = patchName; defaultFieldValueMap.name = patchName;
defaultFieldValueMap.failed = failed;
defaultFieldValueMap.version = version;
await dm.db!.insert('PatchRun', defaultFieldValueMap); await dm.db!.insert('PatchRun', defaultFieldValueMap);
} }

View File

@ -43,6 +43,7 @@
"@codemirror/view": "^6.0.0", "@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0", "@lezer/common": "^1.0.0",
"@types/assert": "^1.5.6", "@types/assert": "^1.5.6",
"@types/better-sqlite3": "^7.6.4",
"@types/electron-devtools-installer": "^2.2.0", "@types/electron-devtools-installer": "^2.2.0",
"@types/lodash": "^4.14.179", "@types/lodash": "^4.14.179",
"@types/luxon": "^2.3.1", "@types/luxon": "^2.3.1",

View File

@ -8,6 +8,18 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Name", "label": "Name",
"required": true "required": true
},
{
"fieldname": "failed",
"fieldtype": "Check",
"label": "Failed",
"default": false
},
{
"fieldname": "version",
"fieldtype": "Data",
"label": "Version",
"default": "0.0.0"
} }
] ]
} }

View File

@ -60,6 +60,7 @@
"fieldname": "version", "fieldname": "version",
"label": "Version", "label": "Version",
"fieldtype": "Data", "fieldtype": "Data",
"default": "0.0.0",
"readOnly": true, "readOnly": true,
"section": "Default" "section": "Default"
}, },

View File

@ -94,9 +94,6 @@ export default defineComponent({
sections: getGetStartedConfig(), sections: getGetStartedConfig(),
}; };
}, },
mounted() {
// this.sections = getGetStartedConfig();
},
async activated() { async activated() {
await fyo.doc.getDoc('GetStarted'); await fyo.doc.getDoc('GetStarted');
await this.checkForCompletedTasks(); await this.checkForCompletedTasks();

View File

@ -585,6 +585,13 @@
resolved "https://registry.yarnpkg.com/@types/assert/-/assert-1.5.6.tgz#a8b5a94ce5fb8f4ba65fdc37fc9507609114189e" resolved "https://registry.yarnpkg.com/@types/assert/-/assert-1.5.6.tgz#a8b5a94ce5fb8f4ba65fdc37fc9507609114189e"
integrity sha512-Y7gDJiIqb9qKUHfBQYOWGngUpLORtirAVPuj/CWJrU2C6ZM4/y3XLwuwfGMF8s7QzW746LQZx23m0+1FSgjfug== integrity sha512-Y7gDJiIqb9qKUHfBQYOWGngUpLORtirAVPuj/CWJrU2C6ZM4/y3XLwuwfGMF8s7QzW746LQZx23m0+1FSgjfug==
"@types/better-sqlite3@^7.6.4":
version "7.6.4"
resolved "https://registry.yarnpkg.com/@types/better-sqlite3/-/better-sqlite3-7.6.4.tgz#102462611e67aadf950d3ccca10292de91e6f35b"
integrity sha512-dzrRZCYPXIXfSR1/surNbJ/grU3scTaygS0OMzjlGf71i9sc2fGyHPXXiXmEvNIoE0cGwsanEFMVJxPXmco9Eg==
dependencies:
"@types/node" "*"
"@types/cacheable-request@^6.0.1": "@types/cacheable-request@^6.0.1":
version "6.0.2" version "6.0.2"
resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9" resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9"