Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,206 @@
|
||||
// Generated, do not edit. sha256:82917722010d150a76bf3e84051a5132d98df9cd1713d95fe6238971925d39e8
|
||||
import { mkdirSync, readFileSync, writeFileSync, readdirSync, renameSync, statSync, rmSync } from 'node:fs';
|
||||
import { join, resolve, dirname, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { fail } from '../shared/app-error.js';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const POINTER_NAME = 'active.json';
|
||||
const LEGACY_POINTER_NAME = 'actief.json';
|
||||
const POINTER_FIELD = 'portfolio';
|
||||
const LEGACY_POINTER_FIELD = 'portefeuille';
|
||||
|
||||
export const RESERVED_DIRS = Object.freeze(['tls', 'Archive']);
|
||||
|
||||
const ARCHIVE_NAME = 'Archive';
|
||||
|
||||
export const DEFAULT_NAME = 'MyAssets';
|
||||
|
||||
export const MIGRATION_NAME = 'Test';
|
||||
|
||||
const PORTFOLIO_FILE_PREFIXES = ['db.sqlite', 'secrets.json'];
|
||||
|
||||
export function installDir() {
|
||||
return process.env.ASSETAPP_DATA_DIR || join(ROOT, 'data');
|
||||
}
|
||||
|
||||
export function pointerFile() {
|
||||
return join(installDir(), POINTER_NAME);
|
||||
}
|
||||
|
||||
export function archiveDir() {
|
||||
return join(installDir(), ARCHIVE_NAME);
|
||||
}
|
||||
|
||||
export function portfolioDir(name) {
|
||||
assertValidName(name);
|
||||
const basis = resolve(installDir());
|
||||
const doel = resolve(join(installDir(), name));
|
||||
if (!doel.startsWith(basis + sep)) {
|
||||
throw new Error(`Portfolio '${name}' would land outside the data directory.`);
|
||||
}
|
||||
return doel;
|
||||
}
|
||||
|
||||
const FORBIDDEN_CHARS = /[<>:"/\\|?*]/;
|
||||
|
||||
const RESERVED_NAMES = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
||||
|
||||
const MAX_LENGTH = 64;
|
||||
|
||||
const hasControlChar = (name) => [...name].some(teken => teken.codePointAt(0) < 0x20);
|
||||
|
||||
export function assertValidName(name) {
|
||||
if (typeof name !== 'string' || name.length === 0) {
|
||||
fail('portfolios.nameRequired', null, 'A portfolio needs a name.');
|
||||
}
|
||||
if (name.length > MAX_LENGTH) {
|
||||
fail('portfolios.nameTooLong', { max: MAX_LENGTH }, `A portfolio name is at most ${MAX_LENGTH} characters long.`);
|
||||
}
|
||||
if (name === '.' || name === '..') {
|
||||
fail('portfolios.nameIsDirRef', { naam: name }, `'${name}' is not a name but a reference to a directory.`);
|
||||
}
|
||||
if (FORBIDDEN_CHARS.test(name) || hasControlChar(name)) {
|
||||
fail('portfolios.nameForbiddenChars', null, 'A portfolio name may not contain any of these characters: < > : " / \\ | ? *');
|
||||
}
|
||||
if (/[ .]$/.test(name)) {
|
||||
fail('portfolios.nameTrailing', null, 'A portfolio name may not end in a space or a dot.');
|
||||
}
|
||||
if (/^ /.test(name)) {
|
||||
fail('portfolios.nameLeadingSpace', null, 'A portfolio name may not start with a space.');
|
||||
}
|
||||
if (RESERVED_NAMES.test(name.split('.')[0])) {
|
||||
fail('portfolios.nameReserved', { naam: name }, `'${name}' is a reserved name on Windows and cannot become a directory.`);
|
||||
}
|
||||
if (RESERVED_DIRS.some(gereserveerd => normalizeName(gereserveerd) === normalizeName(name))) {
|
||||
fail('portfolios.nameTaken', { naam: name }, `'${name}' is a name the app uses itself.`);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
export function normalizeName(name) {
|
||||
if (typeof name !== 'string') {
|
||||
throw new TypeError(`Een portefeuillenaam is tekst; gekregen: ${typeof name}.`);
|
||||
}
|
||||
return name.normalize('NFC').toLocaleLowerCase('nl');
|
||||
}
|
||||
|
||||
export function sameName(a, b) {
|
||||
if (a == null || b == null) return false;
|
||||
return normalizeName(a) === normalizeName(b);
|
||||
}
|
||||
|
||||
export function readActiveName() {
|
||||
let rauw;
|
||||
try { rauw = readFileSync(pointerFile(), 'utf-8'); }
|
||||
catch (e) {
|
||||
if (e.code !== 'ENOENT') throw e;
|
||||
try { rauw = readFileSync(join(installDir(), LEGACY_POINTER_NAME), 'utf-8'); }
|
||||
catch (e2) { if (e2.code === 'ENOENT') return null; throw e2; }
|
||||
}
|
||||
let data;
|
||||
try { data = JSON.parse(rauw); }
|
||||
catch { throw new Error(`${pointerFile()} is not valid JSON; repair or remove the file.`); }
|
||||
const naam = data && (data[POINTER_FIELD] ?? data[LEGACY_POINTER_FIELD]);
|
||||
if (typeof naam !== 'string' || naam.length === 0) {
|
||||
throw new Error(`${pointerFile()} names no portfolio (field '${POINTER_FIELD}').`);
|
||||
}
|
||||
return assertValidName(naam);
|
||||
}
|
||||
|
||||
export function listPortfolios() {
|
||||
let entries;
|
||||
try { entries = readdirSync(installDir(), { withFileTypes: true }); }
|
||||
catch (e) { if (e.code === 'ENOENT') return []; throw e; }
|
||||
const actief = readActiveName();
|
||||
return entries
|
||||
.filter(e => e.isDirectory())
|
||||
.map(e => e.name)
|
||||
.filter(naam => !RESERVED_DIRS.some(g => normalizeName(g) === normalizeName(naam)))
|
||||
.map(naam => {
|
||||
const map = join(installDir(), naam);
|
||||
const db = join(map, 'db.sqlite');
|
||||
return {
|
||||
naam,
|
||||
actief: actief !== null && sameName(naam, actief),
|
||||
heeftDatabase: bestaat(db),
|
||||
heeftGeheimen: bestaat(join(map, 'secrets.json')),
|
||||
laatstGebruikt: mtimeOf(db),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.naam.localeCompare(b.naam, 'nl'));
|
||||
}
|
||||
|
||||
export function writeActiveName(name) {
|
||||
assertValidName(name);
|
||||
mkdirSync(installDir(), { recursive: true });
|
||||
writeFileSync(pointerFile(), `${JSON.stringify({ [POINTER_FIELD]: name }, null, 2)}\n`, 'utf-8');
|
||||
return name;
|
||||
}
|
||||
|
||||
export function activeName() {
|
||||
return readActiveName() || DEFAULT_NAME;
|
||||
}
|
||||
|
||||
export function activeDir() {
|
||||
return portfolioDir(activeName());
|
||||
}
|
||||
|
||||
const bestaat = (pad) => { try { statSync(pad); return true; } catch { return false; } };
|
||||
|
||||
const mtimeOf = (pad) => { try { return statSync(pad).mtime.toISOString(); } catch { return null; } };
|
||||
|
||||
function looseFiles() {
|
||||
let entries;
|
||||
try { entries = readdirSync(installDir(), { withFileTypes: true }); }
|
||||
catch (e) { if (e.code === 'ENOENT') return []; throw e; }
|
||||
return entries
|
||||
.filter(e => e.isFile())
|
||||
.map(e => e.name)
|
||||
.filter(naam => PORTFOLIO_FILE_PREFIXES.some(prefix => naam.startsWith(prefix)));
|
||||
}
|
||||
|
||||
export function migrateLooseLayout() {
|
||||
const losse = looseFiles();
|
||||
if (!losse.length) return null;
|
||||
|
||||
const doelMap = portfolioDir(MIGRATION_NAME);
|
||||
if (bestaat(join(doelMap, 'db.sqlite'))) {
|
||||
const losseNamen = losse.join(', ');
|
||||
throw new Error(
|
||||
`There are loose database files in ${installDir()} (${losseNamen}) and there is already a database `
|
||||
+ `in ${doelMap}. The app does not guess which one is the right one.\n`
|
||||
+ 'Almost always a process from before this layout is still running: an MCP server or an old app '
|
||||
+ 'server then recreates the loose file every time. Close those processes (and restart your AI '
|
||||
+ 'client), check which file is the largest, and remove or rename the loose one so it no longer '
|
||||
+ 'starts with "db.sqlite" or "secrets.json".');
|
||||
}
|
||||
mkdirSync(doelMap, { recursive: true });
|
||||
for (const naam of losse) renameSync(join(installDir(), naam), join(doelMap, naam));
|
||||
return MIGRATION_NAME;
|
||||
}
|
||||
|
||||
function hernoemOudeWijzer() {
|
||||
const oud = join(installDir(), LEGACY_POINTER_NAME);
|
||||
if (!bestaat(oud) || bestaat(pointerFile())) return;
|
||||
const naam = readActiveName();
|
||||
if (naam) writeActiveName(naam);
|
||||
rmSync(oud, { force: true });
|
||||
}
|
||||
|
||||
export function prepareLayout() {
|
||||
mkdirSync(installDir(), { recursive: true });
|
||||
hernoemOudeWijzer();
|
||||
const migrated = migrateLooseLayout();
|
||||
let naam = readActiveName();
|
||||
let herkomst = `uit ${POINTER_NAME}`;
|
||||
if (!naam) {
|
||||
naam = writeActiveName(migrated || DEFAULT_NAME);
|
||||
herkomst = migrated ? `losse bestanden verhuisd naar '${migrated}'` : 'nieuw aangemaakt';
|
||||
} else if (migrated) {
|
||||
herkomst = `uit ${POINTER_NAME}; losse bestanden verhuisd naar '${migrated}'`;
|
||||
}
|
||||
mkdirSync(portfolioDir(naam), { recursive: true });
|
||||
return { name: naam, migrated, regel: `portefeuille: ${naam} (${herkomst}) | map: ${portfolioDir(naam)}` };
|
||||
}
|
||||
Reference in new issue
Block a user