본문 바로가기

취미코딩/python

selenium에서 dropzone에 파일 업로드하기

dropzonejs는 바탕화면이나 탐색기에서 파일을 드래그 앤 드롭해 업로드할 수 있게 해주는 유명한 라이브러리다. 보통 프로필 사진 등을 설정할 때 유용하게 쓴다.

오래전에 구현해서 가물가물하지만, 파일을 떨어트리면 설정한 경로로 파일을 보내고, 서버에서 파일을 정상적으로 저장하면 응답을 받아서 애초 form의 file input에 다시 처리하는 흐름이었던 것 같다.

로컬 파일을 브라우저에서 열 수는 있겠지만(pdf나 미디어 파일) 드래그 앤 드롭을 구현하기는 힘들 것 같았다. 드롭존을 클릭하면 파일 탐색기가 열릴텐데 headless 환경에서도 가능할까. 만약에 올리고 싶은 데이터가 파일 형태가 아니라면 또 어떻게 되는 것일까.

구글링을 해보니 비슷한 궁금증을 품은 사람들이 제법 있었는데, 원리는 비슷하다.

  1. Dropzone에 파일을 떨어트리면 addfile function을 실행시키도록 되어 있다.
  2. addfile의 인자로 받는 파일은 자바스크립트의 File 객체다.
  3. 그러니 자바스크립트 상으로 File 객체를 형성해서, addfile을 직접 실행시키면 드래그 앤 드롭 없이도 업로드가 될 것이다.

그러면 드래그 앤 드롭으로 떨어트린 파일을 드롭존이 File 객체로 바꿔주는 작업을 대신하면 된다는 얘기로 들린다.

File 객체의 명세를 봐도 알아들을 수 있는 용어는 몇개 되지 않는다. 'File 객체는 특정 종류의 Blob'이라는 말이 눈에 들어온다. 구글링한 해결책 중에, dataURI를 Blob으로 변환해 File 객체로 만드는 제안이 있었다.

테스트해볼 드롭존은 이미지 mimetype만 받고, 올리려던 이미지 파일도 pillow로 만든 png여서, 이미지를 base64로 인코딩해 넘기면 될 것 같다. 그리고 selenium에서 Blob으로 변환해 File 객체를 만들고 드롭존의 addfile을 실행하는 스크립트를 실행하면 된다.

def addFileToDropzone(driver, css_selector, name, content):
    """
    Trigger a file add with `name` and `content` to Dropzone element at `css_selector`.
    """
    script = """
var dropzone_instance = Dropzone.forElement(css_selector);
var bstr = atob('%s');
var n = bstr.length;
u8arr = new Uint8Array(n);

while(n--){
    u8arr[n] = bstr.charCodeAt(n);
}
var new_file = new File([u8arr], '%s', {type:'image/png'})
dropzone_instance.addFile(new_file)
""" % (content, name)
    driver.execute_script(script)

평소 보는 base64 이미지 문자열은 앞에 mimetype 등이 들어가 있지만 난 파일을 바로 변환할 것이기 때문에, Blob으로 전환하는 과정에서 해당 문자열을 분리하는 스크립트는 제외했다. python에서 이미지를 base64로 인코딩하는 건

import base64

with open({filepath}, 'rb') as img:
    base64img = base64.b64encode(img.read())

imgdata = base64img.decode('utf-8')

막줄을 빼먹으면 안된다. UTF-8으로 인코딩해야 File로 받을 수 있다고 한다. 이걸 잘 몰라서 base64 스트링을 무작정 str type으로 변환시켜봤는데, 드롭존 업로드는 제대로 되지만 리턴된 이미지 정보를 form에 실어서 다시 post하면 서버에서 파일에 오류가 있는 것으로 인지했다(PHP 기준), 실제로 UTF-8으로 인코딩한 문자열과 어느 부분이 다른지 비교해봤더니 마지막 2글자가 달랐다.

mimetype이 그때그때 달라진다면, 파이썬 스크립트 안에서 mimetype을 따로 인자로 넘겨주던가, 아니면 python-dataURI와 같은 패키지를 설치해 인코딩하고 자바스크립트에서 mimetype을 분리하는 작업을 추가하면 된다.