6

I'm coding a small website with Python and CGI where users can upload zip files and download files uploaded by other users. Currently I'm able to upload correctly the zip's, but I'm having some trouble to correctly send files to the user. My first approach was:

file = open('../../data/code/' + filename + '.zip','rb')

print("Content-type: application/octet-stream")
print("Content-Disposition: filename=%s.zip" %(filename))
print(file.read())

file.close()

But soon I realized that I had to send the file as binary, so I tried:

print("Content-type: application/octet-stream")
print("Content-Disposition: filename=%s.zip" %(filename))
print('Content-transfer-encoding: base64\r')
print( base64.b64encode(file.read()).decode(encoding='UTF-8') )

And different variants of it. It just doesn't works; Apache raises "malformed header from script" error, so I guess I should encode the file in some other way.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
dvilela
  • 1,200
  • 12
  • 29

3 Answers3

6

You need to print an empty line after the headers, and you Content-disposition header is missing the type (attachment):

print("Content-type: application/octet-stream")
print("Content-Disposition: attachment; filename=%s.zip" %(filename))
print()

You may also want to use a more efficient method of uploading the resulting file; use shutil.copyfileobj() to copy the data to sys.stdout.buffer:

from shutil import copyfileobj
import sys

print("Content-type: application/octet-stream")
print("Content-Disposition: attachment; filename=%s.zip" %(filename))
print()

with open('../../data/code/' + filename + '.zip','rb') as zipfile:
    copyfileobj(zipfile, sys.stdout.buffer)

You should not use print() for binary data in any case; all you get is b'...' byte literal syntax. The sys.stdout.buffer object is the underlying binary I/O buffer, copy binary data directly to that.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 1
    I'm using Python 2.7, which may make a difference, but to get this code to work I had to change the final line `copyfileobj(zipfile, sys.stdout)`. – Daniel Griscom Jul 19 '15 at 20:14
  • @DanielGriscom: yes, that's entirely correct; my answer was written for Python 3 where the `io` library takes care of all I/O and `sys.stdout.buffer` gives access to the underlying [`io.BufferedIOBase` implementation](https://docs.python.org/3/library/io.html#io.BufferedIOBase), rather than the (encoding) [`io.TextIOBase`](https://docs.python.org/3/library/io.html#io.TextIOBase); in Python 2 the `sys.stdout` file object accepts encoded bytes directly. – Martijn Pieters Jul 19 '15 at 21:47
  • 1
    Working in python 2, I had to remove the `print()` on the third line as well as change change the last line to `copyfileobj(zipfile, sys.stdout)` as @DanielGriscom suggested above. – eeScott Jan 18 '20 at 21:13
  • @eeScott yes, because this question is specifically tagged with [tag:python-3.x]. In Python 2, `sys.stdout` is a plain Python 2 file object, not an `io.TextIOWrapper()` object. – Martijn Pieters Jan 18 '20 at 21:16
  • 1
    Agreed, I just wanted to document it for the next person because there aren't that many people with this question tailored to python 2... – eeScott Jan 18 '20 at 21:17
5

The header is malformed because, for some reason, Python sends it after sending the file.

What you need to do is flush stdout right after the header:

sys.stdout.flush()

Then put the file copy

Russia Must Remove Putin
  • 374,368
  • 89
  • 403
  • 331
Alecz
  • 59
  • 1
  • 1
3

This is what worked for me, I am running Apache2 and loading this script via cgi. Python 3 is my language.

You may have to replace first line with your python 3 bin path.

#!/usr/bin/python3
import cgitb
import cgi
from zipfile import ZipFile
import sys

# Files to include in archive
source_file = ["/tmp/file1.txt", "/tmp/file2.txt"]

# Name and Path to our zip file.
zip_name = "zipfiles.zip"
zip_path = "/tmp/{}".format(zip_name)

with ZipFile( zip_path,'w' ) as zipFile:
    for f in source_file:
        zipFile.write(f);

# Closing File.
zipFile.close()

# Setting Proper Header.
print ( 'Content-Type:application/octet-stream; name="{}"'.format(zip_name) );
print ( 'Content-Disposition:attachment; filename="{}"\r\n'.format(zip_name) );

# Flushing Out stdout.
sys.stdout.flush()

bstdout = open(sys.stdout.fileno(), 'wb', closefd=False)
file = open(zip_path,'rb')
bstdout.write(file.read())
bstdout.flush()