주문 자료를 엑셀로 만들어 ZIP 으로 묶어 내려받게 했고 파일명에 상품명이 들어갔다. 받아서 풀면 파일명이 깨진다는 문의가 왔다.
Table of contents
Open Table of contents
증상 — 파일이 아니라 해석이 달랐다
서버에서 만든 압축 파일부터 확인했다.
$ unzip -l order_20140205.zip
Length Date Time Name
15420 02-05-2014 09:12 íì´ì´_주문ë´ì.xlsx
리눅스에서 풀어도 깨진다. 만들 때부터 깨진 것으로 보였다. 그런데 인코딩을 지정해서 다시 뽑으니 정상이었다.
$ unzip -O cp949 -l order_20140205.zip
15420 02-05-2014 09:12 타이어_주문내역.xlsx
파일이 깨진 것이 아니라 푸는 쪽이 인코딩을 짐작하고 있었다.
ZIP 은 파일명 바이트만 담고 어느 인코딩인지는 담지 않는다. 그래서 윈도우 기본 도구는 시스템 기본 인코딩으로, 리눅스 도구는 UTF-8 로 해석한다. 만든 쪽이 무엇으로 썼는지 알릴 자리가 없었다.
표시 비트가 있었지만
ZIP 에는 파일명이 UTF-8 이라고 표시하는 비트가 있다. 그것을 켜면 푸는 쪽이 짐작하지 않아도 된다.
$zip = new ZipArchive();
$zip->open($path, ZipArchive::CREATE);
$zip->addFile($src, $name);
문제는 ZipArchive 가 기본으로는 그 비트를 안 켠다는 점이었다. 우리가 쓰는 판에는 그것을 켜는 설정 자체가 없었다. 라이브러리를 바꾸는 것은 이 문제 하나로 하기에는 컸다.
압축 안 이름을 영문으로 바꿨다
가장 확실한 방법은 ZIP 안의 파일명에 한글을 안 쓰는 것이었다.
$name = 'order_' . $orderNo . '_' . date('Ymd') . '.xlsx';
주문 번호와 날짜로 이름을 만드니 어느 도구로 풀어도 안 깨졌다. 상품명은 파일 안의 내용으로 넣었다.
파일 안의 내용은 인코딩 문제가 없어서 한글을 그대로 쓸 수 있었다. 이름은 기계가 다루고 내용은 사람이 본다는 것으로 나눈 셈이다. 이 구분이 다른 자리에서도 그대로 쓸 만했다.
내려받기 이름도 같은 문제였다
브라우저에 알려 주는 이름에도 같은 문제가 있었다.
header('Content-Disposition: attachment; filename="' . $filename . '"');
여기에 한글을 넣으면 브라우저마다 다르게 처리한다. 그래서 두 가지를 같이 보냈다.
$fallback = 'order_' . date('Ymd') . '.zip';
$utf8 = rawurlencode('주문내역_' . date('Ymd') . '.zip');
header("Content-Disposition: attachment; filename=\"{$fallback}\"; filename*=UTF-8''{$utf8}");
앞의 filename 은 옛 브라우저용 영문 이름이고 뒤의 filename* 이 인코딩을 명시한 이름이다. 규격상 둘 다 알아듣는 브라우저는 filename* 쪽을 쓴다.
확인할 환경도 목록으로 적었다.
만드는 쪽 서버 (리눅스, UTF-8)
푸는 쪽
- 윈도우 기본 도구
- 윈도우 별도 압축 프로그램
- 맥 기본 도구
- 리눅스 명령줄
넷 다 확인했고 영문 파일명으로 바꾼 뒤에는 전부 정상이었다. 한 환경에서만 확인하면 다른 환경의 문제를 못 본다.
파일명에 한글이 들어가는 다른 자리도 찾았다.
$ grep -rn "filename\|->addFile\|move_uploaded_file" --include=*.php application/
업로드 저장과 로그 파일명은 이미 영문이었고 메일 첨부에 같은 문제가 있어서 인코딩을 명시하는 방식으로 함께 고쳤다.
정리
ZIP은 파일명 인코딩을 담지 않는다- 푸는 프로그램마다 다르게 해석한다
- 파일이 깨진 것이 아니라 해석이 다를 수 있다. 다른 도구로 풀어 확인한다
UTF-8표시 비트를ZipArchive가 안 켤 수 있다- 압축 안 이름을 영문으로 하면 어디서든 안 깨진다
- 이름은 기계가 다루고 내용은 사람이 본다
- 내려받기 이름은
filename과filename*을 함께 보낸다 - 만드는 환경과 푸는 환경을 나눠 목록으로 확인한다
- 파일명에 한글이 들어가는 다른 자리도 찾아본다