prompt: 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()
  
  ```
  
golden_diff: 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)
  
verification_info: {"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"}]}