Greatly improve zip endpoint with python-zipstream
This commit is contained in:
parent
d5bc65c8f2
commit
b5294431aa
6 changed files with 80 additions and 18 deletions
|
@ -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',
|
||||||
|
|
41
etiquette.py
41
etiquette.py
|
@ -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
|
||||||
|
|
50
helpers.py
50
helpers.py
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue