A lost connection during a large upload does not establish that no parts were stored. I inspected the real uploader’s file check and server reconciliation, then extracted a small function that plans the next action without performing it.
Match byte conditions before relying on the filename
A file can retain its name while its contents change. The current uploader calculates a whole-file SHA-256 and compares its declared server value, size and MIME. The new planning function checks the same three conditions. Finding an old upload identifier is not enough to continue writing the newly selected file into that record.
The server’s whole-file hash is client-declared. The explanation does not claim a new server-side hash of the completed object. The server checks each part’s size and digest, numbering, combined size and completed object size. Retaining that distinction tells readers where the resumption evidence was produced instead of treating every hash field as the same verification.
Do not treat failed observation as a new upload
A temporary failure to read a record does not prove that the upload disappeared. The actual browser uploader distinguishes not-found from other observation errors and retains the previous identifier on an ordinary failure. A null server input in the new function should likewise represent an established absence, not a guessed consequence of a failed request.
If the server is already ready, the plan uses that result without sending parts again. An aborted record explicitly leads toward a new upload. This module makes no network requests and does not infer their failure causes. The calling transport establishes current facts and passes the reconciled state into the planner.
Distinguish matching parts with an observed completion
A part record and an observed object-store completion are distinct facts. The new function compares local digest and size for each numbered part, skipping a match with an observed ETag. Matching conditions without an ETag are planned for another upload attempt. The final portion can be shorter, so its expected size is calculated independently.
Changed content under the same part number or duplicate metadata stops the plan rather than triggering a blind write. The actual server also conflicts when different content was already registered for that number. Explaining which parts are reusable and which uncertain states need reconciliation provides more operational value than making retries merely appear fast.
Explore the implementation
Choose resumption actions from reconciled state
export function planUploadResume(file,server=null){
const validHash=value=>typeof value==='string'&&/^[a-f0-9]{64}$/.test(value);
if(!file||!Number.isSafeInteger(file.size)||file.size<1||!validHash(file.sha256)||typeof file.mime!=='string'||!file.mime)throw new TypeError('Explicit file size, MIME and SHA-256 required');
if(!server)return {action:'create',reason:'No server record has been reconciled',parts:[]};
if(server.declared_sha256!==file.sha256||server.byte_size!==file.size||server.mime_type!==file.mime)throw new Error('Server record belongs to different file conditions');
if(server.state==='aborted')return {action:'create',reason:'Previous upload was aborted',parts:[]};
if(server.state==='ready')return {action:'ready',reason:'Server has already completed this declared file',parts:[]};
if(server.state!=='uploading'||!Number.isSafeInteger(server.chunk_bytes)||server.chunk_bytes<1||!Array.isArray(server.parts)||!Array.isArray(file.partHashes))throw new TypeError('Current upload state and local part hashes required');
const count=Math.ceil(file.size/server.chunk_bytes);if(count>10000||file.partHashes.length!==count||!file.partHashes.every(validHash))throw new RangeError('Part hash plan does not match the file');
const completed=new Map();
for(const part of server.parts){
if(!Number.isInteger(part.part_number)||part.part_number<1||part.part_number>count||completed.has(part.part_number))throw new Error('Invalid or duplicate server part');
if(part.etag!==null&&part.etag!==undefined&&(typeof part.etag!=='string'||!part.etag))throw new TypeError('Observed ETag must be nonempty text');
completed.set(part.part_number,part);
}
const parts=[];
for(let number=1;number<=count;number++){
const offset=(number-1)*server.chunk_bytes,byteSize=Math.min(server.chunk_bytes,file.size-offset),expected=file.partHashes[number-1],part=completed.get(number);
if(part&&(part.sha256!==expected||part.byte_size!==byteSize))throw new Error('Server part content differs; reconcile before another write');
parts.push({number,offset,byteSize,action:part?.etag?'skip':'upload'});
}
return {action:parts.some(x=>x.action==='upload')?'upload-remaining':'complete',parts,scope:'Planning from independently obtained local hashes and current server metadata. No network action, database transaction or server whole-file rehash.'};
}
Keep part delivery and final completion separate
When all part completions are observed, the function returns complete. That means the next action is a completion request, not that the final object is already ready. The real server checks numbering and combined size, reads whether an object already completed and records readiness. Lost completion responses can then be reconciled through current state.
The actual uploader uses 5 MiB parts and a maximum size of 1 GiB. The anonymous example reduces the model to 9 bytes and 4-byte parts for readability; it sends none of those values to the operating server. A small planning example and real large-file transfer or resumption performance require different evidence. The example explains the decision sequence without claiming that performance result.
Retain the plan separately from execution results
The function distinguishes create, ready, upload-remaining and complete, returning each portion’s number, offset, size and action. A caller can inspect that plan before performing transport. Local checks cover a short final part, missing observed completion, another file and duplicate records. The published example uses the same function while performing no upload.
Current evidence covers the function, original source and public HTML delivery. New browser file selection and resumption interaction remain unverified. An integration needs continued checks with its file, connection, interruption point and final state. I want a module that explains what the resume action trusts and sends again, not just a button with an optimistic label.