fix: reject duplicate completed upload names
A user could select another local file with the same name as one that already exists in completed storage. The upload would be allowed to start and only hit an existing-file conflict late in the flow, which made the UI look like the file was uploadable. Reject duplicate sanitized names during upload creation so no staging record or chunk transfer starts for a file that cannot be completed. Keep the completion path non-replacing as a second guard by promoting through a no-overwrite file creation path, with a hard-link fast path and copy fallback for custom temp locations. The browser now treats the server's duplicate-name conflict as a terminal row: it disables the action, marks the item visually, and tells the user to rename the file if they want to upload that copy. Test Plan: - just check Refs: none
This commit is contained in:
+32
-10
@@ -5,7 +5,7 @@ const CHUNK_CONCURRENCY = 3;
|
||||
const FILE_CONCURRENCY = 3;
|
||||
const MAX_RETRIES = 5;
|
||||
const BASE_RETRY_DELAY_MS = 500;
|
||||
const COMPLETE_EXISTS_MESSAGE = "complete file already exists";
|
||||
const FILE_EXISTS_MESSAGE = "file already exists";
|
||||
|
||||
const fileInput = document.querySelector("#file-input");
|
||||
const pickButton = document.querySelector("#pick-button");
|
||||
@@ -191,6 +191,10 @@ function renderUploadItems() {
|
||||
for (const item of state.uploadItems) {
|
||||
const row = document.createElement("li");
|
||||
row.className = "upload-item";
|
||||
if (item.terminal) {
|
||||
row.classList.add("upload-item-blocked");
|
||||
row.setAttribute("aria-invalid", "true");
|
||||
}
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "upload-item-header";
|
||||
@@ -363,6 +367,10 @@ function uploadItemDetail(item) {
|
||||
}
|
||||
|
||||
function uploadActionLabel(item) {
|
||||
if (item.terminal) {
|
||||
return "Unavailable";
|
||||
}
|
||||
|
||||
if (item.finished) {
|
||||
return "Done";
|
||||
}
|
||||
@@ -736,7 +744,25 @@ async function runUploadItem(item) {
|
||||
}
|
||||
|
||||
async function handleTerminalUploadError(item, error) {
|
||||
if (!item.record || typeof error.status !== "number") {
|
||||
if (typeof error.status !== "number") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isFileExistsConflict(error)) {
|
||||
if (item.record) {
|
||||
await deleteRecord(item.record.upload_id);
|
||||
}
|
||||
item.record = null;
|
||||
item.completedChunks = new Set();
|
||||
setItemProgress(item, 0, 0);
|
||||
item.terminal = true;
|
||||
item.statusText =
|
||||
"File already exists on the server. Rename the file to upload this copy.";
|
||||
log(`${item.file.name}: file already exists. Rename it to upload this copy.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!item.record) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -752,17 +778,13 @@ async function handleTerminalUploadError(item, error) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (error.status === 409 && error.message === COMPLETE_EXISTS_MESSAGE) {
|
||||
await deleteRecord(uploadId);
|
||||
item.terminal = true;
|
||||
item.statusText = "Complete file already exists on the server.";
|
||||
log(`${item.file.name}: complete file already exists on the server.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isFileExistsConflict(error) {
|
||||
return error.status === 409 && error.message === FILE_EXISTS_MESSAGE;
|
||||
}
|
||||
|
||||
async function createUploadRecord(item, signal) {
|
||||
const response = await fetchJson("/api/uploads", {
|
||||
method: "POST",
|
||||
|
||||
@@ -109,6 +109,15 @@ h1 {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.upload-item-blocked {
|
||||
border-color: #f04438;
|
||||
background: rgb(240 68 56 / 8%);
|
||||
}
|
||||
|
||||
.upload-item-blocked .upload-meta span {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.upload-item-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
|
||||
Reference in New Issue
Block a user