Greatly improve zip endpoint with python-zipstream

master
voussoir 2016-12-20 17:44:22 -08:00
parent d5bc65c8f2
commit b5294431aa
6 changed files with 80 additions and 18 deletions

View File

@ -11,6 +11,8 @@ except converter.ffmpeg.FFMpegError:
traceback.print_exc() traceback.print_exc()
ffmpeg = None ffmpeg = None
FILENAME_BADCHARS = '\\/:*?<>|"'
ALLOWED_ORDERBY_COLUMNS = [ ALLOWED_ORDERBY_COLUMNS = [
'extension', 'extension',
'width', 'width',

View File

@ -5,6 +5,7 @@ import mimetypes
import os import os
import random import random
import warnings import warnings
import zipstream
import constants import constants
import decorators import decorators
@ -14,10 +15,6 @@ import jsonify
import phototagger import phototagger
import sessions import sessions
# pip install
# https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
from voussoirkit import webstreamzip
site = flask.Flask(__name__) site = flask.Flask(__name__)
site.config.update( site.config.update(
SEND_FILE_MAX_AGE_DEFAULT=180, SEND_FILE_MAX_AGE_DEFAULT=180,
@ -270,14 +267,34 @@ def get_album_json(albumid):
return jsonify.make_json_response(album) return jsonify.make_json_response(album)
@site.route('/album/<albumid>.tar') @site.route('/album/<albumid>.zip')
def get_album_tar(albumid): def get_album_zip(albumid):
album = P_album(albumid) album = P_album(albumid)
photos = list(album.walk_photos())
zipname_map = {p.real_filepath: '%s - %s' % (p.id, p.basename) for p in photos} recursive = request.args.get('recursive', True)
streamed_zip = webstreamzip.stream_tar(zipname_map) recursive = helpers.truthystring(recursive)
#content_length = sum(p.bytes for p in photos) arcnames = helpers.album_zip_filenames(album, recursive=recursive)
outgoing_headers = {'Content-Type': 'application/octet-stream'}
streamed_zip = zipstream.ZipFile()
for (real_filepath, arcname) in arcnames.items():
streamed_zip.write(real_filepath, arcname=arcname)
#if album.description:
# streamed_zip.writestr(
# arcname='%s.txt' % album.id,
# data=album.description.encode('utf-8'),
# )
if album.title:
download_as = '%s - %s.zip' % (album.id, album.title)
else:
download_as = '%s.zip' % album.id
download_as = download_as.replace('"', '\\"')
outgoing_headers = {
'Content-Type': 'application/octet-stream',
'Content-Disposition': 'attachment; filename=%s' % download_as,
}
return flask.Response(streamed_zip, headers=outgoing_headers) return flask.Response(streamed_zip, headers=outgoing_headers)
@ -325,8 +342,6 @@ def get_file(photoid):
else: else:
download_as = photo.id + '.' + photo.extension download_as = photo.id + '.' + photo.extension
## Sorry, but otherwise the attachment filename gets terminated
#download_as = download_as.replace(';', '-')
download_as = download_as.replace('"', '\\"') download_as = download_as.replace('"', '\\"')
response = flask.make_response(send_file(photo.real_filepath)) response = flask.make_response(send_file(photo.real_filepath))
response.headers['Content-Disposition'] = 'attachment; filename="%s"' % download_as response.headers['Content-Disposition'] = 'attachment; filename="%s"' % download_as

View File

@ -9,6 +9,35 @@ import exceptions
from voussoirkit import bytestring from voussoirkit import bytestring
def album_zip_filenames(album, recursive=True):
'''
Given an album, produce a dictionary mapping local filepaths to the filenames
that will appear inside the zip archive.
This includes creating subfolders for sub albums.
If a photo appears in multiple albums, only the first is used.
'''
if album.title:
root_folder = '%s - %s' % (album.id, normalize_filepath(album.title))
else:
root_folder = '%s' % album.id
photos = album.photos()
arcnames = {}
for photo in photos:
photo_name = '%s - %s' % (photo.id, photo.basename)
arcnames[photo.real_filepath] = os.path.join(root_folder, photo_name)
if recursive:
for child_album in album.children():
child_arcnames = album_zip_filenames(child_album)
for (filepath, arcname) in child_arcnames.items():
if filepath in arcnames:
continue
arcname = os.path.join(root_folder, arcname)
arcnames[filepath] = arcname
return arcnames
def chunk_sequence(sequence, chunk_length, allow_incomplete=True): def chunk_sequence(sequence, chunk_length, allow_incomplete=True):
''' '''
Given a sequence, divide it into sequences of length `chunk_length`. Given a sequence, divide it into sequences of length `chunk_length`.
@ -132,14 +161,20 @@ def is_xor(*args):
''' '''
return [bool(a) for a in args].count(True) == 1 return [bool(a) for a in args].count(True) == 1
def normalize_filepath(filepath): def normalize_filepath(filepath, allowed=''):
''' '''
Remove some bad characters. Remove some bad characters.
''' '''
badchars = constants.FILENAME_BADCHARS
for character in allowed:
badchars = badchars.replace(allowed, '')
filepath = remove_control_characters(filepath)
badchars = dict.fromkeys(badchars)
filepath = filepath.translate(badchars)
filepath = filepath.replace('/', os.sep) filepath = filepath.replace('/', os.sep)
filepath = filepath.replace('\\', os.sep) filepath = filepath.replace('\\', os.sep)
filepath = filepath.replace('<', '')
filepath = filepath.replace('>', '')
return filepath return filepath
def now(timestamp=True): def now(timestamp=True):
@ -171,6 +206,15 @@ def read_filebytes(filepath, range_min, range_max, chunk_size=2 ** 20):
yield chunk yield chunk
sent_amount += len(chunk) sent_amount += len(chunk)
def remove_control_characters(text):
'''
Thanks SilentGhost
http://stackoverflow.com/a/4324823
'''
kill = dict.fromkeys(range(32))
text = text.translate(kill)
return text
def seconds_to_hms(seconds): def seconds_to_hms(seconds):
''' '''
Convert integer number of seconds to an hh:mm:ss string. Convert integer number of seconds to an hh:mm:ss string.

View File

@ -992,7 +992,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin, PDBUserMixin):
data_directory = constants.DEFAULT_DATADIR data_directory = constants.DEFAULT_DATADIR
# DATA DIR PREP # DATA DIR PREP
data_directory = helpers.normalize_filepath(data_directory) data_directory = helpers.normalize_filepath(data_directory, allowed='/\\')
self.data_directory = os.path.abspath(data_directory) self.data_directory = os.path.abspath(data_directory)
os.makedirs(self.data_directory, exist_ok=True) os.makedirs(self.data_directory, exist_ok=True)

View File

@ -2,5 +2,6 @@ bcrypt
flask flask
gevent gevent
pillow pillow
zipstream
https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
git+https://github.com/senko/python-video-converter.git git+https://github.com/senko/python-video-converter.git

View File

@ -47,7 +47,7 @@ p
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
<span><a href="/album/{{album.id}}.tar">(download .tar)</a></span> <span><a href="/album/{{album.id}}.zip">(download .zip)</a></span>
{% set photos = album.photos() %} {% set photos = album.photos() %}
{% if photos %} {% if photos %}
<h3>Photos</h3> <h3>Photos</h3>