This commit is contained in:
2025-10-10 15:53:38 +02:00
parent ddce0e39b3
commit dfadc8ba23
3 changed files with 119 additions and 125 deletions
+33 -15
View File
@@ -224,7 +224,7 @@ pub async fn upload_document(
while let Some(field) = multipart.next_field().await.map_err(|err| { while let Some(field) = multipart.next_field().await.map_err(|err| {
let msg = format!("invalid multipart data: {err}"); let msg = format!("invalid multipart data: {err}");
warn!(error = %err, "invalid multipart data"); error!(error = %err, "invalid multipart data");
AppError::bad_request(msg) AppError::bad_request(msg)
})? { })? {
let name = field.name().map(|n| n.to_string()); let name = field.name().map(|n| n.to_string());
@@ -235,7 +235,7 @@ pub async fn upload_document(
content_type = field.content_type().map(|mime| mime.to_string()); content_type = field.content_type().map(|mime| mime.to_string());
let data = field.bytes().await.map_err(|err| { let data = field.bytes().await.map_err(|err| {
let msg = format!("failed to read file bytes: {err}"); let msg = format!("failed to read file bytes: {err}");
warn!(error = %err, "failed to read file bytes"); error!(error = %err, "failed to read file bytes");
AppError::bad_request(msg) AppError::bad_request(msg)
})?; })?;
file_bytes = Some(data.to_vec()); file_bytes = Some(data.to_vec());
@@ -243,7 +243,7 @@ pub async fn upload_document(
Some("folder_id") => { Some("folder_id") => {
let value = field.text().await.map_err(|err| { let value = field.text().await.map_err(|err| {
let msg = format!("invalid folder id: {err}"); let msg = format!("invalid folder id: {err}");
warn!(error = %err, "invalid folder id"); error!(error = %err, "invalid folder id");
AppError::bad_request(msg) AppError::bad_request(msg)
})?; })?;
if !value.trim().is_empty() { if !value.trim().is_empty() {
@@ -255,12 +255,12 @@ pub async fn upload_document(
Some("metadata") => { Some("metadata") => {
let value = field.text().await.map_err(|err| { let value = field.text().await.map_err(|err| {
let msg = format!("invalid metadata: {err}"); let msg = format!("invalid metadata: {err}");
warn!(error = %err, "invalid metadata payload"); error!(error = %err, "invalid metadata payload");
AppError::bad_request(msg) AppError::bad_request(msg)
})?; })?;
metadata = serde_json::from_str(&value).map_err(|err| { metadata = serde_json::from_str(&value).map_err(|err| {
let msg = format!("metadata must be valid JSON: {err}"); let msg = format!("metadata must be valid JSON: {err}");
warn!(error = %err, "metadata parse failure"); error!(error = %err, "metadata parse failure");
AppError::bad_request(msg) AppError::bad_request(msg)
})?; })?;
} }
@@ -269,10 +269,19 @@ pub async fn upload_document(
} }
let file_bytes = file_bytes.ok_or_else(|| { let file_bytes = file_bytes.ok_or_else(|| {
warn!("upload rejected: missing file field"); error!("upload rejected: missing file field");
AppError::bad_request("file field is required") AppError::bad_request("file field is required")
})?; })?;
let original_name = original_name.unwrap_or_else(|| "upload.bin".to_string());
if file_bytes.is_empty() {
error!("upload rejected: empty file payload");
return Err(AppError::bad_request("file field must not be empty"));
}
let original_name = original_name.ok_or_else(|| {
error!("upload rejected: missing original filename");
AppError::bad_request("filename is required")
})?;
let original_name_for_log = original_name.clone();
let request = UploadRequest { let request = UploadRequest {
bytes: file_bytes, bytes: file_bytes,
@@ -283,18 +292,21 @@ pub async fn upload_document(
}; };
let outcome = match process_upload(&state, request).await { let outcome = match process_upload(&state, request).await {
Ok(outcome) => outcome, Ok(outcome) => {
info!(
document_id = %outcome.detail.document.id,
original_name = %outcome.detail.document.original_name,
created = outcome.created,
reused_existing = !outcome.created,
"document upload succeeded"
);
outcome
}
Err(err) => { Err(err) => {
error!(error = ?err, "document upload failed"); error!(error = ?err, original_name = %original_name_for_log, "document upload failed");
return Err(err); return Err(err);
} }
}; };
info!(
document_id = %outcome.detail.document.id,
original_name = %outcome.detail.document.original_name,
created = outcome.created,
"document upload succeeded"
);
let status = if outcome.created { let status = if outcome.created {
StatusCode::CREATED StatusCode::CREATED
} else { } else {
@@ -573,6 +585,12 @@ async fn process_upload(state: &AppState, request: UploadRequest) -> AppResult<U
.find(|asset| asset.asset_type == "thumbnail") .find(|asset| asset.asset_type == "thumbnail")
.cloned(); .cloned();
info!(
document_id = %document.id,
checksum = %checksum_hex,
"upload deduplicated existing document"
);
return Ok(UploadOutcome { return Ok(UploadOutcome {
detail: DocumentDetailResponse { detail: DocumentDetailResponse {
document: to_document_response(document, tags, thumbnail), document: to_document_response(document, tags, thumbnail),
+1
View File
@@ -87,6 +87,7 @@ impl Worker {
JobExecution::Success => { JobExecution::Success => {
if let Ok(mut conn) = self.state.db() { if let Ok(mut conn) = self.state.db() {
mark_job_succeeded(&mut conn, job.id)?; mark_job_succeeded(&mut conn, job.id)?;
info!(job_id = %job.id, job_type = %job.job_type, "job completed successfully");
} else { } else {
error!("failed to mark job succeeded due to pool error"); error!("failed to mark job succeeded due to pool error");
} }
+85 -110
View File
@@ -13,9 +13,6 @@ const API_ROOT = (process.env.API_BASE_URL || '').replace(/\/$/, '');
const api = axios.create({ const api = axios.create({
baseURL: API_ROOT ? `${API_ROOT}/api` : '/api', baseURL: API_ROOT ? `${API_ROOT}/api` : '/api',
headers: {
'Content-Type': 'application/json',
},
}); });
const DEFAULT_FOLDER_NAME = 'All Documents'; const DEFAULT_FOLDER_NAME = 'All Documents';
@@ -858,20 +855,27 @@ function App() {
const uploadFile = useCallback( const uploadFile = useCallback(
async (file, targetFolderId) => { async (file, targetFolderId) => {
if (!file || file.size === 0) {
setStatusMessage('Skipped empty file.', 'error');
return null;
}
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file, file.name);
if (targetFolderId && targetFolderId !== 'root') { if (targetFolderId && targetFolderId !== 'root') {
formData.append('folder_id', targetFolderId); formData.append('folder_id', targetFolderId);
} }
try { try {
const response = await api.post('/documents', formData, { const { data, status } = await api.post('/documents', formData);
headers: { 'Content-Type': 'multipart/form-data' }, const duplicate = data?.reused || status === 200;
}); setStatusMessage(
if (response.status === 200) { duplicate
setStatusMessage(`${file.name} already exists; reused existing document.`, 'info'); ? `${file.name} already exists; reused existing document.`
} else { : `Uploaded ${file.name}`,
setStatusMessage(`Uploaded ${file.name}`, 'success'); duplicate ? 'info' : 'success',
} );
return data;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
const message = error.response?.data?.error || `Failed to upload ${file.name}.`; const message = error.response?.data?.error || `Failed to upload ${file.name}.`;
@@ -911,98 +915,61 @@ function App() {
); );
const extractFilesFromDataTransfer = useCallback(async (dataTransfer) => { const extractFilesFromDataTransfer = useCallback(async (dataTransfer) => {
if (
!dataTransfer ||
!dataTransfer.items ||
!window.isSecureContext ||
!Array.from(dataTransfer.items).some((item) => typeof item.getAsFileSystemHandle === 'function')
) {
throw new Error('File System Access API is required for uploads.');
}
const results = []; const results = [];
if (!dataTransfer) { const pushFile = (file, ancestors) => {
return results; if (file) {
} results.push({
file,
segments: ancestors.filter(Boolean),
});
}
};
const walkDirectoryHandle = async (handle, ancestors) => { const walkDirectoryHandle = async (handle, ancestors) => {
const currentPath = handle.name ? [...ancestors, handle.name] : [...ancestors]; const nextAncestors = handle.name ? [...ancestors, handle.name] : [...ancestors];
for await (const childHandle of handle.values()) { for await (const child of handle.values()) {
if (childHandle.kind === 'file') { if (child.kind === 'file') {
const file = await childHandle.getFile(); const file = await child.getFile();
results.push({ file, segments: currentPath }); pushFile(file, nextAncestors);
} else if (childHandle.kind === 'directory') { } else if (child.kind === 'directory') {
await walkDirectoryHandle(childHandle, currentPath); // eslint-disable-next-line no-await-in-loop
await walkDirectoryHandle(child, nextAncestors);
} }
} }
}; };
const walkEntry = async (entry, ancestors) => { for (const item of Array.from(dataTransfer.items)) {
if (entry.isFile) { if (item.kind !== 'file') continue;
const file = await new Promise((resolve, reject) => entry.file(resolve, reject)); const getHandle = item.getAsFileSystemHandle?.bind(item);
results.push({ file, segments: ancestors }); if (!getHandle) {
return; throw new Error('File System Access API handle missing.');
} }
if (entry.isDirectory) { // eslint-disable-next-line no-await-in-loop
const childAncestors = entry.name ? [...ancestors, entry.name] : [...ancestors]; const handle = await getHandle();
const reader = entry.createReader(); if (!handle) continue;
const readEntries = () =>
new Promise((resolve, reject) => {
reader.readEntries(resolve, reject);
});
let entries; if (handle.kind === 'file') {
do { const file = await handle.getFile();
// eslint-disable-next-line no-await-in-loop pushFile(file, []);
entries = await readEntries(); } else if (handle.kind === 'directory') {
for (const child of entries) {
// eslint-disable-next-line no-await-in-loop
await walkEntry(child, childAncestors);
}
} while (entries.length);
}
};
const items = dataTransfer.items ? Array.from(dataTransfer.items).filter((item) => item.kind === 'file') : [];
if (items.length) {
for (const item of items) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
if (item.getAsFileSystemHandle) { await walkDirectoryHandle(handle, []);
// eslint-disable-next-line no-await-in-loop
const handle = await item.getAsFileSystemHandle();
if (!handle) continue;
if (handle.kind === 'file') {
// eslint-disable-next-line no-await-in-loop
const file = await handle.getFile();
results.push({ file, segments: [] });
} else if (handle.kind === 'directory') {
// eslint-disable-next-line no-await-in-loop
await walkDirectoryHandle(handle, []);
}
continue;
}
const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : null;
if (entry) {
// eslint-disable-next-line no-await-in-loop
await walkEntry(entry, []);
continue;
}
const file = item.getAsFile ? item.getAsFile() : null;
if (file) {
const segments = file.webkitRelativePath
? file.webkitRelativePath.split('/').slice(0, -1).filter(Boolean)
: [];
results.push({ file, segments });
continue;
}
} }
} }
if (!results.length && dataTransfer.files) { if (!results.length) {
results.push( throw new Error('No files detected in drop payload.');
...Array.from(dataTransfer.files).map((file) => ({
file,
segments: file.webkitRelativePath
? file.webkitRelativePath.split('/').slice(0, -1).filter(Boolean)
: [],
})),
);
} }
return results; return results;
@@ -1014,33 +981,41 @@ function App() {
setStatusMessage('Please log in before uploading.', 'error'); setStatusMessage('Please log in before uploading.', 'error');
return; return;
} }
setLoading(true); setLoading(true);
try { try {
folderPathCacheRef.current.clear(); folderPathCacheRef.current.clear();
const extracted = await extractFilesFromDataTransfer(dataTransfer); const extracted = await extractFilesFromDataTransfer(dataTransfer);
const baseFolderId = targetFolderId && targetFolderId !== 'root' ? targetFolderId : null; if (!extracted.length) {
setStatusMessage('No files to upload.', 'info');
if (!extracted.length && dataTransfer?.files) { return;
for (const file of Array.from(dataTransfer.files)) {
// eslint-disable-next-line no-await-in-loop
await uploadFile(file, targetFolderId ?? 'root');
}
} else {
for (const { file, segments } of extracted) {
// eslint-disable-next-line no-await-in-loop
const destinationId = segments.length
? await ensureFolderPathOnServer(baseFolderId, segments)
: baseFolderId;
const uploadTarget = destinationId
?? (targetFolderId && targetFolderId !== 'root' ? targetFolderId : 'root');
// eslint-disable-next-line no-await-in-loop
await uploadFile(file, uploadTarget);
}
} }
const baseFolderId =
targetFolderId && targetFolderId !== 'root' ? targetFolderId : null;
for (const { file, segments } of extracted) {
// eslint-disable-next-line no-await-in-loop
const destinationId = segments.length
? await ensureFolderPathOnServer(baseFolderId, segments)
: baseFolderId;
const uploadTarget =
destinationId ??
(targetFolderId && targetFolderId !== 'root' ? targetFolderId : 'root');
// eslint-disable-next-line no-await-in-loop
await uploadFile(file, uploadTarget);
}
await refreshCurrentFolder(); await refreshCurrentFolder();
if (targetFolderId && targetFolderId !== 'root' && targetFolderId !== selectedFolder) {
if (
targetFolderId &&
targetFolderId !== 'root' &&
targetFolderId !== selectedFolder
) {
await ensureFolderData(targetFolderId, { force: true }); await ensureFolderData(targetFolderId, { force: true });
} }
} catch (error) { } catch (error) {