init
This commit is contained in:
125
src/business-scan.ts
Normal file
125
src/business-scan.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { readdir, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { config } from './config.js';
|
||||
import { query } from './db.js';
|
||||
|
||||
export type BusinessStatus = 'ACTIVE' | 'SOLD' | 'INACTIVE';
|
||||
|
||||
export interface ScanResult {
|
||||
scanned: number;
|
||||
inserted: number;
|
||||
updated: number;
|
||||
missing: number;
|
||||
}
|
||||
|
||||
interface BusinessRow {
|
||||
id: string;
|
||||
name: string;
|
||||
nas_path: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** The three status directories directly below NAS_ROOT. */
|
||||
function statusDirs(): { status: BusinessStatus; dir: string }[] {
|
||||
return [
|
||||
{ status: 'ACTIVE', dir: config.nasDirActive },
|
||||
{ status: 'SOLD', dir: config.nasDirSold },
|
||||
{ status: 'INACTIVE', dir: config.nasDirInactive },
|
||||
];
|
||||
}
|
||||
|
||||
interface FoundBusiness {
|
||||
name: string;
|
||||
nasPath: string;
|
||||
status: BusinessStatus;
|
||||
}
|
||||
|
||||
/** Reads the three status directories; every immediate subdirectory is one business. */
|
||||
async function readFromDisk(): Promise<FoundBusiness[]> {
|
||||
const found: FoundBusiness[] = [];
|
||||
for (const { status, dir } of statusDirs()) {
|
||||
const full = path.join(config.nasRoot, dir);
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(full, { withFileTypes: true });
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`NAS directory not found or not readable: ${full} (${(err as Error).message})`,
|
||||
);
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
found.push({ name: entry.name, nasPath: path.join(full, entry.name), status });
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the NAS and upserts the businesses. Idempotent: existing rows are matched
|
||||
* by name and only touched when nas_path or status actually changed. Rows that no
|
||||
* longer exist on disk are kept and only reported via the logger.
|
||||
*/
|
||||
export async function scanBusinesses(
|
||||
log: { warn: (msg: string) => void } = console,
|
||||
): Promise<ScanResult> {
|
||||
const found = await readFromDisk();
|
||||
|
||||
const existing = await query<BusinessRow>('SELECT id, name, nas_path, status FROM business');
|
||||
const byName = new Map(existing.map((row) => [row.name, row]));
|
||||
|
||||
let inserted = 0;
|
||||
let updated = 0;
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const business of found) {
|
||||
seen.add(business.name);
|
||||
const row = byName.get(business.name);
|
||||
if (!row) {
|
||||
await query(
|
||||
'INSERT INTO business (name, nas_path, status) VALUES ($1, $2, $3)',
|
||||
[business.name, business.nasPath, business.status],
|
||||
);
|
||||
inserted += 1;
|
||||
} else if (row.nas_path !== business.nasPath || row.status !== business.status) {
|
||||
await query('UPDATE business SET nas_path = $1, status = $2 WHERE id = $3', [
|
||||
business.nasPath,
|
||||
business.status,
|
||||
row.id,
|
||||
]);
|
||||
updated += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const missing = existing.filter((row) => !seen.has(row.name));
|
||||
if (missing.length > 0) {
|
||||
log.warn(
|
||||
`[business-scan] ${missing.length} business(es) in the database no longer found on disk: ` +
|
||||
missing.map((row) => row.name).join(', '),
|
||||
);
|
||||
}
|
||||
|
||||
return { scanned: found.length, inserted, updated, missing: missing.length };
|
||||
}
|
||||
|
||||
export interface BusinessFile {
|
||||
name: string;
|
||||
size: number;
|
||||
mtime: string;
|
||||
}
|
||||
|
||||
/** Live, non-recursive listing of a business directory: PDFs first, then the rest, alphabetical. */
|
||||
export async function listBusinessFiles(nasPath: string): Promise<BusinessFile[]> {
|
||||
const entries = await readdir(nasPath, { withFileTypes: true });
|
||||
const files: BusinessFile[] = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
const info = await stat(path.join(nasPath, entry.name));
|
||||
files.push({ name: entry.name, size: info.size, mtime: info.mtime.toISOString() });
|
||||
}
|
||||
const isPdf = (name: string) => name.toLowerCase().endsWith('.pdf');
|
||||
return files.sort((a, b) => {
|
||||
if (isPdf(a.name) !== isPdf(b.name)) return isPdf(a.name) ? -1 : 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user