mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 18:40:34 -05:00
feat(api): enhance build scripts and add file system utilities
- Update build scripts in package.json to include separate commands for building node, migrations, and default database. - Introduce a new file system utility module in loot-core to handle file operations such as reading, writing, and directory management. - Implement error handling and logging for file operations to improve robustness.
This commit is contained in:
@@ -11,7 +11,10 @@
|
||||
"types": "@types/api/index.d.ts",
|
||||
"scripts": {
|
||||
"build:crdt": "yarn workspace @actual-app/crdt build",
|
||||
"build": "yarn run clean && vite build && tsc -p tsconfig.dist.json --emitDeclarationOnly",
|
||||
"build:node": "vite build && tsc -p tsconfig.dist.json --emitDeclarationOnly",
|
||||
"build:migrations": "mkdir dist/migrations && cp ../loot-core/migrations/*.sql dist/migrations",
|
||||
"build:default-db": "cp ../loot-core/default-db.sqlite dist/",
|
||||
"build": "yarn run clean && yarn run build:node && yarn run build:migrations && yarn run build:default-db",
|
||||
"test": "yarn run clean && yarn run build:crdt && vitest --run",
|
||||
"clean": "rm -rf dist @types"
|
||||
},
|
||||
|
||||
190
packages/loot-core/src/platform/server/fs/index.api.ts
Normal file
190
packages/loot-core/src/platform/server/fs/index.api.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
// @ts-strict-ignore
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import promiseRetry from 'promise-retry';
|
||||
|
||||
import { logger } from '../log';
|
||||
|
||||
import type * as T from '.';
|
||||
|
||||
export { getDocumentDir, getBudgetDir, _setDocumentDir } from './shared';
|
||||
|
||||
const rootPath = __dirname;
|
||||
|
||||
export const init = () => {
|
||||
// Nothing to do
|
||||
};
|
||||
|
||||
export const getDataDir = () => {
|
||||
if (!process.env.ACTUAL_DATA_DIR) {
|
||||
throw new Error('ACTUAL_DATA_DIR env variable is required');
|
||||
}
|
||||
return process.env.ACTUAL_DATA_DIR;
|
||||
};
|
||||
|
||||
export const bundledDatabasePath = path.join(rootPath, 'default-db.sqlite');
|
||||
|
||||
export const migrationsPath = path.join(rootPath, 'migrations');
|
||||
|
||||
export const demoBudgetPath = path.join(rootPath, 'demo-budget');
|
||||
|
||||
export const join = path.join;
|
||||
|
||||
export const basename = filepath => path.basename(filepath);
|
||||
|
||||
export const listDir: T.ListDir = filepath =>
|
||||
new Promise((resolve, reject) => {
|
||||
fs.readdir(filepath, (err, files) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(files);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export const exists = filepath =>
|
||||
new Promise(resolve => {
|
||||
fs.access(filepath, fs.constants.F_OK, err => {
|
||||
return resolve(!err);
|
||||
});
|
||||
});
|
||||
|
||||
export const mkdir = filepath =>
|
||||
new Promise((resolve, reject) => {
|
||||
fs.mkdir(filepath, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export const size = filepath =>
|
||||
new Promise((resolve, reject) => {
|
||||
fs.stat(filepath, (err, stats) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(stats.size);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export const copyFile: T.CopyFile = (frompath, topath) => {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const readStream = fs.createReadStream(frompath);
|
||||
const writeStream = fs.createWriteStream(topath);
|
||||
|
||||
readStream.on('error', reject);
|
||||
writeStream.on('error', reject);
|
||||
|
||||
writeStream.on('open', () => readStream.pipe(writeStream));
|
||||
writeStream.once('close', () => resolve(true));
|
||||
});
|
||||
};
|
||||
|
||||
export const readFile: T.ReadFile = (
|
||||
filepath: string,
|
||||
encoding: 'utf8' | 'binary' | null = 'utf8',
|
||||
) => {
|
||||
if (encoding === 'binary') {
|
||||
// `binary` is not actually a valid encoding, you pass `null` into node if
|
||||
// you want a buffer
|
||||
encoding = null;
|
||||
}
|
||||
// `any` as cannot refine return with two function overrides
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
fs.readFile(filepath, encoding, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const writeFile: T.WriteFile = async (filepath, contents) => {
|
||||
try {
|
||||
await promiseRetry(
|
||||
(retry, attempt) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// @ts-expect-error contents type needs refining
|
||||
fs.writeFile(filepath, contents, 'utf8', err => {
|
||||
if (err) {
|
||||
logger.error(
|
||||
`Failed to write to ${filepath}. Attempted ${attempt} times. Something is locking the file - potentially a virus scanner or backup software.`,
|
||||
);
|
||||
reject(err);
|
||||
} else {
|
||||
if (attempt > 1) {
|
||||
logger.info(
|
||||
`Successfully recovered from file lock. It took ${attempt} retries`,
|
||||
);
|
||||
}
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
}).catch(retry);
|
||||
},
|
||||
{
|
||||
retries: 20,
|
||||
minTimeout: 100,
|
||||
maxTimeout: 500,
|
||||
factor: 1.5,
|
||||
},
|
||||
);
|
||||
|
||||
return undefined;
|
||||
} catch (err) {
|
||||
logger.error(`Unable to recover from file lock on file ${filepath}`);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeFile = filepath => {
|
||||
return new Promise(function (resolve, reject) {
|
||||
fs.unlink(filepath, err => {
|
||||
return err ? reject(err) : resolve(undefined);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const removeDir = dirpath => {
|
||||
return new Promise(function (resolve, reject) {
|
||||
fs.rmdir(dirpath, err => {
|
||||
return err ? reject(err) : resolve(undefined);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const removeDirRecursively = async dirpath => {
|
||||
if (await exists(dirpath)) {
|
||||
for (const file of await listDir(dirpath)) {
|
||||
const fullpath = join(dirpath, file);
|
||||
if (fs.statSync(fullpath).isDirectory()) {
|
||||
await removeDirRecursively(fullpath);
|
||||
} else {
|
||||
await removeFile(fullpath);
|
||||
}
|
||||
}
|
||||
|
||||
await removeDir(dirpath);
|
||||
}
|
||||
};
|
||||
|
||||
export const getModifiedTime = filepath => {
|
||||
return new Promise(function (resolve, reject) {
|
||||
fs.stat(filepath, (err, stats) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(new Date(stats.mtime));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user