166 lines
6.1 KiB
JavaScript
166 lines
6.1 KiB
JavaScript
// Generated, do not edit. sha256:1c5123afde8b42e8b0c7944a90a9a3952b38ed44748eb715f09bb8aa35f593d5
|
|
import { mkdir, readdir, rename, rm, readFile, writeFile, copyFile, stat } from 'node:fs/promises';
|
|
import { join, sep, resolve } from 'node:path';
|
|
|
|
import {
|
|
installDir, portfolioDir, archiveDir, assertValidName, sameName,
|
|
activeName, writeActiveName, listPortfolios,
|
|
} from './portfolios.mjs';
|
|
import { run } from './db/conn.mjs';
|
|
import { createDatabase } from './db.js';
|
|
import { CONFIG } from '../shared/config.js';
|
|
import { fail } from '../shared/app-error.js';
|
|
import { capacityReached } from './license.mjs';
|
|
|
|
const BUREN = ['-wal', '-shm'];
|
|
|
|
const HANDLE_SLEUTEL = 'ai_handle_secret';
|
|
|
|
const bestaat = async (pad) => { try { await stat(pad); return true; } catch { return false; } };
|
|
|
|
function eisNietActief(naam, sleutel) {
|
|
if (sameName(naam, activeName())) {
|
|
fail(sleutel, { naam },
|
|
`'${naam}' is the portfolio that is open now; switch to another one first.`);
|
|
}
|
|
}
|
|
|
|
function eisBestaat(naam) {
|
|
const gevonden = listPortfolios().find(p => sameName(p.naam, naam));
|
|
if (!gevonden) fail('portfolios.unknownPortfolio', { naam }, `There is no portfolio '${naam}'.`);
|
|
return gevonden;
|
|
}
|
|
|
|
function eisVrijeNaam(naam) {
|
|
assertValidName(naam);
|
|
const botsing = listPortfolios().find(p => sameName(p.naam, naam));
|
|
if (botsing) {
|
|
fail('portfolios.nameInUse', { naam: botsing.naam },
|
|
`A portfolio '${botsing.naam}' already exists; names that differ only in case count as the same.`);
|
|
}
|
|
return naam;
|
|
}
|
|
|
|
export async function createPortfolio(naam, { kopieVan = null, baseCurrency = null } = {}) {
|
|
if (kopieVan && baseCurrency) {
|
|
throw new Error('A copy keeps the unit of account of its source; do not pass baseCurrency with kopieVan.');
|
|
}
|
|
const max = capacityReached('portfolios', listPortfolios().length);
|
|
if (max !== null) {
|
|
fail('portfolios.maxFree', { max },
|
|
`The free version holds ${max} portfolio(s). A licence lifts that; nothing is removed in the meantime.`);
|
|
}
|
|
eisVrijeNaam(naam);
|
|
const bron = kopieVan ? eisBestaat(kopieVan) : null;
|
|
const doel = portfolioDir(naam);
|
|
await mkdir(doel, { recursive: true });
|
|
|
|
try {
|
|
if (!bron) {
|
|
createDatabase(join(doel, 'db.sqlite'), { baseCurrency: baseCurrency ?? CONFIG.PORTFOLIO.DEFAULT_BASE_CURRENCY });
|
|
return { naam, gekopieerdVan: null, database: true, geheimen: false };
|
|
}
|
|
const database = await kopieerDatabase(bron.naam, doel);
|
|
const geheimen = await kopieerGeheimen(bron.naam, doel);
|
|
return { naam, gekopieerdVan: bron.naam, database, geheimen };
|
|
} catch (e) {
|
|
await rm(doel, { recursive: true, force: true });
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function kopieerDatabase(bronNaam, doelMap) {
|
|
const doel = join(doelMap, 'db.sqlite');
|
|
if (sameName(bronNaam, activeName())) {
|
|
run(`VACUUM INTO '${doel.replace(/'/g, "''")}'`, []);
|
|
return true;
|
|
}
|
|
const bron = join(portfolioDir(bronNaam), 'db.sqlite');
|
|
if (!await bestaat(bron)) return false;
|
|
await copyFile(bron, doel);
|
|
for (const b of BUREN) if (await bestaat(bron + b)) await copyFile(bron + b, doel + b);
|
|
return true;
|
|
}
|
|
|
|
async function kopieerGeheimen(bronNaam, doelMap) {
|
|
const bron = join(portfolioDir(bronNaam), 'secrets.json');
|
|
if (!await bestaat(bron)) return false;
|
|
const store = JSON.parse(await readFile(bron, 'utf-8'));
|
|
delete store[HANDLE_SLEUTEL];
|
|
await writeFile(join(doelMap, 'secrets.json'), JSON.stringify(store, null, 2), { mode: 0o600 });
|
|
return true;
|
|
}
|
|
|
|
export function activatePortfolio(naam) {
|
|
const doel = eisBestaat(naam);
|
|
if (sameName(doel.naam, activeName())) {
|
|
return { naam: doel.naam, gewisseld: false, supervised: isSupervised() };
|
|
}
|
|
writeActiveName(doel.naam);
|
|
return { naam: doel.naam, gewisseld: true, supervised: isSupervised() };
|
|
}
|
|
|
|
export const isSupervised = () => process.env.ASSETAPP_SUPERVISED === '1';
|
|
|
|
export async function renamePortfolio(vanNaam, naarNaam) {
|
|
const bron = eisBestaat(vanNaam);
|
|
eisNietActief(bron.naam, 'portfolios.activeCannotRename');
|
|
if (!sameName(bron.naam, naarNaam)) eisVrijeNaam(naarNaam);
|
|
else assertValidName(naarNaam);
|
|
await rename(portfolioDir(bron.naam), portfolioDir(naarNaam));
|
|
return { van: bron.naam, naar: naarNaam };
|
|
}
|
|
|
|
export async function archivePortfolio(naam) {
|
|
const bron = eisBestaat(naam);
|
|
eisNietActief(bron.naam, 'portfolios.activeCannotArchive');
|
|
await mkdir(archiveDir(), { recursive: true });
|
|
const doelNaam = await vrijeArchiefNaam(bron.naam);
|
|
await rename(portfolioDir(bron.naam), join(archiveDir(), doelNaam));
|
|
return { naam: bron.naam, archiefNaam: doelNaam, map: join(archiveDir(), doelNaam) };
|
|
}
|
|
|
|
async function vrijeArchiefNaam(naam) {
|
|
if (!await bestaat(join(archiveDir(), naam))) return naam;
|
|
for (let n = 2; n < 1000; n++) {
|
|
const kandidaat = `${naam} (${n})`;
|
|
if (!await bestaat(join(archiveDir(), kandidaat))) return kandidaat;
|
|
}
|
|
fail('portfolios.archiveFull', { naam }, `The archive is full of directories named '${naam}'.`);
|
|
}
|
|
|
|
export async function listArchived() {
|
|
let entries;
|
|
try { entries = await readdir(archiveDir(), { withFileTypes: true }); }
|
|
catch (e) { if (e.code === 'ENOENT') return []; throw e; }
|
|
const uit = [];
|
|
for (const e of entries) {
|
|
if (!e.isDirectory()) continue;
|
|
const map = join(archiveDir(), e.name);
|
|
let gearchiveerd = null;
|
|
try { gearchiveerd = (await stat(map)).mtime.toISOString(); } catch { }
|
|
uit.push({ naam: e.name, map, gearchiveerd });
|
|
}
|
|
return uit.sort((a, b) => String(b.gearchiveerd).localeCompare(String(a.gearchiveerd)));
|
|
}
|
|
|
|
export async function purgeArchived(naam) {
|
|
assertValidName(naam.replace(/ \(\d+\)$/, ''));
|
|
const map = resolve(join(archiveDir(), naam));
|
|
if (!map.startsWith(resolve(archiveDir()) + sep)) {
|
|
throw new Error(`'${naam}' points outside the archive.`);
|
|
}
|
|
if (!await bestaat(map)) fail('portfolios.notInArchive', { naam }, `There is no '${naam}' in the archive.`);
|
|
await rm(map, { recursive: true, force: true });
|
|
return { naam, map };
|
|
}
|
|
|
|
export async function overviewPortfolios() {
|
|
return {
|
|
actief: activeName(),
|
|
installatie: installDir(),
|
|
portefeuilles: listPortfolios(),
|
|
archief: await listArchived(),
|
|
};
|
|
}
|