실제 브라우저의 기록 저장 코드를 읽고, 실패할 때 어떤 파일이 남아야 하는지 독립 합성 모듈로 만들었다. 전체 구현과 실제 로컬 실행 결과, 테스트를 함께 공개한다. 원래 브라우저 프로그램과 고객 기록은 연결하지 않는다.

함수가 받는 자료를 작게 제한했다

save는 명시한 기존 신뢰 폴더와 key, phase와 sample만 있는 합성 레코드를 받는다. key는 제한된 문자와 길이이며 경로 구분자와 확장자를 마음대로 넣을 수 없다. 레코드도 허용한 상태와 길이의 텍스트만 받는다. 잘못된 입력은 임시파일을 만들기 전에 거절한다.

이 형식은 원래 제품의 영수증을 복제한 것이 아니다. 계정과 탭, 원고와 결과 주소는 없다. 표준 Python 코드로 파일 정리의 판단을 설명하기 위해 새로 만든 작은 계약이다. 실제 업무 필드를 추가한다면 각 값의 공개 범위와 검증, 저장 필요성을 다시 정해야 한다.

생성 사실로 정리 권한을 결정한다

임시파일은 배타적 생성 모드로 연다. 같은 이름이 있으면 실패하며 created는 거짓인 채로 남는다. 실제로 파일을 만든 다음에만 created를 참으로 둔다. 오류가 나면 그때 만든 파일 한 개만 정리하고 다른 tmp나 기존 목적지는 지우지 않는다. 이름 검색으로 소유권을 추정하지 않은 것이다.

시험은 같은 임시 이름 충돌과 unrelated.tmp 보존을 따로 확인한다. 이 코드는 폴더 전체를 정리하는 청소기가 아니며 save 안에 디렉터리 삭제 기능도 없다. 호출자가 신뢰하는 폴더와 그 상위 경로를 관리해야 한다. 공격자가 경로를 바꾸는 환경까지 방어했다고 설명하지 않는다.

쓰기 이후 문맥을 다시 확인한다

직렬화한 바이트를 쓰고 flush와 파일 sync, 닫기를 마친 뒤 check를 다시 호출한다. 아직 같은 작업을 진행해도 되는지 호출자가 확인하는 자리다. 통과했을 때만 같은 폴더 안의 목적지로 교체한다. 문맥 확인에서 실패하면 이전 파일의 바이트가 남는지 테스트한다.

check는 실제 로그인과 권한을 검사하는 서버가 아니다. 호출자가 제공하는 협력적 검사 함수다. 여러 작성자의 갱신 충돌을 막는 revision과 잠금도 구현하지 않았다. 이번 예제가 제공하는 단일 파일의 실패 경계와 기존 데이터베이스·권한 모듈의 책임을 섞지 않고 읽어야 한다.

코드와 구현 살펴보기

독립 합성 전체 구현

"""Synthetic single-file replacement in an explicitly trusted private directory."""
from pathlib import Path
from uuid import uuid4
import json,re,os,hashlib

def save(root,key,record,check=lambda:None):
    if not isinstance(key,str) or re.fullmatch(r'[a-z][a-z0-9-]{0,39}',key) is None:
        raise ValueError('invalid key')
    if not isinstance(record,dict) or set(record)!={'phase','sample'}:
        raise ValueError('exact synthetic record fields required')
    if record['phase'] not in ('prepared','confirmed') or not isinstance(record['sample'],str) or not 1<=len(record['sample'])<=200:
        raise ValueError('bounded synthetic record required')
    data=json.dumps(record,ensure_ascii=False,sort_keys=True).encode('utf-8')
    root=Path(root)
    if not root.is_absolute() or not root.is_dir() or root.is_symlink():
        raise ValueError('explicit existing trusted directory required')
    target=root/(key+'.json');stage=root/('.'+key+'-'+uuid4().hex+'.tmp')
    created=False
    check()
    try:
        with stage.open('xb') as stream:
            created=True
            stream.write(data);stream.flush();os.fsync(stream.fileno())
        check()
        os.replace(stage,target);created=False
    except Exception as error:
        if created:
            try:stage.unlink(missing_ok=True)
            except Exception as cleanup:
                raise ExceptionGroup('save and owned-stage cleanup failed',[error,cleanup]) from None
        raise
    return {'key':key,'payload_sha256':hashlib.sha256(data).hexdigest(),'bytes':len(data),'state':'stored'}

실제 파일로 실패 후 상태를 확인했다

목적지 이름의 디렉터리로 실제 교체 오류를 만들고 peer 파일과 목적지가 그대로인지 읽었다. key 오류와 빈 레코드, 쓰기 전 취소와 쓰기 후 취소도 검사한다. 정리 실패는 강제로 주입해 원래 오류와 PermissionError가 함께 남는지 확인한다. 모든 상황을 같은 모의 성공으로 처리하지 않았다.

총 12개 테스트가 소스와 ZIP 추출 폴더에서 각각 통과했다. 테스트는 패키지 옆의 소유한 하위 폴더에만 파일을 만들고 그 폴더가 지정한 시험 루트 안인지 확인한 뒤 정리한다. 운영체제 기본 임시 폴더와 실제 사용자 다운로드를 시험 자료로 쓰지 않는다.

선택기는 저장된 기록만 읽는다

아래 선택기는 성공 저장과 쓰기 후 취소, 목적지 충돌과 잘못된 key의 실제 실행 기록을 보여 준다. 파일 보존 여부와 남은 임시 이름을 읽을 수 있지만 브라우저에서 Python을 실행하지 않는다. 방문자의 디스크를 검사하거나 파일을 지우는 기능도 없다. 실제로 바뀌는 것은 표시할 합성 기록의 선택뿐이다.

인라인 선택 코드도 두 언어에서 모든 시나리오를 DOM 대역으로 실행해 원본 records 값과 대조한다. 이는 실제 브라우저 렌더링과 키보드 사용성 검수는 아니다. 원래 앱의 화면과 실제 고객 계정도 연결하지 않았다. 실행 기록과 인터랙티브 표시를 같은 파일 쓰기 기능으로 보이지 않게 설명한다.

저장된 합성 결과를 선택해 읽는 뷰어

보장하지 않는 경계까지 함께 내려받는다

ZIP에는 전체 코어와 12개 테스트, 실행기와 records, 사용 안내가 있다. Python 3.11 이상에서 실행하며 수정·재사용할 수 있다. 강제 종료와 전원 차단 뒤 자동 복구, 모든 파일 시스템의 내구성과 디렉터리 동기화는 보장하지 않는다. 동시 갱신과 인증, 백업도 별도 기능이다.

기존 SQLite 원자 처리 모듈을 이 코드로 바꾸라는 뜻도 아니다. 자료가 놓인 저장소와 실패 경계가 다르면 필요한 검수도 달라진다. 나는 새 모듈이 할 수 있는 일과 다음 작업자가 다시 판단할 부분을 함께 남겼다. 단일 저장 함수가 모든 운영 문제를 끝냈다는 인상을 주지 않는 것이 재사용 안내의 일부다.

코드·테스트·실행 기록 ZIP 받기
독립 합성 코드 · 수정과 재사용 가능