docstring에 적힌 약속
스토어는 계약서 같은 문장으로 시작해. 파일이 DB 행보다 먼저 간다. 그래서 쓰다가 끊기면 아무도 안 가리키는 원본이 남을 수는 있어도, 커밋된 에셋에 원본이 빠져 있는 일은 절대 없다. 이건 실패 두 개 중에 하나를 고르는 얘기야. 하나는 행 없는 파일이야. 디스크가 조금 아깝고, 어느 화면에도 안 보이고, 해는 없어. 다른 하나는 파일 없는 행이야. 열어도 아무것도 안 나오는 라이브러리 카드, 발행할 수 없는 세트 프레임 같은 거지. 레퍼런스 프로필은 그 이미지를 여전히 목록에 올려 두는데, 바이트를 읽으려는 순간마다 503과 The original image is unavailable.가 돌아와. 원본 이미지를 쓸 수 없다는 뜻이야. Avatars는 해 없는 쪽을 골라. 방법은 일의 순서를 정하는 거고.
이름이 곧 체크섬
save_asset은 먼저 Pillow한테 이 바이트가 이미지이긴 하냐고 물어. verify()를 부르고, 바이트를 한 번 더 열어서 너비, 높이, 포맷을 읽어. Pillow 문서가 verify 다음엔 이미지를 다시 열라고 하거든. 확장자는 Pillow가 알아낸 포맷에서 나와. 올린 파일 이름은 안 봐. PNG, JPEG, WebP, GIF, HEIC만 받고, 나머지는 Use PNG, JPEG, WebP, GIF or HEIC.로 거절해. 이 다섯 가지 중에 하나로 올리라는 말이야.
이어서 바이트를 해시하고, 그 SHA-256이 파일 이름이 돼. <sha256>.png, <sha256>.heic 이런 식이야. 클립도 같은 규칙으로 <sha256>.mp4가 되고. 이름이 곧 내용이야. 같은 바이트를 두 번 올리면 같은 파일에 닿고, 어떤 파일이든 언제든 자기 이름과 맞춰 볼 수 있어.
한 번만 쓰고, 다시 쓸 땐 검사해
write_original의 docstring은 한 줄이야. 콘텐츠 주소로 이름 짓고 딱 한 번 쓴다, 이미 있는 파일은 여전히 맞아야 한다. 갈래는 둘인데, 어느 쪽도 이미 있는 원본을 쓰기용으로 열지 않아.
- 파일이 이미 있으면. 읽어서 해시하고 이름과 비교해. 맞으면 바이트는 이미 안전하니까 아무것도 안 써. 틀리면 디스크가 이름이 약속한 걸 더는 안 들고 있다는 뜻이야. 스토어는
Stored original failed its checksum.을 503으로 내고 거절해. 저장된 원본이 체크섬 검사에 떨어졌다는 말이야. 덮어써서 고치지도 않아. 난 이게 좋아. 조용히 덮어쓰면 디스크가 뭘 잃었다는 사실까지 묻히거든. - 새 파일이면. 옆에
.<uuid>.tmp라는 임시 파일을"xb"모드로 열어. 이 모드는 같은 경로에 뭐가 있으면 자르지 않고 그냥 실패해. 쓰고, 파이썬 버퍼를 비우고,os.fsync로 운영체제 캐시에서 바이트를 밀어내. 마지막으로os.replace로 임시 파일을 진짜 이름 자리에 옮겨. macOS나 Linux에선 이 이름 바꾸기가 원자적이야. 읽는 쪽은 파일이 없거나 통째로 있거나, 둘 중 하나만 봐.
틈마다 짚어 보는 순서
save_asset은 write_original이 돌아온 다음에야 add를 불러서 에셋 행을 넣어. 틈마다 엔진이 죽는다고 쳐 봐.
| 엔진이 죽는 때 | 남는 것 | 그 뒤 |
|---|---|---|
| 임시 파일을 쓰는 중 | 숨은 .tmp 파일 | 아무 일 없어. 가리키는 행이 없으니까 |
| 이름은 바꿨는데 삽입 커밋 전 | 행 없는 온전한 원본 | 눈에 띄는 건 없어. 같은 바이트를 다시 올리면 그 파일을 찾고, 해시를 확인하고, 그대로 써 |
| 삽입 커밋 뒤 | 원본과 그 행 | 라이브러리에 보여 |
이 표 어디에도 허공을 가리키는 행으로 끝나는 줄은 없어. 설계는 이게 전부고, 비용은 시스템 콜 두 번이야.
원본엔 다시는 손대지 않아
이미지를 바꾸는 것처럼 보이는 일은 전부 다른 곳에 새 파일을 만드는 일이야.
- 업로드 라우트는 최대 100 MB까지 읽어. 거기 달린 주석이 이게 뭔지 정확히 짚어. 전송 크기 한도일 뿐 크기 줄이기가 아니고, 원본은 올린 바이트 그대로 남는다.
- 폰에서 온 HEIC는 디스크에서도 HEIC야. 브라우저랑 생성기는
browser_image가 만든 무손실 PNG 파생본을 받아. 이 함수의 docstring은 보관된 바이트는 절대 안 바뀐다는 약속으로 끝나. 테스트도 파생본을 만들기 전과 후에 저장된 파일이 올린 것과 똑같은지 확인해. - 썸네일은
thumbnails/의 WebP 파일이야. 원본 해시로 이름 짓고, 처음 요청될 때 그려. - 클립의 포스터 프레임도 그 옆에 있어. 클립 docstring은 이걸 다시 만들 수 있는 파생본이라고 불러.
콘텐츠 주소에 규칙이 하나 더 얹혀 있어. 올린 바이트가 스토어가 이미 아는 에셋과 같으면, 새 에셋은 옛 에셋의 등장 대상을 전부 물려받고 그 에셋을 레퍼런스로 달아. 같은 파일을 다른 이름으로 올려도 누구 얼굴인지는 못 떼어 내. 그 혈통을 어떻게 따라가는지, 그게 왜 소울을 지키는지는 트랙 8에서 다뤄.
왜 이렇게까지 엄격하냐고? 생성된 이미지의 첫 사본은 Ember가 들고 있어. Ember 이력을 정리하다가 아빠가 고른 아바타까지 같이 쓸려 가지 않게, Avatars는 오래 가는 두 번째 사본을 따로 둬. 그 사본은 바이트가 진짜일 때만 값어치가 있어. 한 번만 쓰고 다시 쓸 때마다 검사해야 그 값어치가 지켜져.