Improve & generalize zipfile code.
Moved some heavy lifting out of the flask album.zip endpoint and into helpers.py. Renamed some things for clarity.
This commit is contained in:
parent
53c86c30a1
commit
bc6a0aa907
2 changed files with 67 additions and 35 deletions
|
@ -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):
|
||||||
|
|
|
@ -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 ###########################################################################
|
||||||
|
|
Loading…
Reference in a new issue