This commit is contained in:
seb
2026-10-05 11:14:31 +02:00
parent 9831bea110
commit 25266cfcec
12 changed files with 1991 additions and 58 deletions

View File

@@ -17,6 +17,8 @@ function getMimeType(filePath) {
case '.jpeg': return 'image/jpeg';
case '.txt': return 'text/plain';
case '.csv': return 'text/csv';
case '.md': return 'text/markdown';
case '.skill': return 'text/markdown';
default: return 'application/octet-stream';
}
}
@@ -56,17 +58,56 @@ function assertNotReadOnlyDatabasePath(filePath) {
}
}
/**
* Resolve the on-disk VFS root for the user's CURRENTLY SELECTED company.
*
* This must stay in lockstep with the store, which scopes every `modelvfs` call by
* `google_auth.selected_company` read at call time — see `resolveSelectedCompanyAccess` in
* libdashstore. Callers therefore must NOT pass a `companyId`: a value captured earlier (at
* tool-construction time, or from a request made before a company switch) can disagree with
* what the store will act on, which would hydrate one company's directory from another
* company's rows and delete files from a directory the database never touched.
*
* One authority, read fresh on every call, for both the disk and the database halves.
*
* @param {number} googleAuthId
* @returns {Promise<string>} Absolute `data/vfs/<companyId>` path
* @throws {Error} 'No company selected' when the user has no company selected
*/
export async function resolveVfsRoot(googleAuthId) {
return vfsRootFor(await getStore(), googleAuthId);
}
/**
* Shared implementation of {@link resolveVfsRoot}. Takes an already-resolved store so callers
* that need it anyway (e.g. `prepareVfs`, which hydrates from the same store) do not pay for a
* second lookup.
*
* @param {object} store
* @param {number} googleAuthId
* @returns {Promise<string>}
*/
async function vfsRootFor(store, googleAuthId) {
const row = await store.google_auth.getById(googleAuthId);
const companyId = row?.selectedCompanyId;
if (companyId == null) {
throw new Error('No company selected');
}
return path.resolve(process.cwd(), 'data/vfs', String(companyId));
}
/**
* Ensure the local directory for this company's VFS exists and is hydrated from modelvfs.
* @param {{ companyId: number, googleAuthId: number }} ctx
*
* @param {{ googleAuthId: number }} ctx
* @returns {Promise<string>} vfsRoot directory path
*/
export async function prepareVfs({ companyId, googleAuthId }) {
const vfsRoot = path.resolve(process.cwd(), 'data/vfs', String(companyId));
export async function prepareVfs({ googleAuthId }) {
const store = await getStore();
const vfsRoot = await vfsRootFor(store, googleAuthId);
fs.mkdirSync(vfsRoot, { recursive: true });
try {
const store = await getStore();
const files = await store.modelvfs.list(googleAuthId);
for (const file of files) {
const diskPath = path.resolve(vfsRoot, file.name);
@@ -88,7 +129,11 @@ export async function prepareVfs({ companyId, googleAuthId }) {
/**
* Syncs any modified or newly written files from the disk vfsRoot back to the modelvfs table.
* @param {{ companyId: number, googleAuthId: number, vfsRoot: string }} ctx
*
* The store re-scopes by the currently selected company on every call, so this only stays
* correct while `vfsRoot` was produced by `prepareVfs`/`resolveVfsRoot` in the same turn.
*
* @param {{ googleAuthId: number, vfsRoot: string }} ctx
*/
export async function syncVfsToDb({ googleAuthId, vfsRoot }) {
if (!fs.existsSync(vfsRoot)) return;
@@ -128,8 +173,8 @@ export async function syncVfsToDb({ googleAuthId, vfsRoot }) {
/**
* Creates a sandboxed VFS module object mirroring Node's `fs` and `fs/promises`.
* @param {string} vfsRoot
* @param {{ googleAuthId: number, companyId: number }} [options]
* @param {string} vfsRoot Absolute root, as returned by `prepareVfs`/`resolveVfsRoot`
* @param {{ googleAuthId: number }} [options]
*/
export function createVfs(vfsRoot, options = {}) {
const { googleAuthId } = options;