3
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user