Full DMS: folders + ACLs, versioning, trash, bulk ops, preview, smart folders
Rebuild the dashboard as a Drive-style document management system on top of the existing CaveauAI hybrid RAG pipeline. Backend: - 5 migrations (versions, trash soft-delete, saved searches, categories, audit) - DMS helpers (folder ACL walker, disk storage, audit, version snapshot, XLSX/PPTX/HTML/CSV/MD extractors) - New APIs: folders, document-versions, trash, bulk, preview, saved-searches, categories, diagnostics - Extended APIs: documents (folder_id, soft-delete, ACL filter, sort), upload (9 file types, version-collision detection with replace/new/keep-both, disk persistence), chat-stream (folder scoping + graph related-documents) - 30-day trash purge cron with Qdrant + disk + graph cleanup Frontend: - Drive-style two-pane browser with folder tree, drag-drop, bulk-action bar, right-click context menu, multi-select - New pages: folders (tree + per-folder ACL editor), trash (restore/purge) - Extended pages: upload (folder picker, version-collision modal, 9 file type chips), document (Preview/Versions/Permissions tabs with PDF.js + mammoth.js + audio), index (DMS KPIs + activity feed), settings (live diagnostics ping MariaDB/Qdrant/LiteLLM/FalkorDB/disk), chat (folder scope chips + related-authorities chips) - New CSS (dms.css) + JS bundle (dms.js) exposing window.DBN_DMS - Sidebar nav adds Folders + Trash items All routes return HTTP 200 in local smoke test; all 32 files lint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
/**
|
||||
* /api/dashboard/preview.php?id=123[&version_id=5][&download=1]
|
||||
*
|
||||
* Streams the on-disk original file for a document, with the correct
|
||||
* Content-Type. Used by PDF.js, the <audio> player, image previews, and
|
||||
* "Download original" links.
|
||||
*
|
||||
* ACL: enforces folder read permission on the document.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/includes/bootstrap.php';
|
||||
|
||||
dbnToolsRequireAuth();
|
||||
|
||||
try {
|
||||
$tenant = dbnToolsEnsureDashboardTenant();
|
||||
} catch (DbnToolsHttpException $e) {
|
||||
dbnToolsError($e->getMessage(), $e->status, $e->errorCode);
|
||||
}
|
||||
$clientId = (int)$tenant['client_id'];
|
||||
$userId = (int)($tenant['client_user_id'] ?? 0);
|
||||
$tenantRole = (string)($tenant['role'] ?? 'editor');
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
$versionId = (int)($_GET['version_id'] ?? 0);
|
||||
$download = !empty($_GET['download']);
|
||||
|
||||
if ($id <= 0) {
|
||||
dbnToolsError('id is required.', 400, 'missing_id');
|
||||
}
|
||||
|
||||
$db = dbnToolsDb();
|
||||
|
||||
$stmt = $db->prepare('SELECT id, folder_id, storage_path, original_filename, source_type FROM client_documents WHERE id = ? AND client_id = ?');
|
||||
$stmt->execute([$id, $clientId]);
|
||||
$doc = $stmt->fetch();
|
||||
if (!$doc) {
|
||||
dbnToolsError('Document not found.', 404, 'not_found');
|
||||
}
|
||||
$fid = $doc['folder_id'] ? (int)$doc['folder_id'] : 0;
|
||||
if (!dbnDmsUserCanAccessFolder($fid ?: null, 'read', $clientId, $userId, $tenantRole)) {
|
||||
dbnToolsError('Forbidden.', 403, 'forbidden');
|
||||
}
|
||||
|
||||
$path = (string)($doc['storage_path'] ?? '');
|
||||
$filename = (string)($doc['original_filename'] ?? '');
|
||||
|
||||
if ($versionId > 0) {
|
||||
$vs = $db->prepare('SELECT storage_path, original_filename FROM client_document_versions WHERE id = ? AND document_id = ? AND client_id = ?');
|
||||
$vs->execute([$versionId, $id, $clientId]);
|
||||
$ver = $vs->fetch();
|
||||
if (!$ver) {
|
||||
dbnToolsError('Version not found.', 404, 'version_not_found');
|
||||
}
|
||||
$path = (string)($ver['storage_path'] ?? '');
|
||||
$filename = (string)($ver['original_filename'] ?? $filename);
|
||||
}
|
||||
|
||||
if ($path === '' || !is_file($path) || !is_readable($path)) {
|
||||
dbnToolsError('Original file is not available for this document.', 404, 'file_missing',
|
||||
['hint' => 'Document predates disk storage, or file was purged.']);
|
||||
}
|
||||
|
||||
$ext = dbnDmsExtensionFromFilename($filename);
|
||||
if ($ext === '' && $path !== '') {
|
||||
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
}
|
||||
$contentType = dbnDmsContentTypeForExt($ext);
|
||||
|
||||
dbnDmsLogAudit($clientId, $userId ?: null, $download ? 'download' : 'preview',
|
||||
['version_id' => $versionId ?: null, 'ext' => $ext], $id, $fid ?: null);
|
||||
|
||||
// Suppress any earlier output (defensive).
|
||||
if (ob_get_level() > 0) { @ob_end_clean(); }
|
||||
|
||||
$size = filesize($path) ?: 0;
|
||||
header('Content-Type: ' . $contentType);
|
||||
header('Content-Length: ' . $size);
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: private, max-age=300');
|
||||
$disposition = $download ? 'attachment' : 'inline';
|
||||
$safeName = $filename !== '' ? $filename : ('document-' . $id . '.' . ($ext ?: 'bin'));
|
||||
$safeName = preg_replace('/[\r\n"]/', '_', $safeName) ?? $safeName;
|
||||
header(sprintf('Content-Disposition: %s; filename="%s"', $disposition, $safeName));
|
||||
|
||||
// Range requests (basic) — useful for PDF.js + audio scrubbing.
|
||||
$range = $_SERVER['HTTP_RANGE'] ?? '';
|
||||
if ($range && preg_match('/bytes=(\d+)-(\d+)?/', $range, $m)) {
|
||||
$start = (int)$m[1];
|
||||
$end = isset($m[2]) && $m[2] !== '' ? (int)$m[2] : ($size - 1);
|
||||
if ($end >= $size) $end = $size - 1;
|
||||
$length = $end - $start + 1;
|
||||
http_response_code(206);
|
||||
header('Accept-Ranges: bytes');
|
||||
header("Content-Range: bytes {$start}-{$end}/{$size}");
|
||||
header('Content-Length: ' . $length);
|
||||
$fh = fopen($path, 'rb');
|
||||
if ($fh) {
|
||||
fseek($fh, $start);
|
||||
$remaining = $length;
|
||||
while (!feof($fh) && $remaining > 0) {
|
||||
$chunk = fread($fh, min(8192, $remaining));
|
||||
if ($chunk === false) break;
|
||||
echo $chunk;
|
||||
$remaining -= strlen($chunk);
|
||||
@ob_flush(); @flush();
|
||||
}
|
||||
fclose($fh);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Accept-Ranges: bytes');
|
||||
readfile($path);
|
||||
exit;
|
||||
Reference in New Issue
Block a user