Skip to content

Commit b4b692e

Browse files
CopilotArchmongerCopilot
authored
Fix MediaRestore path corruption for files containing "media" in their paths (#601)
This PR fixes a critical bug in the `mediarestore` command where files containing "media" in their path were being corrupted during restoration. The fix removes an unnecessary string replacement operation that was incorrectly modifying file paths. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Archmonger <16909269+Archmonger@users.noreply.github.com> Co-authored-by: Mark Bakhit <archiethemonger@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent dc478bf commit b4b692e

14 files changed

Lines changed: 75 additions & 123 deletions

File tree

.github/copilot-instructions.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,14 @@ Modern development process using Hatch:
9090
6. **Auto-format code**: `hatch run lint:format` (2 seconds)
9191
7. **Test documentation**: `hatch run docs:build` (2 seconds)
9292
8. **Update documentation** when making changes to Python source code (required)
93+
9. **Add changelog entry** for all significant changes (bug fixes, new features, breaking changes) to `CHANGELOG.md` under the "Unreleased" section
9394

9495
Always run `hatch run lint:check` before committing. The CI (.github/workflows/build.yml) includes comprehensive checks across all supported Python/Django combinations.
9596

9697
**IMPORTANT**: Documentation must be updated whenever changes are made to Python source code. This is enforced as part of the development workflow.
9798

99+
**IMPORTANT**: Significant changes must always include a changelog entry in `CHANGELOG.md` under the appropriate category (Added, Changed, Deprecated, Removed, Fixed, Security) in the "Unreleased" section.
100+
98101
## Repository Structure and Navigation
99102

100103
Key directories and files:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Don't forget to remove deprecated code on each major release!
3434
- Fix issues with parsing excess whitespace within `dbbackup -d "<COMMA_SEPARATED_ARGS>"`
3535
- Fix encryption support when using `gnupg==5.x`.
3636
- Resolve SQLite backup temporary file locking issues on Windows.
37+
- Fix MediaRestore path corruption for files containing "media" in their paths.
3738

3839
### Security
3940

dbbackup/checks.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@
3232
)
3333
W006 = Warning(
3434
"FAILURE_RECIPIENTS has been deprecated",
35-
hint="settings.DBBACKUP_FAILURE_RECIPIENTS is replaced by "
36-
"settings.DBBACKUP_ADMINS",
35+
hint="settings.DBBACKUP_FAILURE_RECIPIENTS is replaced by settings.DBBACKUP_ADMINS",
3736
id="dbbackup.W006",
3837
)
3938
W007 = Warning(
@@ -97,16 +96,10 @@ def check_settings(app_configs, **kwargs):
9796
if not settings.STORAGE or not isinstance(settings.STORAGE, str):
9897
errors.append(W002)
9998

100-
if (
101-
not callable(settings.FILENAME_TEMPLATE)
102-
and "{datetime}" not in settings.FILENAME_TEMPLATE
103-
):
99+
if not callable(settings.FILENAME_TEMPLATE) and "{datetime}" not in settings.FILENAME_TEMPLATE:
104100
errors.append(W003)
105101

106-
if (
107-
not callable(settings.MEDIA_FILENAME_TEMPLATE)
108-
and "{datetime}" not in settings.MEDIA_FILENAME_TEMPLATE
109-
):
102+
if not callable(settings.MEDIA_FILENAME_TEMPLATE) and "{datetime}" not in settings.MEDIA_FILENAME_TEMPLATE:
110103
errors.append(W004)
111104

112105
if re.search(r"[^A-Za-z0-9%_-]", settings.DATE_FORMAT):

dbbackup/db/postgresql.py

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,7 @@ def _restore_dump(self, dump):
7070

7171
cmd += " {}".format(self.settings["NAME"])
7272
cmd = f"{self.restore_prefix} {cmd} {self.restore_suffix}"
73-
stdout, stderr = self.run_command(
74-
cmd, stdin=dump, env={**self.restore_env, **pg_env}
75-
)
73+
stdout, stderr = self.run_command(cmd, stdin=dump, env={**self.restore_env, **pg_env})
7674
return stdout, stderr
7775

7876

@@ -148,25 +146,13 @@ def _restore_dump(self, dump: str):
148146

149147
# Flatten optional values
150148
if self.restore_prefix:
151-
cmd.extend(
152-
self.restore_prefix
153-
if isinstance(self.restore_prefix, list)
154-
else [self.restore_prefix]
155-
)
149+
cmd.extend(self.restore_prefix if isinstance(self.restore_prefix, list) else [self.restore_prefix])
156150

157151
if self.restore_cmd:
158-
cmd.extend(
159-
self.restore_cmd
160-
if isinstance(self.restore_cmd, list)
161-
else [self.restore_cmd]
162-
)
152+
cmd.extend(self.restore_cmd if isinstance(self.restore_cmd, list) else [self.restore_cmd])
163153

164154
if self.pg_options:
165-
cmd.extend(
166-
self.pg_options
167-
if isinstance(self.pg_options, list)
168-
else [self.pg_options]
169-
)
155+
cmd.extend(self.pg_options if isinstance(self.pg_options, list) else [self.pg_options])
170156

171157
cmd.extend([dbname])
172158

@@ -184,15 +170,9 @@ def _restore_dump(self, dump: str):
184170
cmd.extend(["--if-exists"])
185171

186172
if self.restore_suffix:
187-
cmd.extend(
188-
self.restore_suffix
189-
if isinstance(self.restore_suffix, list)
190-
else [self.restore_suffix]
191-
)
173+
cmd.extend(self.restore_suffix if isinstance(self.restore_suffix, list) else [self.restore_suffix])
192174

193175
cmd_str = " ".join(cmd)
194-
stdout, _ = self.run_command(
195-
cmd_str, stdin=dump, env={**self.dump_env, **pg_env}
196-
)
176+
stdout, _ = self.run_command(cmd_str, stdin=dump, env={**self.dump_env, **pg_env})
197177

198178
return stdout

dbbackup/log.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ def emit(self, record):
1616
def send_mail(self, subject, message, *args, **kwargs):
1717
from . import utils
1818

19-
utils.mail_admins(
20-
subject, message, *args, connection=self.connection(), **kwargs
21-
)
19+
utils.mail_admins(subject, message, *args, connection=self.connection(), **kwargs)
2220

2321

2422
class MailEnabledFilter(logging.Filter):

dbbackup/management/commands/_base.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,14 @@ class BaseDbBackupCommand(BaseCommand):
6363
def __init__(self, *args, **kwargs):
6464
self.option_list = self.base_option_list + self.option_list
6565
if django.VERSION < (1, 10):
66-
options = tuple(
67-
optparse_make_option(*_args, **_kwargs)
68-
for _args, _kwargs in self.option_list
69-
)
66+
options = tuple(optparse_make_option(*_args, **_kwargs) for _args, _kwargs in self.option_list)
7067

7168
self.option_list = options + BaseCommand.option_list
7269
super().__init__(*args, **kwargs)
7370

7471
def add_arguments(self, parser):
7572
for args, kwargs in self.option_list:
76-
kwargs = {
77-
k: v
78-
for k, v in kwargs.items()
79-
if not k.startswith("_") and k not in USELESS_ARGS
80-
}
73+
kwargs = {k: v for k, v in kwargs.items() if not k.startswith("_") and k not in USELESS_ARGS}
8174
parser.add_argument(*args, **kwargs)
8275

8376
def _set_logger_level(self):

dbbackup/management/commands/dbrestore.py

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,11 @@ class Command(BaseDbBackupCommand):
2121
option_list = BaseDbBackupCommand.option_list + (
2222
make_option("-d", "--database", help="Database to restore"),
2323
make_option("-i", "--input-filename", help="Specify filename to backup from"),
24-
make_option(
25-
"-I", "--input-path", help="Specify path on local filesystem to backup from"
26-
),
24+
make_option("-I", "--input-path", help="Specify path on local filesystem to backup from"),
2725
make_option(
2826
"-s",
2927
"--servername",
30-
help="If backup file is not specified, filter the "
31-
"existing ones with the given servername",
28+
help="If backup file is not specified, filter the existing ones with the given servername",
3229
),
3330
make_option(
3431
"-c",
@@ -37,9 +34,7 @@ class Command(BaseDbBackupCommand):
3734
action="store_true",
3835
help="Decrypt data before restoring",
3936
),
40-
make_option(
41-
"-p", "--passphrase", help="Passphrase for decrypt file", default=None
42-
),
37+
make_option("-p", "--passphrase", help="Passphrase for decrypt file", default=None),
4338
make_option(
4439
"-z",
4540
"--uncompress",
@@ -85,9 +80,7 @@ def handle(self, *args, **options):
8580
self.passphrase = options.get("passphrase")
8681
self.interactive = options.get("interactive")
8782
self.input_database_name = options.get("database")
88-
self.database_name, self.database = self._get_database(
89-
self.input_database_name
90-
)
83+
self.database_name, self.database = self._get_database(self.input_database_name)
9184
self.storage = get_storage()
9285
self.no_drop = options.get("no_drop")
9386
self.pg_options = options.get("pg_options", "")
@@ -100,10 +93,7 @@ def _get_database(self, database_name: str):
10093
"""Get the database to restore."""
10194
if not database_name:
10295
if len(settings.DATABASES) > 1:
103-
errmsg = (
104-
"Because this project contains more than one database, you"
105-
" must specify the --database option."
106-
)
96+
errmsg = "Because this project contains more than one database, you must specify the --database option."
10797
raise CommandError(errmsg)
10898
database_name = list(settings.DATABASES.keys())[0]
10999
if database_name not in settings.DATABASES:
@@ -128,15 +118,11 @@ def _restore_backup(self):
128118
self.logger.info(f"Restoring: {input_filename}")
129119

130120
if self.decrypt:
131-
unencrypted_file, input_filename = utils.unencrypt_file(
132-
input_file, input_filename, self.passphrase
133-
)
121+
unencrypted_file, input_filename = utils.unencrypt_file(input_file, input_filename, self.passphrase)
134122
input_file.close()
135123
input_file = unencrypted_file
136124
if self.uncompress:
137-
uncompressed_file, input_filename = utils.uncompress_file(
138-
input_file, input_filename
139-
)
125+
uncompressed_file, input_filename = utils.uncompress_file(input_file, input_filename)
140126
input_file.close()
141127
input_file = uncompressed_file
142128

dbbackup/management/commands/listbackups.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ class Command(BaseDbBackupCommand):
4545
default=None,
4646
dest="encrypted",
4747
),
48-
make_option(
49-
"-c", "--content-type", help="Filter by content type 'db' or 'media'"
50-
),
48+
make_option("-c", "--content-type", help="Filter by content type 'db' or 'media'"),
5149
)
5250

5351
def handle(self, **options):

dbbackup/management/commands/mediabackup.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,7 @@ class Command(BaseDbBackupCommand):
4444
action="store_true",
4545
default=False,
4646
),
47-
make_option(
48-
"-o", "--output-filename", default=None, help="Specify filename on storage"
49-
),
47+
make_option("-o", "--output-filename", default=None, help="Specify filename on storage"),
5048
make_option(
5149
"-O",
5250
"--output-path",
@@ -111,9 +109,7 @@ def backup_mediafiles(self):
111109
filename = self.filename
112110
else:
113111
extension = f"tar{'.gz' if self.compress else ''}"
114-
filename = utils.filename_generate(
115-
extension, servername=self.servername, content_type=self.content_type
116-
)
112+
filename = utils.filename_generate(extension, servername=self.servername, content_type=self.content_type)
117113

118114
tarball = self._create_tar(filename)
119115
# Apply trans

dbbackup/management/commands/mediarestore.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,11 @@ class Command(BaseDbBackupCommand):
2121
action="store",
2222
help="Specify filename to backup from",
2323
),
24-
make_option(
25-
"-I", "--input-path", help="Specify path on local filesystem to backup from"
26-
),
24+
make_option("-I", "--input-path", help="Specify path on local filesystem to backup from"),
2725
make_option(
2826
"-s",
2927
"--servername",
30-
help="If backup file is not specified, filter the existing ones with the "
31-
"given servername",
28+
help="If backup file is not specified, filter the existing ones with the given servername",
3229
),
3330
make_option(
3431
"-e",
@@ -37,18 +34,14 @@ class Command(BaseDbBackupCommand):
3734
action="store_true",
3835
help="Decrypt data before restoring",
3936
),
40-
make_option(
41-
"-p", "--passphrase", default=None, help="Passphrase for decrypt file"
42-
),
37+
make_option("-p", "--passphrase", default=None, help="Passphrase for decrypt file"),
4338
make_option(
4439
"-z",
4540
"--uncompress",
4641
action="store_true",
4742
help="Uncompress gzip data before restoring",
4843
),
49-
make_option(
50-
"-r", "--replace", help="Replace existing files", action="store_true"
51-
),
44+
make_option("-r", "--replace", help="Replace existing files", action="store_true"),
5245
)
5346

5447
def handle(self, *args, **options):
@@ -87,9 +80,7 @@ def _restore_backup(self):
8780
self.logger.info("Restoring: %s", input_filename)
8881

8982
if self.decrypt:
90-
unencrypted_file, input_filename = utils.unencrypt_file(
91-
input_file, input_filename, self.passphrase
92-
)
83+
unencrypted_file, input_filename = utils.unencrypt_file(input_file, input_filename, self.passphrase)
9384
input_file.close()
9485
input_file = unencrypted_file
9586

@@ -110,5 +101,5 @@ def _restore_backup(self):
110101
media_file = tar_file.extractfile(media_file_info)
111102
if media_file is None:
112103
continue # Skip directories
113-
name = media_file_info.path.replace("media/", "")
104+
name = media_file_info.path
114105
self._upload_file(name, media_file)

0 commit comments

Comments
 (0)