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:
Matiss Janis Aboltins
2026-02-03 09:42:19 +00:00
parent 6f9dab9aab
commit 4ff3b35168
2 changed files with 194 additions and 1 deletions

View File

@@ -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"
},

View 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));
}
});
});
};