[python] ZipFile을 이용해 .zip파일 안의 파일들 읽어오기

haremeat·2023년 3월 20일
0

Python

목록 보기
5/5
post-thumbnail

나는 rest api 만들 때 파일 형식을 form data 형식이 아닌 base64 기반으로 받는 편이다.

파일 필드 하나를 추가해서 base64 기반으로 받으려는데 확장자 제한이 필요했다.
문제는 허용된 확장자 중 zip 파일도 있었던 것.

이런 경우 zip 파일 안의 파일들도 검사하는 로직이 있어야 확장자 제한하는 맛이 있다.

zip 파일 안의 파일들 체크

zip_file = ZipFile(BytesIO(decoded_file))
zip_file_names = [zip_file_name for zip_file_name in zip_file.namelist()]

decoded_file 값은 byte 값으로 변환된 파일 값이다.
ZipFile class를 이용해 값을 넘기면 파일 이름들을 가져오는 namelist(), ZipInfo 객체를 포함하는 infolist() 등 해당 zip 파일 안의 정보들을 받아올 수 있다.
이 경우엔 확장자 체크만 하면 되니 위 코드처럼 namelist만 받아와서 반복문 돌리고 확장자 체크만 하면 된다.

zip 파일 안의 zip 파일 체크

하지만 그것만으로는 부족하다.
위 방법으로는 zip 파일 안에 zip 파일이 있을 경우
그 안에 허용되지 않은 파일이 포함되어 있어도 통과가 된다.
때문에 내가 해야 했던 일 두 가지는

  1. 재귀(recursion)를 사용할 것
  2. 재귀로 넘길 값(decoded_file) 찾기

zip 파일 안의 파일들 확장자만 체크하려면 그냥 namelist만 가져와서 거기서 확장자만 체크하면 되지만
zip 파일 안의 zip 파일이나 폴더를 체크하려면 재귀를 써야 한다.

문제는 ZipFile(BytesIO(decoded_file))를 사용해 해체한 파일들 중 어떤 걸 넘겨야 하는지 고민됐다.

namelist는 단순한 이름 나열 str이니 말이 안 되고
디버깅해보니 Zipinfo 리스트를 내뱉는 zip_file.NameToInfo 이런 객체가 있었는데 당연히 이걸 변수로 넘기면 제대로 된 zip file이 아니라고 오류 먹는다.

저걸 byte로 바꾼다고 해결이 될까..? 하던 차에
document를 뒤지니 나오는 read 함수

💡 ZipFile.read(namepwd=None)
Return the bytes of the file name in the archive. name is the name of the file in the archive, or a ZipInfo object. The archive must be open for read or append. pwd is the password used for encrypted files as a bytes object and, if specified, overrides the default password set with setpassword(). Calling read() on a ZipFile that uses a compression method other than ZIP_STORED, ZIP_DEFLATED, ZIP_BZIP2 or ZIP_LZMA will raise a NotImplementedError. An error will also be raised if the corresponding compression module is not available.

정확히 내가 찾던 메서드였다!

def zip_file_check(self, decoded_file):
  names_to_ignore = ['__MACOSX/']
  zip_file = ZipFile(BytesIO(decoded_file))
  zip_file_names = []

  for name_to_ignore in names_to_ignore:
      pass_file_names = [zip_file_name for zip_file_name in zip_file.namelist() if not str(zip_file_name).startswith(name_to_ignore)]
      zip_file_names.extend(pass_file_names)

  for zip_file_name in zip_file_names:
      zip_file_extension = Path(zip_file_name).suffix.replace('.', '')

      if zip_file_extension == 'zip':
          file_zip_info = zip_file.NameToInfo[zip_file_name]
          bytes_zip_file = zip_file.read(name=file_zip_info)
          self.zip_file_check(bytes_zip_file)
          
      ....

위처럼 파일 돌려서 나온 확장자가 zip일 경우 zip_file.NameToInfo[파일명]을 이용해 ZipInfo 객체를 가져온 후 그걸 read 메서드의 name 파라미터로 넘기면 된다.
그렇게 나온 byte값을 다시 재귀로 넘기면 함수는 다시 zip 파일 안의 zip 파일을 풀어서 확장자를 체크하는 함수가 완성되는 것...

확장자 체크해서 ValidationError 내는 부분의 코드는 생략했다.
여기까지만 봐도 다들 잘 알아서 쓸거라 믿습니다.

__MACOSX/로 시작하는 애들은 빼는 이유

namelist 안에는 맥에서 자동생성된 __MACOSX/ 폴더 안의 파일들이 같이 딸려온다.
확장자 검사만 할 때는 버그가 나지 않지만 zip_file.read() 부분에서 zip파일이라며? 아닌데? 요런 에러가 뜨기 때문에
어차피 쓸모도 없는 거 없애버립시다

profile
버그와 함께하는 삶

0개의 댓글