Improve & generalize zipfile code.

Moved some heavy lifting out of the flask album.zip endpoint
and into helpers.py.
Renamed some things for clarity.
master
voussoir 2018-08-14 22:58:26 -07:00
parent 53c86c30a1
commit bc6a0aa907
2 changed files with 67 additions and 35 deletions

View File

@ -10,6 +10,7 @@ import mimetypes
import os import os
import PIL.Image import PIL.Image
import unicodedata import unicodedata
import zipstream
from . import constants from . import constants
from . import exceptions from . import exceptions
@ -17,11 +18,13 @@ from . import exceptions
from voussoirkit import bytestring from voussoirkit import bytestring
from voussoirkit import pathclass from voussoirkit import pathclass
def album_zip_directories(album, recursive=True): def album_as_directory_map(album, recursive=True):
''' '''
Given an album, produce a dictionary mapping Album objects to directory Given an album, produce a dictionary mapping Album objects to directory
names as they will appear inside the zip archive. names as they will appear inside the zip archive.
Sub-albums become subfolders. Sub-albums become subfolders.
If an album is a child of multiple albums, only one instance is used.
''' '''
directories = {} directories = {}
if album.title: if album.title:
@ -33,30 +36,28 @@ def album_zip_directories(album, recursive=True):
directories[album] = root_folder directories[album] = root_folder
if recursive: if recursive:
for child_album in album.get_children(): for child_album in album.get_children():
child_directories = album_zip_directories(child_album, recursive=True) child_directories = album_as_directory_map(child_album, recursive=True)
for (child_album, child_directory) in child_directories.items(): for (child_album, child_directory) in child_directories.items():
child_directory = os.path.join(root_folder, child_directory) child_directory = os.path.join(root_folder, child_directory)
directories[child_album] = child_directory directories[child_album] = child_directory
return directories return directories
def album_zip_filenames(album, recursive=True): def album_photos_as_filename_map(album, recursive=True):
''' '''
Given an album, produce a dictionary mapping local filepaths to the Given an album, produce a dictionary mapping Photo objects to the
filenames that will appear inside the zip archive. filenames that will appear inside the zip archive.
This includes creating subfolders for sub albums. This includes creating subfolders for sub albums.
If a photo appears in multiple albums, only the first is used. If a photo appears in multiple albums, only one instance is used.
''' '''
arcnames = {} arcnames = {}
directories = album_zip_directories(album, recursive=recursive) directories = album_as_directory_map(album, recursive=recursive)
for (album, directory) in directories.items(): for (album, directory) in directories.items():
photos = album.get_photos() photos = album.get_photos()
for photo in photos: for photo in photos:
filepath = photo.real_path.absolute_path
if filepath in arcnames:
continue
photo_name = f'{photo.id} - {photo.basename}' photo_name = f'{photo.id} - {photo.basename}'
arcnames[filepath] = os.path.join(directory, photo_name) arcnames[photo] = os.path.join(directory, photo_name)
return arcnames return arcnames
@ -495,6 +496,57 @@ def truthystring(s):
return None return None
return False return False
def zip_album(album, recursive=True):
'''
Given an album, return a zipstream zipfile that contains the album's
photos (recursive = include childen's photos) organized into folders
for each album. Each album folder also gets a text file containing
the album's name and description if applicable.
If an album is a child of multiple albums, only one instance is used.
'''
zipfile = zipstream.ZipFile()
# Add the photos.
arcnames = album_photos_as_filename_map(album, recursive=recursive)
for (photo, arcname) in arcnames.items():
zipfile.write(filename=photo.real_path.absolute_path, arcname=arcname)
# Add the album metadata as an {id}.txt file within each directory.
directories = album_as_directory_map(album, recursive=recursive)
for (inner_album, directory) in directories.items():
metafile_text = []
if inner_album.title:
metafile_text.append(f'Title: {inner_album.title}')
if inner_album.description:
metafile_text.append(f'Description: {inner_album.description}')
if not metafile_text:
continue
metafile_text = '\r\n\r\n'.join(metafile_text)
metafile_text = metafile_text.encode('utf-8')
metafile_name = f'album {inner_album.id}.txt'
metafile_name = os.path.join(directory, metafile_name)
zipfile.writestr(
arcname=metafile_name,
data=metafile_text,
)
return zipfile
def zip_photos(photos):
'''
Given some photos, return a zipstream zipfile that contains the files.
'''
zipfile = zipstream.ZipFile()
for photo in photos:
arcname = os.path.join('photos', f'{photo.id} - {photo.basename}')
zipfile.write(filename=photo.real_path.absolute_path, arcname=arcname)
return zipfile
_numerical_characters = set('0123456789.') _numerical_characters = set('0123456789.')
def _unitconvert(value): def _unitconvert(value):

View File

@ -45,40 +45,20 @@ def get_album_zip(album_id):
recursive = request.args.get('recursive', True) recursive = request.args.get('recursive', True)
recursive = etiquette.helpers.truthystring(recursive) recursive = etiquette.helpers.truthystring(recursive)
arcnames = etiquette.helpers.album_zip_filenames(album, recursive=recursive) streamed_zip = etiquette.helpers.zip_album(album, recursive=recursive)
streamed_zip = zipstream.ZipFile()
for (real_filepath, arcname) in arcnames.items():
streamed_zip.write(real_filepath, arcname=arcname)
# Add the album metadata as an {id}.txt file within each directory.
directories = etiquette.helpers.album_zip_directories(album, recursive=recursive)
for (inner_album, directory) in directories.items():
text = []
if inner_album.title:
text.append('Title: ' + inner_album.title)
if inner_album.description:
text.append('Description: ' + inner_album.description)
if not text:
continue
text = '\r\n\r\n'.join(text)
streamed_zip.writestr(
arcname=os.path.join(directory, 'album %s.txt' % inner_album.id),
data=text.encode('utf-8'),
)
if album.title: if album.title:
download_as = 'album %s - %s.zip' % (album.id, album.title) download_as = f'album {album.id} - {album.title}.zip'
else: else:
download_as = 'album %s.zip' % album.id download_as = f'album {album.id}.zip'
download_as = etiquette.helpers.remove_path_badchars(download_as) download_as = etiquette.helpers.remove_path_badchars(download_as)
download_as = urllib.parse.quote(download_as) download_as = urllib.parse.quote(download_as)
outgoing_headers = { outgoing_headers = {
'Content-Type': 'application/octet-stream', 'Content-Type': 'application/octet-stream',
'Content-Disposition': 'attachment; filename*=UTF-8\'\'%s' % download_as, 'Content-Disposition': f'attachment; filename*=UTF-8\'\'{download_as}',
} }
return flask.Response(streamed_zip, headers=outgoing_headers) return flask.Response(streamed_zip, headers=outgoing_headers)
# Album photo operations ########################################################################### # Album photo operations ###########################################################################