From bea9f905bd905d61b5c7947f38c9fc57ec69e6c9 Mon Sep 17 00:00:00 2001 From: Ethan Dalool Date: Tue, 14 Aug 2018 23:02:06 -0700 Subject: [PATCH] Support downloading .zip of arbitrary photos, clipboard. Now that creating zips of any photo set is easier, we can let the user download whatever is on their clipboard. --- etiquette/helpers.py | 12 ++++ .../endpoints/photo_endpoints.py | 56 +++++++++++++++++++ .../etiquette_flask/templates/clipboard.html | 31 +++++++++- 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/etiquette/helpers.py b/etiquette/helpers.py index abc63a7..e53ef1d 100644 --- a/etiquette/helpers.py +++ b/etiquette/helpers.py @@ -232,6 +232,18 @@ def hash_file(filepath, hasher): def hash_file_md5(filepath): return hash_file(filepath, hasher=hashlib.md5()) +def hash_photoset(photos): + ''' + Given some photos, return a fingerprint string for that particular set. + ''' + hasher = hashlib.md5() + + photo_ids = sorted(set(p.id for p in photos)) + for photo_id in photo_ids: + hasher.update(photo_id.encode('utf-8')) + + return hasher.hexdigest() + def hyphen_range(s): ''' Given a string like '1-3', return numbers (1, 3) representing lower diff --git a/frontends/etiquette_flask/etiquette_flask/endpoints/photo_endpoints.py b/frontends/etiquette_flask/etiquette_flask/endpoints/photo_endpoints.py index cbc6aa1..24bdc8e 100644 --- a/frontends/etiquette_flask/etiquette_flask/endpoints/photo_endpoints.py +++ b/frontends/etiquette_flask/etiquette_flask/endpoints/photo_endpoints.py @@ -5,12 +5,15 @@ import urllib.parse import etiquette +from voussoirkit import cacheclass + from .. import common from .. import decorators from .. import jsonify site = common.site session_manager = common.session_manager +photo_download_zip_tokens = cacheclass.Cache(maxlen=100) # Individual photos ################################################################################ @@ -226,6 +229,59 @@ def post_batch_photos_photo_cards(): response = jsonify.make_json_response(divs) return response +# Zipping ########################################################################################## + +@site.route('/batch/photos/download_zip/', methods=['GET']) +def get_batch_photos_download_zip(zip_token): + ''' + After the user has generated their zip token, they can retrieve + that zip file. + ''' + zip_token = zip_token.split('.')[0] + try: + photo_ids = photo_download_zip_tokens[zip_token] + except KeyError: + flask.abort(404) + + # Let's re-validate those IDs just in case anything has changed. + photos = list(common.P_photos(photo_ids, response_type='json')) + if not photos: + flask.abort(400) + + streamed_zip = etiquette.helpers.zip_photos(photos) + download_as = zip_token + '.zip' + download_as = urllib.parse.quote(download_as) + + outgoing_headers = { + 'Content-Type': 'application/octet-stream', + 'Content-Disposition': f'attachment; filename*=UTF-8\'\'{download_as}', + } + return flask.Response(streamed_zip, headers=outgoing_headers) + +@site.route('/batch/photos/download_zip', methods=['POST']) +@decorators.required_fields(['photo_ids'], forbid_whitespace=True) +def post_batch_photos_download_zip(): + ''' + Initiating file downloads via POST requests is a bit clunky and unreliable, + so the way this works is we generate a token representing the photoset + that they want, and then they can retrieve the zip itself via GET. + ''' + photo_ids = request.form['photo_ids'] + photo_ids = etiquette.helpers.comma_space_split(photo_ids) + + photos = list(common.P_photos(photo_ids, response_type='json')) + if not photos: + flask.abort(400) + + photo_ids = [p.id for p in photos] + + zip_token = etiquette.helpers.hash_photoset(photos) + photo_download_zip_tokens[zip_token] = photo_ids + + response = {'zip_token': zip_token} + response = jsonify.make_json_response(response) + return response + # Search ########################################################################################### def get_search_core(): diff --git a/frontends/etiquette_flask/templates/clipboard.html b/frontends/etiquette_flask/templates/clipboard.html index a6300ca..a552192 100644 --- a/frontends/etiquette_flask/templates/clipboard.html +++ b/frontends/etiquette_flask/templates/clipboard.html @@ -43,12 +43,13 @@ body grid-area: right; display: grid; - grid-template-rows: 75px 75px 75px 75px auto; + grid-template-rows: 75px 75px 75px 75px 75px auto; grid-template-areas: "add_tag_area" "remove_tag_area" "refresh_metadata_area" "searchhidden_area" + "download_zip_area" "message_area"; background-color: rgba(0, 0, 0, 0.1); @@ -73,6 +74,11 @@ body grid-area: searchhidden_area; margin: auto; } +#download_zip_area +{ + grid-area: download_zip_area; + margin: auto; +} #message_area { grid-area: message_area; @@ -112,6 +118,10 @@ body +
+ +
+
@@ -262,6 +272,25 @@ function add_remove_callback(response) common.create_message_bubble(message_area, message_positivity, message_text, 8000); } +function submit_download_zip(callback) +{ + if (photo_clipboard.clipboard.size == 0) + {return;} + + var url = "/batch/photos/download_zip"; + var photo_ids = Array.from(photo_clipboard.clipboard).join(","); + var data = new FormData(); + data.append("photo_ids", photo_ids); + common.post(url, data, callback); +} + +function download_zip_callback(response) +{ + var zip_token = response["data"]["zip_token"]; + var url = `/batch/photos/download_zip/${zip_token}.zip`; + window.location.href = url; +} + var refresh_in_progress = false; function submit_refresh_metadata(callback) {