Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,165 @@
|
||||
// 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(),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user