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