We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Journalists should be able to bulk download from document server
Right now, journalists can only download one file at a time, even if there are dozens new submissions in any given session. The New Yorker team asked if we can enable bulk downloading so that journalists can download multiple files at once. 


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `modules/deaddrop/files/deaddrop/journalist.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 import os
3 from datetime import datetime
4 import uuid
5 
6 from flask import Flask, request, render_template, send_file
7 from flask_wtf.csrf import CsrfProtect
8 
9 import config, version, crypto, store, background
10 
11 app = Flask(__name__, template_folder=config.JOURNALIST_TEMPLATES_DIR)
12 app.secret_key = config.SECRET_KEY
13 
14 app.jinja_env.globals['version'] = version.__version__
15 
16 def get_docs(sid):
17     """Get docs associated with source id `sid` sorted by submission date"""
18     docs = []
19     for filename in os.listdir(store.path(sid)):
20         os_stat = os.stat(store.path(sid, filename))
21         docs.append(dict(
22             name=filename,
23             date=str(datetime.fromtimestamp(os_stat.st_mtime)),
24             size=os_stat.st_size,
25         ))
26     # sort by date since ordering by filename is meaningless
27     docs.sort(key=lambda x: x['date'])
28     return docs
29 
30 @app.after_request
31 def no_cache(response):
32   """Minimize potential traces of site access by telling the browser not to
33   cache anything"""
34   no_cache_headers = {
35       'Cache-Control': 'no-cache, no-store, must-revalidate',
36       'Pragma': 'no-cache',
37       'Expires': '-1',
38       }
39   for header, header_value in no_cache_headers.iteritems():
40     response.headers.add(header, header_value)
41   return response
42 
43 @app.route('/')
44 def index():
45   dirs = os.listdir(config.STORE_DIR)
46   cols = []
47   for d in dirs:
48     cols.append(dict(
49       name=d,
50       sid=crypto.displayid(d),
51       date=str(datetime.fromtimestamp(os.stat(store.path(d)).st_mtime)).split('.')[0]
52     ))
53   cols.sort(key=lambda x: x['date'], reverse=True)
54   return render_template('index.html', cols=cols)
55 
56 @app.route('/col/<sid>')
57 def col(sid):
58     return render_template("col.html", sid=sid, codename=crypto.displayid(sid),
59             docs=get_docs(sid), haskey=crypto.getkey(sid))
60 
61 @app.route('/col/<sid>/<fn>')
62 def doc(sid, fn):
63   if '..' in fn or fn.startswith('/'):
64     abort(404)
65   return send_file(store.path(sid, fn), mimetype="application/pgp-encrypted")
66 
67 @app.route('/reply', methods=('POST',))
68 def reply():
69   sid, msg = request.form['sid'], request.form['msg']
70   crypto.encrypt(crypto.getkey(sid), request.form['msg'], output=
71     store.path(sid, 'reply-%s.gpg' % uuid.uuid4()))
72   return render_template('reply.html', sid=sid, codename=crypto.displayid(sid))
73 
74 @app.route('/delete', methods=('POST',))
75 def delete():
76   sid = request.form['sid']
77   doc_names_selected = request.form.getlist('doc_names_selected')
78   docs_selected = [doc for doc in get_docs(sid) if doc['name'] in doc_names_selected]
79   confirm_delete = bool(request.form.get('confirm_delete', False))
80   if confirm_delete:
81       for doc in docs_selected:
82           fn = store.path(sid, doc['name'])
83           crypto.secureunlink(fn)
84   return render_template('delete.html', sid=sid, codename=crypto.displayid(sid),
85                          docs_selected=docs_selected, confirm_delete=confirm_delete)
86 
87 if __name__ == "__main__":
88   # TODO: make sure this gets run by the web server
89   CsrfProtect(app)
90   app.run(debug=True, port=8081)
91
```

Path: `modules/deaddrop/files/deaddrop/store.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 import os
3 import re
4 import config
5 
6 VALIDATE_FILENAME = re.compile("^(reply-)?[a-f0-9-]+(_msg|_doc|)\.gpg$").match
7 
8 class PathException(Exception):
9     '''An exception raised by `store.verify` when it encounters a bad path. A path
10     can be bad when it is not absolute, not normalized, not within
11     `config.STORE_DIR`, or doesn't match the filename format.
12     '''
13     pass
14 
15 def verify(p):
16     '''Assert that the path is absolute, normalized, inside `config.STORE_DIR`, and
17     matches the filename format.
18     '''
19     if not os.path.isabs(config.STORE_DIR):
20         raise PathException("config.STORE_DIR(%s) is not absolute" % (
21             config.STORE_DIR, ))
22 
23     # os.path.abspath makes the path absolute and normalizes '/foo/../bar' to
24     # '/bar', etc. We have to check that the path is normalized before checking
25     # that it starts with the `config.STORE_DIR` or else a malicious actor could
26     # append a bunch of '../../..' to access files outside of the store.
27     if not p == os.path.abspath(p):
28         raise PathException("The path is not absolute and/or normalized")
29 
30     if os.path.commonprefix([config.STORE_DIR, p]) != config.STORE_DIR:
31         raise PathException("Invalid directory %s" % (p, ))
32 
33     filename = os.path.basename(p)
34     ext = os.path.splitext(filename)[-1]
35 
36     if os.path.isfile(p):
37         if ext != '.gpg':
38             # if there's an extension, verify it's a GPG
39             raise PathException("Invalid file extension %s" % (ext, ))
40 
41         if not VALIDATE_FILENAME(filename):
42             raise PathException("Invalid filename %s" % (filename, ))
43 
44 def path(*s):
45     '''Get the normalized, absolute file path, within `config.STORE_DIR`.'''
46     joined = os.path.join(os.path.abspath(config.STORE_DIR), *s)
47     absolute = os.path.abspath(joined)
48     verify(absolute)
49     return absolute
50 
51 def log(msg):
52     file(path('NOTES'), 'a').write(msg)
53
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/modules/deaddrop/files/deaddrop/journalist.py b/modules/deaddrop/files/deaddrop/journalist.py
--- a/modules/deaddrop/files/deaddrop/journalist.py
+++ b/modules/deaddrop/files/deaddrop/journalist.py
@@ -71,11 +71,22 @@
     store.path(sid, 'reply-%s.gpg' % uuid.uuid4()))
   return render_template('reply.html', sid=sid, codename=crypto.displayid(sid))
 
-@app.route('/delete', methods=('POST',))
-def delete():
+@app.route('/bulk', methods=('POST',))
+def bulk():
+  action = request.form['action']
+
   sid = request.form['sid']
   doc_names_selected = request.form.getlist('doc_names_selected')
-  docs_selected = [doc for doc in get_docs(sid) if doc['name'] in doc_names_selected]
+  docs_selected = [ doc for doc in get_docs(sid) if doc['name'] in doc_names_selected ]
+
+  if action == 'download':
+    return bulk_download(sid, docs_selected)
+  elif action == 'delete':
+    return bulk_delete(sid, docs_selected)
+  else:
+    abort(422)
+
+def bulk_delete(sid, docs_selected):
   confirm_delete = bool(request.form.get('confirm_delete', False))
   if confirm_delete:
       for doc in docs_selected:
@@ -84,6 +95,13 @@
   return render_template('delete.html', sid=sid, codename=crypto.displayid(sid),
                          docs_selected=docs_selected, confirm_delete=confirm_delete)
 
+def bulk_download(sid, docs_selected):
+  filenames = [store.path(sid, doc['name']) for doc in docs_selected]
+  zip = store.get_bulk_archive(filenames)
+  return send_file(zip, mimetype="application/zip",  attachment_filename=crypto.displayid(sid), as_attachment=True)
+
+
+  
 if __name__ == "__main__":
   # TODO: make sure this gets run by the web server
   CsrfProtect(app)
diff --git a/modules/deaddrop/files/deaddrop/store.py b/modules/deaddrop/files/deaddrop/store.py
--- a/modules/deaddrop/files/deaddrop/store.py
+++ b/modules/deaddrop/files/deaddrop/store.py
@@ -2,6 +2,9 @@
 import os
 import re
 import config
+import zipfile
+import crypto
+import uuid
 
 VALIDATE_FILENAME = re.compile("^(reply-)?[a-f0-9-]+(_msg|_doc|)\.gpg$").match
 
@@ -48,5 +51,14 @@
     verify(absolute)
     return absolute
 
+def get_bulk_archive(filenames):
+    zip_file_name = os.path.join(config.TEMP_DIR, str(uuid.uuid4()) + '.zip')
+    with zipfile.ZipFile(zip_file_name, 'w') as zip:
+        for filename in filenames:
+            verify(filename)
+            basename = os.path.basename(filename)
+            zip.write(filename, arcname=basename)
+    return zip_file_name
+
 def log(msg):
     file(path('NOTES'), 'a').write(msg)

{"golden_diff": "diff --git a/modules/deaddrop/files/deaddrop/journalist.py b/modules/deaddrop/files/deaddrop/journalist.py\n--- a/modules/deaddrop/files/deaddrop/journalist.py\n+++ b/modules/deaddrop/files/deaddrop/journalist.py\n@@ -71,11 +71,22 @@\n     store.path(sid, 'reply-%s.gpg' % uuid.uuid4()))\n   return render_template('reply.html', sid=sid, codename=crypto.displayid(sid))\n \n-@app.route('/delete', methods=('POST',))\n-def delete():\n+@app.route('/bulk', methods=('POST',))\n+def bulk():\n+  action = request.form['action']\n+\n   sid = request.form['sid']\n   doc_names_selected = request.form.getlist('doc_names_selected')\n-  docs_selected = [doc for doc in get_docs(sid) if doc['name'] in doc_names_selected]\n+  docs_selected = [ doc for doc in get_docs(sid) if doc['name'] in doc_names_selected ]\n+\n+  if action == 'download':\n+    return bulk_download(sid, docs_selected)\n+  elif action == 'delete':\n+    return bulk_delete(sid, docs_selected)\n+  else:\n+    abort(422)\n+\n+def bulk_delete(sid, docs_selected):\n   confirm_delete = bool(request.form.get('confirm_delete', False))\n   if confirm_delete:\n       for doc in docs_selected:\n@@ -84,6 +95,13 @@\n   return render_template('delete.html', sid=sid, codename=crypto.displayid(sid),\n                          docs_selected=docs_selected, confirm_delete=confirm_delete)\n \n+def bulk_download(sid, docs_selected):\n+  filenames = [store.path(sid, doc['name']) for doc in docs_selected]\n+  zip = store.get_bulk_archive(filenames)\n+  return send_file(zip, mimetype=\"application/zip\",  attachment_filename=crypto.displayid(sid), as_attachment=True)\n+\n+\n+  \n if __name__ == \"__main__\":\n   # TODO: make sure this gets run by the web server\n   CsrfProtect(app)\ndiff --git a/modules/deaddrop/files/deaddrop/store.py b/modules/deaddrop/files/deaddrop/store.py\n--- a/modules/deaddrop/files/deaddrop/store.py\n+++ b/modules/deaddrop/files/deaddrop/store.py\n@@ -2,6 +2,9 @@\n import os\n import re\n import config\n+import zipfile\n+import crypto\n+import uuid\n \n VALIDATE_FILENAME = re.compile(\"^(reply-)?[a-f0-9-]+(_msg|_doc|)\\.gpg$\").match\n \n@@ -48,5 +51,14 @@\n     verify(absolute)\n     return absolute\n \n+def get_bulk_archive(filenames):\n+    zip_file_name = os.path.join(config.TEMP_DIR, str(uuid.uuid4()) + '.zip')\n+    with zipfile.ZipFile(zip_file_name, 'w') as zip:\n+        for filename in filenames:\n+            verify(filename)\n+            basename = os.path.basename(filename)\n+            zip.write(filename, arcname=basename)\n+    return zip_file_name\n+\n def log(msg):\n     file(path('NOTES'), 'a').write(msg)\n", "issue": "Journalists should be able to bulk download from document server\nRight now, journalists can only download one file at a time, even if there are dozens new submissions in any given session. The New Yorker team asked if we can enable bulk downloading so that journalists can download multiple files at once. \n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\nimport os\nfrom datetime import datetime\nimport uuid\n\nfrom flask import Flask, request, render_template, send_file\nfrom flask_wtf.csrf import CsrfProtect\n\nimport config, version, crypto, store, background\n\napp = Flask(__name__, template_folder=config.JOURNALIST_TEMPLATES_DIR)\napp.secret_key = config.SECRET_KEY\n\napp.jinja_env.globals['version'] = version.__version__\n\ndef get_docs(sid):\n    \"\"\"Get docs associated with source id `sid` sorted by submission date\"\"\"\n    docs = []\n    for filename in os.listdir(store.path(sid)):\n        os_stat = os.stat(store.path(sid, filename))\n        docs.append(dict(\n            name=filename,\n            date=str(datetime.fromtimestamp(os_stat.st_mtime)),\n            size=os_stat.st_size,\n        ))\n    # sort by date since ordering by filename is meaningless\n    docs.sort(key=lambda x: x['date'])\n    return docs\n\n@app.after_request\ndef no_cache(response):\n  \"\"\"Minimize potential traces of site access by telling the browser not to\n  cache anything\"\"\"\n  no_cache_headers = {\n      'Cache-Control': 'no-cache, no-store, must-revalidate',\n      'Pragma': 'no-cache',\n      'Expires': '-1',\n      }\n  for header, header_value in no_cache_headers.iteritems():\n    response.headers.add(header, header_value)\n  return response\n\n@app.route('/')\ndef index():\n  dirs = os.listdir(config.STORE_DIR)\n  cols = []\n  for d in dirs:\n    cols.append(dict(\n      name=d,\n      sid=crypto.displayid(d),\n      date=str(datetime.fromtimestamp(os.stat(store.path(d)).st_mtime)).split('.')[0]\n    ))\n  cols.sort(key=lambda x: x['date'], reverse=True)\n  return render_template('index.html', cols=cols)\n\n@app.route('/col/<sid>')\ndef col(sid):\n    return render_template(\"col.html\", sid=sid, codename=crypto.displayid(sid),\n            docs=get_docs(sid), haskey=crypto.getkey(sid))\n\n@app.route('/col/<sid>/<fn>')\ndef doc(sid, fn):\n  if '..' in fn or fn.startswith('/'):\n    abort(404)\n  return send_file(store.path(sid, fn), mimetype=\"application/pgp-encrypted\")\n\n@app.route('/reply', methods=('POST',))\ndef reply():\n  sid, msg = request.form['sid'], request.form['msg']\n  crypto.encrypt(crypto.getkey(sid), request.form['msg'], output=\n    store.path(sid, 'reply-%s.gpg' % uuid.uuid4()))\n  return render_template('reply.html', sid=sid, codename=crypto.displayid(sid))\n\n@app.route('/delete', methods=('POST',))\ndef delete():\n  sid = request.form['sid']\n  doc_names_selected = request.form.getlist('doc_names_selected')\n  docs_selected = [doc for doc in get_docs(sid) if doc['name'] in doc_names_selected]\n  confirm_delete = bool(request.form.get('confirm_delete', False))\n  if confirm_delete:\n      for doc in docs_selected:\n          fn = store.path(sid, doc['name'])\n          crypto.secureunlink(fn)\n  return render_template('delete.html', sid=sid, codename=crypto.displayid(sid),\n                         docs_selected=docs_selected, confirm_delete=confirm_delete)\n\nif __name__ == \"__main__\":\n  # TODO: make sure this gets run by the web server\n  CsrfProtect(app)\n  app.run(debug=True, port=8081)\n", "path": "modules/deaddrop/files/deaddrop/journalist.py"}, {"content": "# -*- coding: utf-8 -*-\nimport os\nimport re\nimport config\n\nVALIDATE_FILENAME = re.compile(\"^(reply-)?[a-f0-9-]+(_msg|_doc|)\\.gpg$\").match\n\nclass PathException(Exception):\n    '''An exception raised by `store.verify` when it encounters a bad path. A path\n    can be bad when it is not absolute, not normalized, not within\n    `config.STORE_DIR`, or doesn't match the filename format.\n    '''\n    pass\n\ndef verify(p):\n    '''Assert that the path is absolute, normalized, inside `config.STORE_DIR`, and\n    matches the filename format.\n    '''\n    if not os.path.isabs(config.STORE_DIR):\n        raise PathException(\"config.STORE_DIR(%s) is not absolute\" % (\n            config.STORE_DIR, ))\n\n    # os.path.abspath makes the path absolute and normalizes '/foo/../bar' to\n    # '/bar', etc. We have to check that the path is normalized before checking\n    # that it starts with the `config.STORE_DIR` or else a malicious actor could\n    # append a bunch of '../../..' to access files outside of the store.\n    if not p == os.path.abspath(p):\n        raise PathException(\"The path is not absolute and/or normalized\")\n\n    if os.path.commonprefix([config.STORE_DIR, p]) != config.STORE_DIR:\n        raise PathException(\"Invalid directory %s\" % (p, ))\n\n    filename = os.path.basename(p)\n    ext = os.path.splitext(filename)[-1]\n\n    if os.path.isfile(p):\n        if ext != '.gpg':\n            # if there's an extension, verify it's a GPG\n            raise PathException(\"Invalid file extension %s\" % (ext, ))\n\n        if not VALIDATE_FILENAME(filename):\n            raise PathException(\"Invalid filename %s\" % (filename, ))\n\ndef path(*s):\n    '''Get the normalized, absolute file path, within `config.STORE_DIR`.'''\n    joined = os.path.join(os.path.abspath(config.STORE_DIR), *s)\n    absolute = os.path.abspath(joined)\n    verify(absolute)\n    return absolute\n\ndef log(msg):\n    file(path('NOTES'), 'a').write(msg)\n", "path": "modules/deaddrop/files/deaddrop/store.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\nimport os\nfrom datetime import datetime\nimport uuid\n\nfrom flask import Flask, request, render_template, send_file\nfrom flask_wtf.csrf import CsrfProtect\n\nimport config, version, crypto, store, background\n\napp = Flask(__name__, template_folder=config.JOURNALIST_TEMPLATES_DIR)\napp.secret_key = config.SECRET_KEY\n\napp.jinja_env.globals['version'] = version.__version__\n\ndef get_docs(sid):\n    \"\"\"Get docs associated with source id `sid` sorted by submission date\"\"\"\n    docs = []\n    for filename in os.listdir(store.path(sid)):\n        os_stat = os.stat(store.path(sid, filename))\n        docs.append(dict(\n            name=filename,\n            date=str(datetime.fromtimestamp(os_stat.st_mtime)),\n            size=os_stat.st_size,\n        ))\n    # sort by date since ordering by filename is meaningless\n    docs.sort(key=lambda x: x['date'])\n    return docs\n\n@app.after_request\ndef no_cache(response):\n  \"\"\"Minimize potential traces of site access by telling the browser not to\n  cache anything\"\"\"\n  no_cache_headers = {\n      'Cache-Control': 'no-cache, no-store, must-revalidate',\n      'Pragma': 'no-cache',\n      'Expires': '-1',\n      }\n  for header, header_value in no_cache_headers.iteritems():\n    response.headers.add(header, header_value)\n  return response\n\n@app.route('/')\ndef index():\n  dirs = os.listdir(config.STORE_DIR)\n  cols = []\n  for d in dirs:\n    cols.append(dict(\n      name=d,\n      sid=crypto.displayid(d),\n      date=str(datetime.fromtimestamp(os.stat(store.path(d)).st_mtime)).split('.')[0]\n    ))\n  cols.sort(key=lambda x: x['date'], reverse=True)\n  return render_template('index.html', cols=cols)\n\n@app.route('/col/<sid>')\ndef col(sid):\n    return render_template(\"col.html\", sid=sid, codename=crypto.displayid(sid),\n            docs=get_docs(sid), haskey=crypto.getkey(sid))\n\n@app.route('/col/<sid>/<fn>')\ndef doc(sid, fn):\n  if '..' in fn or fn.startswith('/'):\n    abort(404)\n  return send_file(store.path(sid, fn), mimetype=\"application/pgp-encrypted\")\n\n@app.route('/reply', methods=('POST',))\ndef reply():\n  sid, msg = request.form['sid'], request.form['msg']\n  crypto.encrypt(crypto.getkey(sid), request.form['msg'], output=\n    store.path(sid, 'reply-%s.gpg' % uuid.uuid4()))\n  return render_template('reply.html', sid=sid, codename=crypto.displayid(sid))\n\n@app.route('/bulk', methods=('POST',))\ndef bulk():\n  action = request.form['action']\n\n  sid = request.form['sid']\n  doc_names_selected = request.form.getlist('doc_names_selected')\n  docs_selected = [ doc for doc in get_docs(sid) if doc['name'] in doc_names_selected ]\n\n  if action == 'download':\n    return bulk_download(sid, docs_selected)\n  elif action == 'delete':\n    return bulk_delete(sid, docs_selected)\n  else:\n    abort(422)\n\ndef bulk_delete(sid, docs_selected):\n  confirm_delete = bool(request.form.get('confirm_delete', False))\n  if confirm_delete:\n      for doc in docs_selected:\n          fn = store.path(sid, doc['name'])\n          crypto.secureunlink(fn)\n  return render_template('delete.html', sid=sid, codename=crypto.displayid(sid),\n                         docs_selected=docs_selected, confirm_delete=confirm_delete)\n\ndef bulk_download(sid, docs_selected):\n  filenames = [store.path(sid, doc['name']) for doc in docs_selected]\n  zip = store.get_bulk_archive(filenames)\n  return send_file(zip, mimetype=\"application/zip\",  attachment_filename=crypto.displayid(sid), as_attachment=True)\n\n\n  \nif __name__ == \"__main__\":\n  # TODO: make sure this gets run by the web server\n  CsrfProtect(app)\n  app.run(debug=True, port=8081)\n", "path": "modules/deaddrop/files/deaddrop/journalist.py"}, {"content": "# -*- coding: utf-8 -*-\nimport os\nimport re\nimport config\nimport zipfile\nimport crypto\nimport uuid\n\nVALIDATE_FILENAME = re.compile(\"^(reply-)?[a-f0-9-]+(_msg|_doc|)\\.gpg$\").match\n\nclass PathException(Exception):\n    '''An exception raised by `store.verify` when it encounters a bad path. A path\n    can be bad when it is not absolute, not normalized, not within\n    `config.STORE_DIR`, or doesn't match the filename format.\n    '''\n    pass\n\ndef verify(p):\n    '''Assert that the path is absolute, normalized, inside `config.STORE_DIR`, and\n    matches the filename format.\n    '''\n    if not os.path.isabs(config.STORE_DIR):\n        raise PathException(\"config.STORE_DIR(%s) is not absolute\" % (\n            config.STORE_DIR, ))\n\n    # os.path.abspath makes the path absolute and normalizes '/foo/../bar' to\n    # '/bar', etc. We have to check that the path is normalized before checking\n    # that it starts with the `config.STORE_DIR` or else a malicious actor could\n    # append a bunch of '../../..' to access files outside of the store.\n    if not p == os.path.abspath(p):\n        raise PathException(\"The path is not absolute and/or normalized\")\n\n    if os.path.commonprefix([config.STORE_DIR, p]) != config.STORE_DIR:\n        raise PathException(\"Invalid directory %s\" % (p, ))\n\n    filename = os.path.basename(p)\n    ext = os.path.splitext(filename)[-1]\n\n    if os.path.isfile(p):\n        if ext != '.gpg':\n            # if there's an extension, verify it's a GPG\n            raise PathException(\"Invalid file extension %s\" % (ext, ))\n\n        if not VALIDATE_FILENAME(filename):\n            raise PathException(\"Invalid filename %s\" % (filename, ))\n\ndef path(*s):\n    '''Get the normalized, absolute file path, within `config.STORE_DIR`.'''\n    joined = os.path.join(os.path.abspath(config.STORE_DIR), *s)\n    absolute = os.path.abspath(joined)\n    verify(absolute)\n    return absolute\n\ndef get_bulk_archive(filenames):\n    zip_file_name = os.path.join(config.TEMP_DIR, str(uuid.uuid4()) + '.zip')\n    with zipfile.ZipFile(zip_file_name, 'w') as zip:\n        for filename in filenames:\n            verify(filename)\n            basename = os.path.basename(filename)\n            zip.write(filename, arcname=basename)\n    return zip_file_name\n\ndef log(msg):\n    file(path('NOTES'), 'a').write(msg)\n", "path": "modules/deaddrop/files/deaddrop/store.py"}]}