This commit is contained in:
Timothy Jaeryang Baek
2026-05-20 01:30:26 +04:00
parent 60c9db1cb8
commit 73bdf86766
3 changed files with 148 additions and 103 deletions
+1
View File
@@ -185,6 +185,7 @@ async def process_uploaded_file(
request,
ProcessFileForm(file_id=file_item.id, collection_name=knowledge_id),
user=user,
db=db_session,
)
log.info(f'Linked file {file_item.id} to knowledge {knowledge_id}')
except Exception as e:
+3 -2
View File
@@ -5,7 +5,8 @@ export const uploadFile = async (
token: string,
file: File,
metadata?: object | null,
process?: boolean | null
process?: boolean | null,
stream: boolean = true
) => {
const data = new FormData();
data.append('file', file);
@@ -42,7 +43,7 @@ export const uploadFile = async (
throw error;
}
if (res) {
if (res && stream) {
const status = await getFileProcessStatus(token, res.id);
if (status && status.ok) {
@@ -78,7 +78,8 @@
let showNewDirectoryModal = false;
let showSyncConfirmModal = false;
let pendingSyncFiles: Array<{path: string, filename: string, file: File}> | null = null;
let pendingSyncFiles: Array<{ path: string; filename: string; file: File }> | null = null;
let syncing: string | null = null;
let showAccessControlModal = false;
let minSize = 0;
@@ -542,13 +543,17 @@
};
// Collect files from a directory without uploading — returns {path, filename, file}[]
const collectDirectoryFiles = async (): Promise<Array<{path: string, filename: string, file: File}> | null> => {
const collectDirectoryFiles = async (): Promise<Array<{
path: string;
filename: string;
file: File;
}> | null> => {
const isFileSystemAccessSupported = 'showDirectoryPicker' in window;
try {
if (isFileSystemAccessSupported) {
const dirHandle = await window.showDirectoryPicker();
const collected: Array<{path: string, filename: string, file: File}> = [];
const collected: Array<{ path: string; filename: string; file: File }> = [];
async function traverse(handle: FileSystemDirectoryHandle, dirPath = '') {
for await (const entry of handle.values()) {
@@ -580,8 +585,9 @@
input.onchange = () => {
try {
const files = Array.from(input.files || [])
.filter((file) => !hasHiddenFolder(file.webkitRelativePath) && !file.name.startsWith('.'));
const files = Array.from(input.files || []).filter(
(file) => !hasHiddenFolder(file.webkitRelativePath) && !file.name.startsWith('.')
);
const collected = files.map((file) => {
const parts = file.webkitRelativePath.split('/');
@@ -618,92 +624,112 @@
const syncDirectoryHandler = async () => {
if (!pendingSyncFiles?.length) return;
// ── 2. Compute checksums ──
toast.info($i18n.t('Computing checksums...'));
const manifest = await Promise.all(
pendingSyncFiles.map(async (entry) => ({
...entry,
checksum: await computeFileHash(entry.file),
size: entry.file.size
}))
);
pendingSyncFiles = null;
// ── 3. Diff against knowledge base ──
toast.info($i18n.t('Comparing with knowledge base...'));
const diff = await syncKnowledgeDiff(
localStorage.token,
id,
manifest.map(({ filename, path, checksum, size }) => ({ filename, path, checksum, size }))
);
if (!diff) {
toast.error($i18n.t('Failed to compare files.'));
return;
}
// ── 4. mkdir — create missing directories (parents first) ──
const createdDirectoryIds: Record<string, string> = {};
for (const dirPath of diff.mkdir) {
const segments = dirPath.split('/');
const name = segments.at(-1)!;
const parentPath = segments.slice(0, -1).join('/');
const parentId = parentPath ? createdDirectoryIds[parentPath] : null;
const directory = await createKnowledgeDirectory(
localStorage.token, knowledge.id, name, parentId
);
if (directory) {
createdDirectoryIds[dirPath] = directory.id;
}
}
// ── 5. Upload added + modified files ──
const filesToUpload = manifest.filter((entry) =>
diff.added.some((a: any) => a.filename === entry.filename && a.path === entry.path) ||
diff.modified.some((m: any) => m.filename === entry.filename && m.path === entry.path)
);
let uploadedCount = 0;
for (const entry of filesToUpload) {
const fileObject = new File([entry.file], entry.filename, { type: entry.file.type });
await uploadFile(localStorage.token, fileObject, {
knowledge_id: knowledge.id,
file_hash: entry.checksum,
directory_id: entry.path ? createdDirectoryIds[entry.path] : null
try {
// ── 2. Compute checksums ──
syncing = $i18n.t('Computing checksums ({{count}} files)', {
count: pendingSyncFiles.length
});
uploadedCount++;
toast.info(
$i18n.t('Uploading: {{current}}/{{total}}', {
current: uploadedCount,
total: filesToUpload.length
})
const manifest = await Promise.all(
pendingSyncFiles.map(async (entry) => ({
...entry,
checksum: await computeFileHash(entry.file),
size: entry.file.size
}))
);
}
pendingSyncFiles = null;
// ── 6. Cleanup — remove deleted files + rmdir orphaned directories ──
const staleFileIds = [
...diff.deleted.map((d: any) => d.file_id),
...diff.modified.map((m: any) => m.stale_file_id)
];
// ── 3. Diff against knowledge base ──
syncing = $i18n.t('Comparing with knowledge base...');
const diff = await syncKnowledgeDiff(
localStorage.token,
id,
manifest.map(({ filename, path, checksum, size }) => ({ filename, path, checksum, size }))
);
if (staleFileIds.length > 0 || diff.rmdir.length > 0) {
await syncKnowledgeCleanup(localStorage.token, id, staleFileIds, diff.rmdir);
}
if (!diff) {
toast.error($i18n.t('Failed to compare files.'));
return;
}
// ── 7. Report ──
toast.success(
$i18n.t(
'Sync complete: {{added}} added, {{modified}} modified, {{deleted}} deleted, {{unmodified}} unmodified',
{
added: diff.added.length,
modified: diff.modified.length,
deleted: diff.deleted.length,
unmodified: diff.unmodified_count
// ── 4. mkdir — create missing directories (parents first) ──
const createdDirectoryIds: Record<string, string> = {};
for (const dirPath of diff.mkdir) {
const segments = dirPath.split('/');
const name = segments.at(-1)!;
const parentPath = segments.slice(0, -1).join('/');
const parentId = parentPath ? createdDirectoryIds[parentPath] : null;
const directory = await createKnowledgeDirectory(
localStorage.token,
knowledge.id,
name,
parentId
);
if (directory) {
createdDirectoryIds[dirPath] = directory.id;
}
)
);
init();
}
// ── 5. Upload added + modified files ──
const filesToUpload = manifest.filter(
(entry) =>
diff.added.some((a: any) => a.filename === entry.filename && a.path === entry.path) ||
diff.modified.some((m: any) => m.filename === entry.filename && m.path === entry.path)
);
let uploadedCount = 0;
for (const entry of filesToUpload) {
uploadedCount++;
const displayPath = entry.path ? `${entry.path}/${entry.filename}` : entry.filename;
syncing = $i18n.t('Uploading {{current}}/{{total}}: {{file}}', {
current: uploadedCount,
total: filesToUpload.length,
file: displayPath
});
const fileObject = new File([entry.file], entry.filename, { type: entry.file.type });
await uploadFile(
localStorage.token,
fileObject,
{
knowledge_id: knowledge.id,
file_hash: entry.checksum,
directory_id: entry.path ? createdDirectoryIds[entry.path] : null
},
null,
false
).catch(() => null);
}
// ── 6. Cleanup — remove deleted files + rmdir orphaned directories ──
const staleFileIds = [
...diff.deleted.map((d: any) => d.file_id),
...diff.modified.map((m: any) => m.stale_file_id)
];
if (staleFileIds.length > 0 || diff.rmdir.length > 0) {
syncing = $i18n.t('Removing {{count}} stale files...', { count: staleFileIds.length });
await syncKnowledgeCleanup(localStorage.token, id, staleFileIds, diff.rmdir);
}
// ── 7. Report ──
toast.success(
$i18n.t(
'Sync complete: {{added}} added, {{modified}} modified, {{deleted}} deleted, {{unmodified}} unmodified',
{
added: diff.added.length,
modified: diff.modified.length,
deleted: diff.deleted.length,
unmodified: diff.unmodified_count
}
)
);
init();
} catch (e) {
toast.error(`${e}`);
} finally {
syncing = null;
}
};
const addFileHandler = async (fileId) => {
@@ -1216,17 +1242,17 @@
/>
<div class="hidden md:block">
<Tooltip content={$i18n.t('Click to copy ID')}>
<button
class="text-xs text-gray-500 font-mono shrink-0 px-2 py-1 rounded-lg cursor-pointer hover:underline transition whitespace-nowrap"
on:click={() => {
copyToClipboard(id);
toast.success($i18n.t('ID copied to clipboard'));
}}
>
{id}
</button>
</Tooltip>
<Tooltip content={$i18n.t('Click to copy ID')}>
<button
class="text-xs text-gray-500 font-mono shrink-0 px-2 py-1 rounded-lg cursor-pointer hover:underline transition whitespace-nowrap"
on:click={() => {
copyToClipboard(id);
toast.success($i18n.t('ID copied to clipboard'));
}}
>
{id}
</button>
</Tooltip>
</div>
</div>
</div>
@@ -1281,7 +1307,13 @@
{#if currentDirectoryId !== null}
<div class="px-5 -mt-1 pb-2">
<KnowledgeBreadcrumbs rootLabel={knowledge.name} {breadcrumbs} onNavigate={(dirId) => navigateToDirectory(dirId)} onMoveFile={(fileId, dirId) => moveFileToDirectoryHandler(fileId, dirId)} onMoveDir={(dirId, targetId) => moveDirectoryHandler(dirId, targetId)} />
<KnowledgeBreadcrumbs
rootLabel={knowledge.name}
{breadcrumbs}
onNavigate={(dirId) => navigateToDirectory(dirId)}
onMoveFile={(fileId, dirId) => moveFileToDirectoryHandler(fileId, dirId)}
onMoveDir={(dirId, targetId) => moveDirectoryHandler(dirId, targetId)}
/>
</div>
{/if}
@@ -1341,6 +1373,17 @@
</div>
</div>
{#if syncing}
<div class="mx-2.5 mt-2">
<div class="flex items-center gap-2.5 rounded-xl py-2 px-3 bg-gray-50 dark:bg-gray-850">
<Spinner className="size-3.5 shrink-0" />
<div class="text-xs text-gray-500 dark:text-gray-400 truncate">
{syncing}
</div>
</div>
</div>
{/if}
{#if fileItems !== null && fileItemsTotal !== null}
<div class="flex flex-row flex-1 gap-3 px-2.5 mt-2">
<div class="flex-1 flex">
@@ -1372,14 +1415,14 @@
deleteFileHandler(fileId);
}}
onRename={(fileId, name) => renameFileHandler(fileId, name)}
onNavigateDirectory={(dirId) => navigateToDirectory(dirId)}
onNavigateDirectory={(dirId) => navigateToDirectory(dirId)}
onRenameDirectory={(id, name) => renameDirectoryHandler(id, name)}
onDeleteDirectory={(id) => confirmDeleteDirectory(id)}
onMoveFileToDirectory={(fileId, dirId) =>
moveFileToDirectoryHandler(fileId, dirId)}
onMoveDirectoryToDirectory={(dirId, targetId) => moveDirectoryHandler(dirId, targetId)}
/>
onMoveDirectoryToDirectory={(dirId, targetId) =>
moveDirectoryHandler(dirId, targetId)}
/>
</div>
{#if fileItemsTotal > 30}