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:
parent
23916a3b1d
commit
823739f564
@ -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[]> {
|
|
||||||
if (this.db === undefined) {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const query: { name: string }[] = await this.db.knex!('PatchRun').select(
|
await runPatches(patches.pre, this, version);
|
||||||
'name'
|
await this.db!.migrate({
|
||||||
);
|
pre: async () => {
|
||||||
const executedPatches = query.map((q) => q.name);
|
if (hasPatches) {
|
||||||
return patches.filter((p) => !executedPatches.includes(p.name));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.#createBackup();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await runPatches(patches.post, this, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #getPatchesToExecute(
|
||||||
|
version: string
|
||||||
|
): Promise<{ pre: Patch[]; post: Patch[] }> {
|
||||||
|
if (this.db === undefined) {
|
||||||
|
return { pre: [], post: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = (await this.db.knex!('PatchRun').select()) as {
|
||||||
|
name: string;
|
||||||
|
version?: string;
|
||||||
|
failed?: boolean;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}`;
|
||||||
|
emitMainProcessError(error, { patchName: patch.name, notifyUser: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
error.message = `Patch Failed: ${patch.name}\n${error.message}`;
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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();
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user