We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Extra level in zip folders for single sources
## Description

There is an extra unnecessary folder for zipfiles downloaded from the journalist interface for single sources. 

## Steps to Reproduce

0. Submit some files to SecureDrop
1. Load the home page of the journalist interface
2. Select one source using the checkbox
3. Select "Download"
4. Unzip the resulting file

## Expected Behavior

File structure would ideally be: 

`source-name/number_date`

e.g.:

`ecstatic-potato/1_03-20-20`

## Actual Behavior

Instead there is an additional layer:

`ecstatic-potato/ecstatic-potato/1_03-20-20`

## Comment

This additional folder is confusing to users and should be removed.

Note: if you download unread or download all for multiple sources - the outer folder will not be named after the source and will be e.g. `all`:

`all/ecstatic-potato/1_03-20-20`

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `securedrop/store.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 import os
3 import re
4 import config
5 import zipfile
6 import crypto_util
7 import tempfile
8 import gzip
9 from werkzeug import secure_filename
10 
11 from secure_tempfile import SecureTemporaryFile
12 
13 import logging
14 log = logging.getLogger(__name__)
15 
16 VALIDATE_FILENAME = re.compile(
17     "^(?P<index>\d+)\-[a-z0-9-_]*"
18     "(?P<file_type>msg|doc\.(gz|zip)|reply)\.gpg$").match
19 
20 
21 class PathException(Exception):
22 
23     """An exception raised by `util.verify` when it encounters a bad path. A path
24     can be bad when it is not absolute or not normalized.
25     """
26     pass
27 
28 
29 def verify(p):
30     """Assert that the path is absolute, normalized, inside `config.STORE_DIR`, and
31     matches the filename format.
32     """
33     if not os.path.isabs(config.STORE_DIR):
34         raise PathException("config.STORE_DIR(%s) is not absolute" % (
35             config.STORE_DIR, ))
36 
37     # os.path.abspath makes the path absolute and normalizes
38     # '/foo/../bar' to '/bar', etc. We have to check that the path is
39     # normalized before checking that it starts with the
40     # `config.STORE_DIR` or else a malicious actor could append a
41     # bunch of '../../..' to access files outside of the store.
42     if not p == os.path.abspath(p):
43         raise PathException("The path is not absolute and/or normalized")
44 
45     # Check that the path p is in config.STORE_DIR
46     if os.path.relpath(p, config.STORE_DIR).startswith('..'):
47         raise PathException("Invalid directory %s" % (p, ))
48 
49     if os.path.isfile(p):
50         filename = os.path.basename(p)
51         ext = os.path.splitext(filename)[-1]
52         if filename == '_FLAG':
53             return True
54         if ext != '.gpg':
55             # if there's an extension, verify it's a GPG
56             raise PathException("Invalid file extension %s" % (ext, ))
57         if not VALIDATE_FILENAME(filename):
58             raise PathException("Invalid filename %s" % (filename, ))
59 
60 
61 def path(*s):
62     """Get the normalized, absolute file path, within `config.STORE_DIR`."""
63     joined = os.path.join(os.path.abspath(config.STORE_DIR), *s)
64     absolute = os.path.abspath(joined)
65     verify(absolute)
66     return absolute
67 
68 
69 def get_bulk_archive(selected_submissions, zip_directory=''):
70     """Generate a zip file from the selected submissions"""
71     zip_file = tempfile.NamedTemporaryFile(prefix='tmp_securedrop_bulk_dl_',
72                                            dir=config.TEMP_DIR,
73                                            delete=False)
74     sources = set([i.source.journalist_designation
75                    for i in selected_submissions])
76     # The below nested for-loops are there to create a more usable
77     # folder structure per #383
78     with zipfile.ZipFile(zip_file, 'w') as zip:
79         for source in sources:
80             submissions = [s for s in selected_submissions
81                            if s.source.journalist_designation == source]
82             for submission in submissions:
83                 filename = path(submission.source.filesystem_id,
84                                 submission.filename)
85                 verify(filename)
86                 document_number = submission.filename.split('-')[0]
87                 zip.write(filename, arcname=os.path.join(
88                     zip_directory,
89                     source,
90                     "%s_%s" % (document_number,
91                                submission.source.last_updated.date()),
92                     os.path.basename(filename)
93                 ))
94     return zip_file
95 
96 
97 def save_file_submission(filesystem_id, count, journalist_filename, filename,
98                          stream):
99     sanitized_filename = secure_filename(filename)
100 
101     # We store file submissions in a .gz file for two reasons:
102     #
103     # 1. Downloading large files over Tor is very slow. If we can
104     # compress the file, we can speed up future downloads.
105     #
106     # 2. We want to record the original filename because it might be
107     # useful, either for context about the content of the submission
108     # or for figuring out which application should be used to open
109     # it. However, we'd like to encrypt that info and have the
110     # decrypted file automatically have the name of the original
111     # file. Given various usability constraints in GPG and Tails, this
112     # is the most user-friendly way we have found to do this.
113 
114     encrypted_file_name = "{0}-{1}-doc.gz.gpg".format(
115         count,
116         journalist_filename)
117     encrypted_file_path = path(filesystem_id, encrypted_file_name)
118     with SecureTemporaryFile("/tmp") as stf:
119         with gzip.GzipFile(filename=sanitized_filename,
120                            mode='wb', fileobj=stf) as gzf:
121             # Buffer the stream into the gzip file to avoid excessive
122             # memory consumption
123             while True:
124                 buf = stream.read(1024 * 8)
125                 if not buf:
126                     break
127                 gzf.write(buf)
128 
129         crypto_util.encrypt(stf, config.JOURNALIST_KEY, encrypted_file_path)
130 
131     return encrypted_file_name
132 
133 
134 def save_message_submission(filesystem_id, count, journalist_filename,
135                             message):
136     filename = "{0}-{1}-msg.gpg".format(count, journalist_filename)
137     msg_loc = path(filesystem_id, filename)
138     crypto_util.encrypt(message, config.JOURNALIST_KEY, msg_loc)
139     return filename
140 
141 
142 def rename_submission(filesystem_id, orig_filename, journalist_filename):
143     check_submission_name = VALIDATE_FILENAME(orig_filename)
144     if check_submission_name:
145         parsed_filename = check_submission_name.groupdict()
146         if parsed_filename.get('file_type'):
147             new_filename = "{}-{}-{}.gpg".format(
148                 parsed_filename['index'], journalist_filename,
149                 parsed_filename['file_type'])
150             try:
151                 os.rename(path(filesystem_id, orig_filename),
152                           path(filesystem_id, new_filename))
153             except OSError:
154                 pass
155             else:
156                 return new_filename  # Only return new filename if successful
157     return orig_filename
158
```
--- 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/securedrop/store.py b/securedrop/store.py
--- a/securedrop/store.py
+++ b/securedrop/store.py
@@ -78,6 +78,7 @@
     # folder structure per #383
     with zipfile.ZipFile(zip_file, 'w') as zip:
         for source in sources:
+            fname = ""
             submissions = [s for s in selected_submissions
                            if s.source.journalist_designation == source]
             for submission in submissions:
@@ -85,9 +86,12 @@
                                 submission.filename)
                 verify(filename)
                 document_number = submission.filename.split('-')[0]
+                if zip_directory == submission.source.journalist_filename:
+                    fname = zip_directory
+                else:
+                    fname = os.path.join(zip_directory, source)
                 zip.write(filename, arcname=os.path.join(
-                    zip_directory,
-                    source,
+                    fname,
                     "%s_%s" % (document_number,
                                submission.source.last_updated.date()),
                     os.path.basename(filename)

{"golden_diff": "diff --git a/securedrop/store.py b/securedrop/store.py\n--- a/securedrop/store.py\n+++ b/securedrop/store.py\n@@ -78,6 +78,7 @@\n     # folder structure per #383\n     with zipfile.ZipFile(zip_file, 'w') as zip:\n         for source in sources:\n+            fname = \"\"\n             submissions = [s for s in selected_submissions\n                            if s.source.journalist_designation == source]\n             for submission in submissions:\n@@ -85,9 +86,12 @@\n                                 submission.filename)\n                 verify(filename)\n                 document_number = submission.filename.split('-')[0]\n+                if zip_directory == submission.source.journalist_filename:\n+                    fname = zip_directory\n+                else:\n+                    fname = os.path.join(zip_directory, source)\n                 zip.write(filename, arcname=os.path.join(\n-                    zip_directory,\n-                    source,\n+                    fname,\n                     \"%s_%s\" % (document_number,\n                                submission.source.last_updated.date()),\n                     os.path.basename(filename)\n", "issue": "Extra level in zip folders for single sources\n## Description\r\n\r\nThere is an extra unnecessary folder for zipfiles downloaded from the journalist interface for single sources. \r\n\r\n## Steps to Reproduce\r\n\r\n0. Submit some files to SecureDrop\r\n1. Load the home page of the journalist interface\r\n2. Select one source using the checkbox\r\n3. Select \"Download\"\r\n4. Unzip the resulting file\r\n\r\n## Expected Behavior\r\n\r\nFile structure would ideally be: \r\n\r\n`source-name/number_date`\r\n\r\ne.g.:\r\n\r\n`ecstatic-potato/1_03-20-20`\r\n\r\n## Actual Behavior\r\n\r\nInstead there is an additional layer:\r\n\r\n`ecstatic-potato/ecstatic-potato/1_03-20-20`\r\n\r\n## Comment\r\n\r\nThis additional folder is confusing to users and should be removed.\r\n\r\nNote: if you download unread or download all for multiple sources - the outer folder will not be named after the source and will be e.g. `all`:\r\n\r\n`all/ecstatic-potato/1_03-20-20`\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\nimport os\nimport re\nimport config\nimport zipfile\nimport crypto_util\nimport tempfile\nimport gzip\nfrom werkzeug import secure_filename\n\nfrom secure_tempfile import SecureTemporaryFile\n\nimport logging\nlog = logging.getLogger(__name__)\n\nVALIDATE_FILENAME = re.compile(\n    \"^(?P<index>\\d+)\\-[a-z0-9-_]*\"\n    \"(?P<file_type>msg|doc\\.(gz|zip)|reply)\\.gpg$\").match\n\n\nclass PathException(Exception):\n\n    \"\"\"An exception raised by `util.verify` when it encounters a bad path. A path\n    can be bad when it is not absolute or not normalized.\n    \"\"\"\n    pass\n\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\n    # '/foo/../bar' to '/bar', etc. We have to check that the path is\n    # normalized before checking that it starts with the\n    # `config.STORE_DIR` or else a malicious actor could append a\n    # 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    # Check that the path p is in config.STORE_DIR\n    if os.path.relpath(p, config.STORE_DIR).startswith('..'):\n        raise PathException(\"Invalid directory %s\" % (p, ))\n\n    if os.path.isfile(p):\n        filename = os.path.basename(p)\n        ext = os.path.splitext(filename)[-1]\n        if filename == '_FLAG':\n            return True\n        if ext != '.gpg':\n            # if there's an extension, verify it's a GPG\n            raise PathException(\"Invalid file extension %s\" % (ext, ))\n        if not VALIDATE_FILENAME(filename):\n            raise PathException(\"Invalid filename %s\" % (filename, ))\n\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\n\ndef get_bulk_archive(selected_submissions, zip_directory=''):\n    \"\"\"Generate a zip file from the selected submissions\"\"\"\n    zip_file = tempfile.NamedTemporaryFile(prefix='tmp_securedrop_bulk_dl_',\n                                           dir=config.TEMP_DIR,\n                                           delete=False)\n    sources = set([i.source.journalist_designation\n                   for i in selected_submissions])\n    # The below nested for-loops are there to create a more usable\n    # folder structure per #383\n    with zipfile.ZipFile(zip_file, 'w') as zip:\n        for source in sources:\n            submissions = [s for s in selected_submissions\n                           if s.source.journalist_designation == source]\n            for submission in submissions:\n                filename = path(submission.source.filesystem_id,\n                                submission.filename)\n                verify(filename)\n                document_number = submission.filename.split('-')[0]\n                zip.write(filename, arcname=os.path.join(\n                    zip_directory,\n                    source,\n                    \"%s_%s\" % (document_number,\n                               submission.source.last_updated.date()),\n                    os.path.basename(filename)\n                ))\n    return zip_file\n\n\ndef save_file_submission(filesystem_id, count, journalist_filename, filename,\n                         stream):\n    sanitized_filename = secure_filename(filename)\n\n    # We store file submissions in a .gz file for two reasons:\n    #\n    # 1. Downloading large files over Tor is very slow. If we can\n    # compress the file, we can speed up future downloads.\n    #\n    # 2. We want to record the original filename because it might be\n    # useful, either for context about the content of the submission\n    # or for figuring out which application should be used to open\n    # it. However, we'd like to encrypt that info and have the\n    # decrypted file automatically have the name of the original\n    # file. Given various usability constraints in GPG and Tails, this\n    # is the most user-friendly way we have found to do this.\n\n    encrypted_file_name = \"{0}-{1}-doc.gz.gpg\".format(\n        count,\n        journalist_filename)\n    encrypted_file_path = path(filesystem_id, encrypted_file_name)\n    with SecureTemporaryFile(\"/tmp\") as stf:\n        with gzip.GzipFile(filename=sanitized_filename,\n                           mode='wb', fileobj=stf) as gzf:\n            # Buffer the stream into the gzip file to avoid excessive\n            # memory consumption\n            while True:\n                buf = stream.read(1024 * 8)\n                if not buf:\n                    break\n                gzf.write(buf)\n\n        crypto_util.encrypt(stf, config.JOURNALIST_KEY, encrypted_file_path)\n\n    return encrypted_file_name\n\n\ndef save_message_submission(filesystem_id, count, journalist_filename,\n                            message):\n    filename = \"{0}-{1}-msg.gpg\".format(count, journalist_filename)\n    msg_loc = path(filesystem_id, filename)\n    crypto_util.encrypt(message, config.JOURNALIST_KEY, msg_loc)\n    return filename\n\n\ndef rename_submission(filesystem_id, orig_filename, journalist_filename):\n    check_submission_name = VALIDATE_FILENAME(orig_filename)\n    if check_submission_name:\n        parsed_filename = check_submission_name.groupdict()\n        if parsed_filename.get('file_type'):\n            new_filename = \"{}-{}-{}.gpg\".format(\n                parsed_filename['index'], journalist_filename,\n                parsed_filename['file_type'])\n            try:\n                os.rename(path(filesystem_id, orig_filename),\n                          path(filesystem_id, new_filename))\n            except OSError:\n                pass\n            else:\n                return new_filename  # Only return new filename if successful\n    return orig_filename\n", "path": "securedrop/store.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\nimport os\nimport re\nimport config\nimport zipfile\nimport crypto_util\nimport tempfile\nimport subprocess\nimport gzip\nfrom werkzeug import secure_filename\n\nfrom secure_tempfile import SecureTemporaryFile\n\nimport logging\nlog = logging.getLogger(__name__)\n\nVALIDATE_FILENAME = re.compile(\n    \"^(?P<index>\\d+)\\-[a-z0-9-_]*\"\n    \"(?P<file_type>msg|doc\\.(gz|zip)|reply)\\.gpg$\").match\n\n\nclass PathException(Exception):\n\n    \"\"\"An exception raised by `util.verify` when it encounters a bad path. A path\n    can be bad when it is not absolute or not normalized.\n    \"\"\"\n    pass\n\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\n    # '/foo/../bar' to '/bar', etc. We have to check that the path is\n    # normalized before checking that it starts with the\n    # `config.STORE_DIR` or else a malicious actor could append a\n    # 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    # Check that the path p is in config.STORE_DIR\n    if os.path.relpath(p, config.STORE_DIR).startswith('..'):\n        raise PathException(\"Invalid directory %s\" % (p, ))\n\n    if os.path.isfile(p):\n        filename = os.path.basename(p)\n        ext = os.path.splitext(filename)[-1]\n        if filename == '_FLAG':\n            return True\n        if ext != '.gpg':\n            # if there's an extension, verify it's a GPG\n            raise PathException(\"Invalid file extension %s\" % (ext, ))\n        if not VALIDATE_FILENAME(filename):\n            raise PathException(\"Invalid filename %s\" % (filename, ))\n\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\n\ndef get_bulk_archive(selected_submissions, zip_directory=''):\n    \"\"\"Generate a zip file from the selected submissions\"\"\"\n    zip_file = tempfile.NamedTemporaryFile(prefix='tmp_securedrop_bulk_dl_',\n                                           dir=config.TEMP_DIR,\n                                           delete=False)\n    sources = set([i.source.journalist_designation\n                   for i in selected_submissions])\n    # The below nested for-loops are there to create a more usable\n    # folder structure per #383\n    with zipfile.ZipFile(zip_file, 'w') as zip:\n        for source in sources:\n            fname = \"\"\n            submissions = [s for s in selected_submissions\n                           if s.source.journalist_designation == source]\n            for submission in submissions:\n                filename = path(submission.source.filesystem_id,\n                                submission.filename)\n                verify(filename)\n                document_number = submission.filename.split('-')[0]\n                if zip_directory == submission.source.journalist_filename:\n                    fname = zip_directory\n                else:\n                    fname = os.path.join(zip_directory, source)\n                zip.write(filename, arcname=os.path.join(\n                    fname,\n                    \"%s_%s\" % (document_number,\n                               submission.source.last_updated.date()),\n                    os.path.basename(filename)\n                ))\n    return zip_file\n\n\ndef save_file_submission(filesystem_id, count, journalist_filename, filename,\n                         stream):\n    sanitized_filename = secure_filename(filename)\n\n    # We store file submissions in a .gz file for two reasons:\n    #\n    # 1. Downloading large files over Tor is very slow. If we can\n    # compress the file, we can speed up future downloads.\n    #\n    # 2. We want to record the original filename because it might be\n    # useful, either for context about the content of the submission\n    # or for figuring out which application should be used to open\n    # it. However, we'd like to encrypt that info and have the\n    # decrypted file automatically have the name of the original\n    # file. Given various usability constraints in GPG and Tails, this\n    # is the most user-friendly way we have found to do this.\n\n    encrypted_file_name = \"{0}-{1}-doc.gz.gpg\".format(\n        count,\n        journalist_filename)\n    encrypted_file_path = path(filesystem_id, encrypted_file_name)\n    with SecureTemporaryFile(\"/tmp\") as stf:\n        with gzip.GzipFile(filename=sanitized_filename,\n                           mode='wb', fileobj=stf) as gzf:\n            # Buffer the stream into the gzip file to avoid excessive\n            # memory consumption\n            while True:\n                buf = stream.read(1024 * 8)\n                if not buf:\n                    break\n                gzf.write(buf)\n\n        crypto_util.encrypt(stf, config.JOURNALIST_KEY, encrypted_file_path)\n\n    return encrypted_file_name\n\n\ndef save_message_submission(filesystem_id, count, journalist_filename,\n                            message):\n    filename = \"{0}-{1}-msg.gpg\".format(count, journalist_filename)\n    msg_loc = path(filesystem_id, filename)\n    crypto_util.encrypt(message, config.JOURNALIST_KEY, msg_loc)\n    return filename\n\n\ndef rename_submission(filesystem_id, orig_filename, journalist_filename):\n    check_submission_name = VALIDATE_FILENAME(orig_filename)\n    if check_submission_name:\n        parsed_filename = check_submission_name.groupdict()\n        if parsed_filename.get('file_type'):\n            new_filename = \"{}-{}-{}.gpg\".format(\n                parsed_filename['index'], journalist_filename,\n                parsed_filename['file_type'])\n            try:\n                os.rename(path(filesystem_id, orig_filename),\n                          path(filesystem_id, new_filename))\n            except OSError:\n                pass\n            else:\n                return new_filename  # Only return new filename if successful\n    return orig_filename\n\n\ndef secure_unlink(fn, recursive=False):\n    verify(fn)\n    command = ['srm']\n    if recursive:\n        command.append('-r')\n    command.append(fn)\n    subprocess.check_call(command)\n    return \"success\"\n\n\ndef delete_source_directory(filesystem_id):\n    secure_unlink(path(filesystem_id), recursive=True)\n    return \"success\"\n", "path": "securedrop/store.py"}]}