+ {dbPhase === 'idle'
+ ? 'The locate database will be downloaded on first search, or tap the scan icon to rebuild it.'
+ : 'Type a filename or path to search the index.'}
+
)}
diff --git a/src/app/locate-db.ts b/src/app/locate-db.ts
index f8744f8..28ee1fa 100644
--- a/src/app/locate-db.ts
+++ b/src/app/locate-db.ts
@@ -1,5 +1,5 @@
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
-import { getWebDAVBaseUrl, basename } from './webdav';
+import { getWebDAVBaseUrl, basename, listDirectory, putFileContents, deletePath } from './webdav';
const LOCATE_PATH = '/sd/.locate';
@@ -76,6 +76,87 @@ export async function openLocateDb(onProgress?: (bytes: number) => void): Promis
_db = db;
}
+export type ScanPhase = 'scanning' | 'building' | 'saving';
+
+/**
+ * Recursively scan /sd, build a fresh SQLite locate database in memory,
+ * and upload it to /sd/.locate via WebDAV PUT.
+ *
+ * onProgress(phase, value):
+ * scanning → bytes of PROPFIND XML received so far
+ * building → number of entries inserted so far
+ * saving → bytes of the serialized DB written to the server
+ */
+export async function buildLocateDb(
+ onProgress?: (phase: ScanPhase, value: number) => void,
+): Promise<{ count: number; bytes: number }> {
+ const sqlite3 = await getSqlite3();
+
+ // ── 1. Recursive PROPFIND on /sd ────────────────────────────────────────
+ const entries = await listDirectory(
+ '/sd', true,
+ bytes => onProgress?.('scanning', bytes),
+ );
+
+ // ── 2. Build in-memory DB ───────────────────────────────────────────────
+ const db = new sqlite3.oo1.DB(':memory:', 'ct');
+ db.exec(
+ 'CREATE TABLE files (' +
+ ' path TEXT PRIMARY KEY,' +
+ ' size INTEGER NOT NULL DEFAULT 0,' +
+ ' mtime INTEGER NOT NULL DEFAULT 0,' +
+ ' is_dir INTEGER NOT NULL DEFAULT 0' +
+ ');' +
+ 'CREATE INDEX IF NOT EXISTS idx_path ON files(path);',
+ );
+
+ db.exec('BEGIN');
+ const stmt = db.prepare('INSERT OR REPLACE INTO files VALUES (?,?,?,?)');
+ try {
+ for (let i = 0; i < entries.length; i++) {
+ const e = entries[i];
+ stmt.bind([
+ e.path,
+ e.size,
+ e.lastModified ? Math.floor(e.lastModified.getTime() / 1000) : 0,
+ e.type === 'folder' ? 1 : 0,
+ ]).stepReset();
+ if (i % 250 === 0) onProgress?.('building', i);
+ }
+ } finally {
+ stmt.finalize();
+ }
+ db.exec('COMMIT');
+ onProgress?.('building', entries.length);
+
+ // ── 3. Serialize to bytes ────────────────────────────────────────────────
+ const pSaved = sqlite3.wasm.pstack.pointer;
+ let dbBytes: Uint8Array;
+ try {
+ const pSize = sqlite3.wasm.pstack.alloc(8);
+ const pData = sqlite3.capi.sqlite3_serialize(db.pointer, 'main', pSize, 0);
+ if (!pData) throw new Error('sqlite3_serialize returned NULL');
+ const rawSize = sqlite3.wasm.peek(pSize, 'i64');
+ const size = Number(typeof rawSize === 'bigint' ? rawSize : rawSize);
+ dbBytes = sqlite3.wasm.heap8u().slice(pData, pData + size);
+ sqlite3.capi.sqlite3_free(pData);
+ } finally {
+ sqlite3.wasm.pstack.restore(pSaved);
+ db.close();
+ }
+
+ // ── 4. Delete existing + upload ─────────────────────────────────────────
+ onProgress?.('saving', 0);
+ await deletePath(LOCATE_PATH).catch(() => {});
+ await putFileContents(LOCATE_PATH, dbBytes);
+ onProgress?.('saving', dbBytes.length);
+
+ // Invalidate cached DB so the next search reloads the fresh file.
+ resetLocateDb();
+
+ return { count: entries.length, bytes: dbBytes.length };
+}
+
export interface LocateEntry {
path: string;
name: string;