// 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(), }; }