같은 이름으로 내려받아도 앞의 결과는 남아야 한다. 다만 저장 이름을 고르는 방법은 작업에 따라 달랐다. 브라우저는 기존 파일과 진행 중인 경로를 피해 새 이름을 골랐고, 수집기는 서로 다른 주소가 같은 파일 이름이 되는 오류를 고쳐야 했다. 새 결과를 추가하는 일과 같은 원본을 다시 찾는 일에는 다른 기준이 필요했다.
Rust 별도판의 CSV 내보내기에서는 다른 계정이 예약한 경로를 먼저 차지하는 문제도 드러났다. 해당 수정 기록과 집필 당시 소스를 뒤에 덧붙였다. 아래 수집기·공개 모듈의 시험은 이전 공개본에 남은 과거 결과이며 이번 집필에서 다시 실행하지 않았다.
먼저 파일로 저장할 수 있는 이름을 만든다
safeDownloadName은 경로 구분자와 Windows 금지 문자, 제어 문자를 정리하고 끝의 점·공백을 제거한다. 비어 버리면 기본 이름을 쓴다. CON이나 NUL처럼 장치명으로 취급될 수 있는 이름에는 접두 문자를 붙인다. 서버가 제시한 글자를 그대로 저장할 수 있다고 가정하지 않은 것이다.
길이가 너무 길면 최대 길이를 줄이되 적당한 길이의 확장자는 끝에 보존한다. 확장자 앞에서 이모지의 surrogate 한쪽만 남는 상황도 피한다. 모든 Unicode 정규화와 모든 운영체제 규칙을 해결한 범용 이름 라이브러리라고 말하지는 않는다. 현재 제품이 대상으로 삼은 저장 환경에서 예상할 수 있는 실패를 구체적으로 줄이는 코드다.
디스크와 진행 중 목록을 함께 피한다
네이티브 다운로드는 후보 경로가 이미 디스크에 있거나 activeDownloadPaths에 예약돼 있으면 이름에 번호를 붙여 다시 고른다. 확장자를 떼어낸 본문 뒤에 번호를 넣고 확장자는 끝에 둔다. 두 번째 보고서가 첫 번째 보고서와 다른 위치를 갖게 하는 단순하지만 중요한 규칙이다.
디스크 존재 검사만으로는 같은 순간에 시작한 다운로드 두 개를 구별하기 어렵다. 둘 다 아직 파일을 만들기 전일 수 있기 때문이다. 그래서 현재 앱이 고른 저장 위치를 예약 집합에 넣고 종료 이벤트에서 해제한다. 이 집합은 앱 내부의 동시 전송을 조정하며 외부 프로그램 전체와 공유하는 운영체제 잠금은 아니다.
버퍼 저장에는 배타 생성으로 한 번 더 막는다
생성 CSV처럼 이미 메모리에 있는 내용을 저장하는 saveDownloadBuffer는 후보를 골라도 open의 wx 모드로 새 파일만 만든다. 다른 저장이 먼저 같은 이름을 만들었다면 EEXIST를 받아 다음 번호로 간다. 존재 여부를 읽은 직후 다른 작업이 끼어드는 간격을 실제 생성 시점에서 다시 다룬다.
여기서도 네이티브 다운로드의 예약 위치를 피한다. 원본 단위시험은 기존 owned 파일과 예약된 다음 번호를 남긴 채 두 개의 저장을 동시에 호출하고, 결과 경로가 다르며 각각의 내용이 보존되는지 검사한다. 두 저장 경로가 같은 구현을 쓴다고 뭉뚱그리지 않고 각각 어떤 단계가 충돌을 막는지 설명했다.
사용자가 고른 저장 위치에는 별도 맥락이 있다
시크릿 네이티브 다운로드는 저장 대화상자에서 사용자가 직접 위치를 고른다. 취소하면 전송을 취소하고 기록을 만들지 않는 흐름이다. 이 경로의 사용자 선택을 일반 자동 번호 부여와 완전히 같은 정책이라고 적으면 실제 동작을 오해하게 된다. 선택한 파일은 디스크에 남는다는 안내도 필요했다.
진행 중 예약과 충돌하면 새 item을 취소하는 검사도 있다. 하지만 이 집합이 이미 존재하는 경로를 저장창에서 선택하는 모든 상황까지 결정하지는 않는다. 자동 번호 부여, 사용자의 명시적인 위치 선택, 외부 프로그램과의 경쟁을 각각의 조건으로 설명해야 한다.
확장자 보존과 내용 보존을 각각 확인한다
이름 시험에는 긴 이름의 확장자, 예약 장치명, 금지 문자, 잘린 surrogate, 점만 남은 이름이 들어 있다. 버퍼 시험은 기존 파일을 보존하면서 동시 저장한 두 내용이 모두 남는지 본다. 파일 감사 시험에는 동명 순차 다운로드와 여섯 개 동시 다운로드의 경로·해시 확인도 설계돼 있다.
첫 집필에서는 그 브라우저의 소스와 시험을 읽었고 원래 제품을 새로 구동하지 않았다. 공개한 파일 영수증 예제의 이름 검사는 위험한 입력을 고치는 대신 거절하는 더 작은 계약이다. 운영의 이름 정책을 그대로 복제했다고 하지 않는다. 아래 수집기의 후속 수정과도 구분해, 이름이 유효한가, 기존 결과가 남는가, 실제 내용이 맞는가를 각각의 근거로 확인한다.
수집 주소 두 개가 같은 저장 이름이 됐다
후속 개발에서는 수집기의 실제 저장 함수를 합성 임시 폴더에서 호출했다. 확장자가 없는 /guide를 HTML 이름으로 고르면 guide.html이 되고, 원래 /guide.html인 주소도 같은 이름이 됐다. 서로 다른 문서 두 개를 차례로 저장하자 경로가 같았고, 첫 문서의 바이트가 둘째 문서로 바뀌었다. 과거 고객 자료에서 손실을 발견했다는 뜻이 아니라 현재 코드의 반례를 직접 만든 것이다.
문자를 안전하게 바꾸는 일도 같은 문제를 만들 수 있었다. 서로 다른 주소가 금지 문자 치환이나 대소문자 차이, 긴 이름 축약 뒤에 같은 파일 경로로 보일 수 있다. 사람이 읽기 쉬운 이름을 만들었다는 사실만으로 원래 주소의 구별까지 남았다고 판단할 수 없었다.
기존 검사가 모두 통과했다는 기록도 이 반례를 대신하지 못했다. 수정 전의 Python 20개와 Node 11개 검사는 통과했지만 이 두 문서의 저장을 구별하지 않았다. 실패를 확인한 뒤에는 전체 수집 범위를 줄이거나 특정 주소를 버리는 대신 저장 식별자의 책임을 고쳤다.
새 다운로드의 번호와 원본 주소의 식별자는 다르다
브라우저에서는 같은 이름의 새 결과를 별도 위치에 남겨야 하므로 번호가 유용하다. 수집기는 저장한 주소를 다시 찾아 링크를 읽고 이어가야 한다. 매번 새 번호만 붙이면 파일은 남아도 주소와 저장본의 연결이 흐려진다. 이 경로에는 같은 주소가 같은 위치를 고르는 결정적 식별자를 적용했다.
수정한 이름 함수는 읽을 수 있는 기본명에 전체 URL의 SHA-256을 더한다. 스킴·경로·쿼리가 다른 입력은 그 전체 문자열로 구별하며, 이름이 길면 기존의 짧은 해시 경로를 유지한다. 저장 위치를 고치는 동안 기존 파일을 일괄 이동하거나 삭제하지 않았다. 새 실행이 고르는 위치와 옛 결과의 존재를 따로 다뤘다.
여기서 해시한 것은 URL 문자열이다. 그 주소에서 받은 문서의 바이트를 해시한 것이 아니다. 같은 주소의 내용이 바뀌어도 주소 식별자는 같을 수 있고, 바이트가 같은 두 주소도 따로 존재할 수 있다. 파일 영수증 예제의 내용 해시와 이번 저장용 주소 지문을 같은 근거라고 부르지 않는 이유다. 예상 내용의 일치나 원문 인증, 최신성은 별도로 확인해야 한다.
옛 파일은 이름만 믿고 이어받지 않았다
저장 경로를 구별해도 읽는 쪽이 예전 guide.html을 무조건 신뢰하면 잘못된 이어받기가 남는다. 처음 코드의 페이지 읽기는 해당 이름의 파일이 있으면 현재 주소의 HTML처럼 반환했다. 그 파일이 다른 주소의 저장본인지 확인하는 연결이 없었다. 새 저장과 기존 캐시 읽기를 함께 바꿔야 했다.
새 경로를 먼저 확인하고, 이전 형식의 HTML은 수집 목록에 현재 URL과 대응하는 기록이 있을 때만 후보로 삼았다. 기록의 경로가 pages 안에 있고 다른 URL이 같은 경로를 소유한다고 적혀 있지 않은지도 확인한다. 같은 URL의 비모호한 옛 HTML은 계속 재사용할 수 있게 했다.
대응 기록이 없거나 여러 주소가 같은 파일을 가리키면 옛 파일을 지우지 않고 기존 HTTP 조회 경로로 돌아간다. 수집 목록의 경로가 잘못됐거나 바깥을 가리키는 경우도 재사용 근거로 받지 않는다. 이미 덮인 과거 바이트를 되살리는 기능은 아니다. 이 목록은 주소와 파일의 대응 근거일 뿐 내용의 진위나 현재성을 인증하지 않는다.
파일 수 대신 각 문서의 바이트를 검사했다
새로 추가한 15개 검사는 원본 저장·캐시·보고서 함수를 합성 폴더에서 실행한다. 확장자 보정, 대소문자·금지 문자·축약·쿼리의 구별과 실제 페이지·자산 두 내용의 보존을 확인했다. 같은 URL의 갱신, 파일과 폴더 경로, 동시에 저장한 여러 문서도 대조했다. 경로가 둘이라는 확인에서 멈추지 않고 각 파일에서 기대한 바이트를 다시 읽었다.
옛 캐시 시험은 비모호한 대응의 재사용과, 미등록·모호·잘못된 목록의 재조회 경로를 나눴다. 실제 보고서와 ZIP 함수도 호출해 서로 다른 두 문서와 옛 파일의 바이트가 압축 안에 남는지 확인했다. 총 결과는 Python 35개와 기존 Node 11개, 합계 46개 통과였다. 이는 직전 개발의 원제품 함수 시험이며 이번 집필에서 다시 실행한 숫자는 아니다.
일반 수집·사이트맵·자산·robots·리다이렉트·보고서의 기존 메서드와 파서·옵션·기록 구조는 대조해 유지했다. 보호 대상 26개 파일의 바이트와 수정 시각도 보존했다. 실제 사이트를 새로 수집하거나 브라우저를 렌더링한 시험은 아니고, 고객의 기존 수집 결과를 전수 감사한 것도 아니다. 실행 중인 서비스나 프로필을 교체하지 않았다.
복구 시험에는 필요한 실행 환경도 드러났다
전후 소스를 묶고 실제로 풀어 28개와 29개, 합계 57개 파일의 해시를 대조했다. 처음에는 소스만 푼 폴더에서 Python 검사가 통과했지만 Node 정책 검사는 기존 robots-parser 의존성이 없어 멈췄다. 이 실패를 지우지 않고 이미 만든 ZIP과 로그를 보존했다. 코드가 잘못됐다고 단정하거나 복구 검사가 모두 끝났다고 보고하지 않았다.
기존 설치에 있던 의존성을 별도의 해시 묶음으로 보관하고 새 복구 폴더에 함께 추출한 뒤 다시 확인했다. 복구한 이전 소스는 31개, 수정 소스는 46개 검사를 통과했다. 새 패키지를 설치한 결과나 새 PC의 전체 설치 검수는 아니다. 원래 설치 환경은 그대로 두었고, 이미 덮어쓴 고객 원문의 복원 성공으로 확대하지 않았다.
공유 예제는 기존 파일 선택·바이트 영수증 모듈을 다시 사용했다. 당시 개정에서는 공개 ZIP의 바이트·해시를 확인하고 추출 폴더의 합성 검사 36개와 저장 기록 선택기를 점검했다. 이 모듈은 메모리에 전달한 바이트만 비교하며 파일을 쓰거나 충돌 경로를 고르지 않는다. 수집기에 자동 설치되는 수정본도 아니다. 주소 식별과 저장 결과 보존, 바이트 비교를 구분해서 살펴볼 예제다.
다른 계정이 예약한 경로는 파일이 없어도 피한다
후속 기록의 반례는 같은 다운로드 폴더를 쓰는 두 계정을 포함한다. 첫 CSV는 이미 저장돼 있다. 다른 계정이 다음 번호의 경로를 진행 중 다운로드에 예약했지만 그 파일은 아직 없다. 현재 계정의 CSV 내보내기가 디스크와 자기 계정의 목록만 보면 그 자리를 빈 경로로 고를 수 있다.
남아 있는 실패 로그에는 이 조건의 회귀 검사가 실패한 기록이 있다. 수정한 검사는 기존 파일을 유지하고, 다른 계정의 다음 경로를 실제 파일 없이 진행 중 기록에 넣는다. 이어 내보낸 파일의 이름이 그다음 번호를 쓰며 예약된 자리에는 파일을 만들지 않았는지 확인한다. 아래 이름은 그 관계를 설명하는 합성 예시다.
이 사례에서는 파일이 아직 없는 경로도 이미 할당된 자리일 수 있다. 디스크에 바이트가 생겼는지와 앱이 경로를 예약했는지를 함께 재현해야 빠진 검사 범위를 찾을 수 있다.
코드와 구현 살펴보기
합성 저장 이름 예시 · 실행 코드 아님
Synthetic names in one shared folder:
report.csv -> already exists
report (1).csv -> reserved by another account; file absent
report (2).csv -> candidate for this export내용 접근은 계정에 묶고 충돌은 공유 저장소에서 찾는다
집필 당시 Rust 내보내기는 현재 프로젝트·계정에 등록된 결과물에서만 CSV 내용을 가져왔다. 저장 이름은 화면에 보이는 다운로드 목록과 모든 프로필의 다운로드 기록을 함께 보고 골랐다. 취소 가능한 진행 중 항목의 경로를 소문자로 맞춰 후보와 비교한다. 내용은 허용된 계정에서 읽고, 공유 폴더의 충돌은 전체 진행 기록에서 찾는 구조다.
예약된 후보는 건너뛰고, 실제 파일을 만들 때에는 새 파일만 생성하는 옵션을 쓴다. 이미 존재하는 파일을 발견하면 다음 번호를 시도한다. 일반 내보내기와 비공개 저장창 경로도 나뉘어 있으므로 이 번호 부여를 모든 저장 대화상자의 덮어쓰기 정책으로 설명하지 않는다. 앱 밖의 모든 쓰기 작업과 경로 별칭까지 조정하는 전역 예약 장치라는 의미도 아니다.
수정 전 실패와 수정 후 같은 회귀 검사의 통과를 로그에서 대조했다. 개발 기록의 전체 474개 검사·빌드 통과는 당시 Rust판의 결과다. 이번에는 그 로그와 시험 정의를 읽었으며 원제품 검사나 다운로드를 실행하지 않았다. 당시 독립 코드 검수와 실제 화면 검수는 미확인 상태였고 전체 기능 전환도 진행 중이었다.
같은 구조를 적용한다면 자신이 소유한 합성 계정·파일로 조건을 만들 수 있다. 다른 계정의 진행 중 경로가 아직 파일로 생기지 않게 두고, 내용 조회 권한은 현재 계정에 유지한다. 내보내기가 예약 경로를 피하는지, 기존 파일과 예약 자리가 그대로 남는지 각각 확인한다.