큰 자료를 올릴 때 연결이 끊기는 것과 저장한 조각이 없다는 것은 같은 상황이 아니다. 나는 실제 업로드 코드에서 파일을 다시 확인하고 서버 상태를 읽는 흐름을 살펴본 뒤, 다음 행동을 고르는 계획만 작은 함수로 분리했다.

파일 이름보다 바이트 조건을 먼저 맞춘다

같은 이름으로 저장한 파일도 내용이 바뀔 수 있다. 현재 업로드 코드는 전체 파일의 SHA-256을 계산하고 서버 기록의 선언 값과 크기, 형식을 비교한다. 새 계획 함수도 같은 세 조건을 확인한다. 예전 업로드 식별자를 찾았다는 사실만으로 지금 선택한 파일을 그 기록에 이어 쓰지 않는다.

현재 서버의 전체 해시는 클라이언트가 선언한 값이다. 서버가 완료한 객체 전체를 다시 해시했다고 설명하지 않는다. 서버는 각 조각의 크기와 해시, 번호와 합계, 완성 객체 크기를 확인한다. 이 차이를 남겨야 파일 재개의 근거가 어느 단계에서 만들어졌는지 독자가 이해할 수 있다.

상태 조회 실패를 새 업로드의 시작으로 바꾸지 않는다

기록을 읽는 요청이 잠깐 실패한 경우에는 업로드가 없어졌다고 판단할 수 없다. 실제 브라우저 업로더는 조회 오류가 찾을 수 없음인 경우와 그 밖의 실패를 나누고, 일반 실패에서는 이전 식별자를 버리지 않는다. 새 함수의 server가 비어 있는 경우도 현재 상태를 확인해 기록이 없다고 판단한 입력으로 사용해야 한다.

서버가 이미 준비 상태라면 다시 조각을 보내지 않고 그 결과를 사용한다. 중단한 기록은 새 업로드를 고르는 조건으로 남긴다. 이 모듈은 네트워크 요청을 하지 않으므로 실패의 원인까지 추정하지 않는다. 상태를 읽는 호출부가 사실을 확인하고 계획 함수에는 확인한 상태를 전달하는 구조다.

해시가 맞고 완료가 관찰된 조각을 구별한다

조각 기록이 있다는 것과 객체 저장소의 완료 응답을 확인한 것은 구별한다. 새 함수는 같은 번호의 로컬 해시와 크기를 비교하고 ETag가 관찰된 조각을 건너뛴다. 내용 조건은 같지만 ETag가 없으면 다시 보낼 조각으로 계획한다. 마지막 조각은 전체 크기에 맞춰 짧아질 수 있으므로 그 길이도 따로 계산한다.

같은 번호의 내용이 달라지거나 기록이 중복되면 무조건 다시 쓰지 않고 멈춘다. 현재 서버도 다른 내용이 이미 등록된 조각을 충돌로 처리한다. 나는 자동 재시도가 빠르게 보이는 것보다 어느 조각을 재사용하고 어느 상태를 먼저 대조해야 하는지 설명하는 편이 실제 운영에 도움이 된다고 생각한다.

코드와 구현 살펴보기

조회한 서버 상태로 재개 순서를 고르는 함수

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.'};
}

조각 전달과 최종 완료를 다른 단계로 남긴다

모든 조각의 완료가 관찰되면 새 계획 함수는 complete를 반환한다. 이것은 완성 객체가 준비됐다는 결과가 아니라 완료 요청을 보낼 차례라는 뜻이다. 실제 서버는 번호와 합계를 확인하고 기존 완성 객체가 있는지도 읽은 뒤 준비 상태를 기록한다. 완료 응답을 놓친 경우에도 그 상태를 다시 읽어 판단할 수 있게 한 구조다.

실제 업로드는 5MiB 조각과 최대 1GiB 조건을 사용한다. 아래 익명 예제는 원리를 읽기 쉽도록 9바이트 파일과 4바이트 조각으로 줄인 모형이다. 이 값으로 운영 서버에 요청하지 않는다. 작은 예제의 성공과 실제 대용량 파일의 전송·재개 성능은 다른 근거로 확인해야 한다.

읽을 수 있는 계획과 실행 결과를 함께 기록한다

새 함수는 create와 ready, upload-remaining과 complete를 구별하고 각 조각의 번호와 위치, 길이와 행동을 반환한다. 호출부는 이 계획을 읽고 실제 전송을 수행할 수 있다. 로컬 검사에서는 짧은 마지막 조각과 누락된 완료 응답, 다른 파일과 중복 기록을 확인했다. 공개 예제는 같은 함수를 사용하지만 전송은 하지 않는다.

현재 확인은 함수와 원래 코드, 공개 HTML 전달의 범위이며 새 브라우저의 파일 선택과 재개 조작은 미검증이다. 실제 적용에는 사용하는 파일과 연결, 중단 지점과 완료 상태를 이어서 확인해야 한다. 나는 재개 버튼 하나보다 버튼이 어떤 기록을 믿고 무엇을 다시 보내는지 읽을 수 있는 모듈을 남기려 한다.