Skip to content

Commit febee1b

Browse files
authored
Merge pull request #1130 from NASA-IMPACT/database_import_bug_fixes
Updated database restore command
2 parents 626285d + 09d7438 commit febee1b

File tree

4 files changed

+100
-105
lines changed

4 files changed

+100
-105
lines changed

README.md

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ $ docker-compose -f local.yml build
1818
```bash
1919
$ docker-compose -f local.yml up
2020
```
21-
2221
### Non-Docker Local Setup
2322

2423
If you prefer to run the project without Docker, follow these steps:
@@ -69,12 +68,23 @@ $ docker-compose -f local.yml run --rm django python manage.py createsuperuser
6968
#### Creating Additional Users
7069

7170
Create additional users through the admin interface (/admin).
71+
## Database Backup and Restore
72+
73+
COSMOS provides dedicated management commands for backing up and restoring your PostgreSQL database. These commands handle both compressed and uncompressed backups and work seamlessly in both local and production environments using Docker.
74+
75+
### Backup Directory Structure
7276

73-
### Database Backup and Restore
77+
All backups are stored in the `/backups` directory at the root of your project. This directory is mounted as a volume in both local and production Docker configurations, making it easy to manage backups across different environments.
7478

75-
COSMOS provides dedicated management commands for backing up and restoring your PostgreSQL database. These commands handle both compressed and uncompressed backups and automatically detect your server environment from your configuration.
79+
- Local development: `./backups/`
80+
- Production server: `/path/to/project/backups/`
7681

77-
#### Creating a Database Backup
82+
If the directory doesn't exist, create it:
83+
```bash
84+
mkdir backups
85+
```
86+
87+
### Creating a Database Backup
7888

7989
To create a backup of your database:
8090

@@ -85,23 +95,24 @@ docker-compose -f local.yml run --rm django python manage.py database_backup
8595
# Create an uncompressed backup
8696
docker-compose -f local.yml run --rm django python manage.py database_backup --no-compress
8797

88-
# Specify custom output location
89-
docker-compose -f local.yml run --rm django python manage.py database_backup --output /path/to/output.sql
98+
# Specify custom output location within backups directory
99+
docker-compose -f local.yml run --rm django python manage.py database_backup --output my_custom_backup.sql
90100
```
91101

92102
The backup command will automatically:
93103
- Detect your server environment (Production/Staging/Local)
94104
- Use database credentials from your environment settings
95105
- Generate a dated filename if no output path is specified
106+
- Save the backup to the mounted `/backups` directory
96107
- Compress the backup by default (can be disabled with --no-compress)
97108

98-
#### Restoring from a Database Backup
109+
### Restoring from a Database Backup
99110

100-
To restore your database from a backup:
111+
To restore your database from a backup, it will need to be in the `/backups` directory. You can then run the following command:
101112

102113
```bash
103114
# Restore from a backup (handles both .sql and .sql.gz files)
104-
docker-compose -f local.yml run --rm django python manage.py database_restore path/to/backup.sql[.gz]
115+
docker-compose -f local.yml run --rm django python manage.py database_restore backups/backup_file_name.sql.gz
105116
```
106117

107118
The restore command will:
@@ -111,7 +122,7 @@ The restore command will:
111122
- Restore all data from the backup
112123
- Handle all database credentials from your environment settings
113124

114-
#### Working with Remote Servers
125+
### Working with Remote Servers
115126

116127
When working with production or staging servers:
117128

@@ -120,44 +131,37 @@ When working with production or staging servers:
120131
# For production
121132
ssh user@production-server
122133
cd /path/to/project
123-
124-
# For staging
125-
ssh user@staging-server
126-
cd /path/to/project
127134
```
128135

129-
2. Then run the backup command with the production configuration:
136+
2. Create a backup on the remote server:
130137
```bash
131138
docker-compose -f production.yml run --rm django python manage.py database_backup
132139
```
133140

134-
3. Copy the backup to your local machine:
141+
3. Copy the backup from the remote server's backup directory to your local machine:
135142
```bash
136-
scp user@remote-server:/path/to/backup.sql.gz ./local-backup.sql.gz
143+
scp user@remote-server:/path/to/project/backups/backup_name.sql.gz ./backups/
137144
```
138145

139-
4. Finally, restore locally:
146+
4. Restore locally:
140147
```bash
141-
docker-compose -f local.yml run --rm django python manage.py database_restore local-backup.sql.gz
148+
docker-compose -f local.yml run --rm django python manage.py database_restore backups/backup_name.sql.gz
142149
```
143150

144-
#### Alternative Methods
145-
146-
While the database_backup and database_restore commands are the recommended approach, there are alternative methods available:
151+
### Alternative Methods
147152

148-
##### Using JSON Fixtures (for smaller datasets)
149-
If you're working with a smaller dataset, you can use Django's built-in fixtures:
153+
While the database_backup and database_restore commands are the recommended approach, you can also use Django's built-in fixtures for smaller datasets:
150154

151155
```bash
152156
# Create a backup excluding content types
153-
docker-compose -f production.yml run --rm --user root django python manage.py dumpdata \
157+
docker-compose -f production.yml run --rm django python manage.py dumpdata \
154158
--natural-foreign --natural-primary \
155159
--exclude=contenttypes --exclude=auth.Permission \
156160
--indent 2 \
157-
--output /app/backups/prod_backup-$(date +%Y%m%d).json
161+
--output backups/prod_backup-$(date +%Y%m%d).json
158162

159163
# Restore from a fixture
160-
docker-compose -f local.yml run --rm django python manage.py loaddata /path/to/backup.json
164+
docker-compose -f local.yml run --rm django python manage.py loaddata backups/backup_name.json
161165
```
162166

163167
Note: For large databases (>1.5GB), the database_backup and database_restore commands are strongly recommended over JSON fixtures as they handle large datasets more efficiently.

sde_collections/management/commands/database_backup.py

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44
Usage:
55
docker-compose -f local.yml run --rm django python manage.py database_backup
66
docker-compose -f local.yml run --rm django python manage.py database_backup --no-compress
7-
docker-compose -f local.yml run --rm django python manage.py database_backup --output /path/to/output.sql
7+
docker-compose -f local.yml run --rm django python manage.py database_backup --output my_backup.sql
88
docker-compose -f production.yml run --rm django python manage.py database_backup
9+
10+
All backups are stored in the /backups directory, which is mounted as a volume in both local
11+
and production environments. If specifying a custom output path, it will be relative to this directory.
912
"""
1013

11-
import enum
1214
import gzip
1315
import os
1416
import shutil
15-
import socket
1617
import subprocess
1718
from contextlib import contextmanager
1819
from datetime import datetime
@@ -21,21 +22,6 @@
2122
from django.core.management.base import BaseCommand
2223

2324

24-
class Server(enum.Enum):
25-
PRODUCTION = "PRODUCTION"
26-
STAGING = "STAGING"
27-
UNKNOWN = "UNKNOWN"
28-
29-
30-
def detect_server() -> Server:
31-
hostname = socket.gethostname().upper()
32-
if "PRODUCTION" in hostname:
33-
return Server.PRODUCTION
34-
elif "STAGING" in hostname:
35-
return Server.STAGING
36-
return Server.UNKNOWN
37-
38-
3925
@contextmanager
4026
def temp_file_handler(filename: str):
4127
"""Context manager to handle temporary files, ensuring cleanup."""
@@ -58,36 +44,45 @@ def add_arguments(self, parser):
5844
parser.add_argument(
5945
"--output",
6046
type=str,
61-
help="Output file path (default: auto-generated based on server name and date)",
47+
help="Output file path (default: auto-generated in /app/backups directory)",
6248
)
6349

64-
def get_backup_filename(self, server: Server, compress: bool, custom_output: str = None) -> tuple[str, str]:
50+
def get_backup_filename(self, compress: bool, custom_output: str = None) -> tuple[str, str]:
6551
"""Generate backup filename and actual dump path.
6652
6753
Args:
68-
server: Server enum indicating the environment
6954
compress: Whether the output should be compressed
7055
custom_output: Optional custom output path
7156
7257
Returns:
73-
tuple[str, str]: A tuple containing (final_filename, temp_filename)
74-
- final_filename: The name of the final backup file (with .gz if compressed)
75-
- temp_filename: The name of the temporary dump file (always without .gz)
58+
tuple[str, str]: A tuple containing:
59+
- final_filename: Full path for the final backup file (with .gz if compressed)
60+
- temp_filename: Full path for the temporary dump file (without .gz)
7661
"""
62+
backup_dir = "/app/backups"
63+
os.makedirs(backup_dir, exist_ok=True)
64+
7765
if custom_output:
66+
# If custom_output is relative, make it relative to backup_dir
67+
if not custom_output.startswith("/"):
68+
custom_output = os.path.join(backup_dir, custom_output)
69+
7870
# Ensure the output directory exists
7971
output_dir = os.path.dirname(custom_output)
8072
if output_dir:
8173
os.makedirs(output_dir, exist_ok=True)
8274

8375
if compress:
84-
return custom_output + (".gz" if not custom_output.endswith(".gz") else ""), custom_output.removesuffix(
76+
return custom_output + (
77+
".gz" if not custom_output.endswith(".gz") else ""
78+
), custom_output.removesuffix( # noqa
8579
".gz"
8680
)
8781
return custom_output, custom_output
8882
else:
8983
date_str = datetime.now().strftime("%Y%m%d")
90-
temp_filename = f"{server.value.lower()}_backup_{date_str}.sql"
84+
env_name = os.getenv("BACKUP_ENVIRONMENT", "unknown")
85+
temp_filename = os.path.join(backup_dir, f"{env_name}_backup_{date_str}.sql")
9186
final_filename = f"{temp_filename}.gz" if compress else temp_filename
9287
return final_filename, temp_filename
9388

@@ -116,9 +111,15 @@ def compress_file(self, input_file: str, output_file: str) -> None:
116111
shutil.copyfileobj(f_in, f_out)
117112

118113
def handle(self, *args, **options):
119-
server = detect_server()
114+
if not os.getenv("BACKUP_ENVIRONMENT"):
115+
self.stdout.write(
116+
self.style.WARNING(
117+
"Note: Set BACKUP_ENVIRONMENT in your env if you want automatic environment-based filenames"
118+
)
119+
)
120+
120121
compress = not options["no_compress"]
121-
backup_file, dump_file = self.get_backup_filename(server, compress, options.get("output"))
122+
backup_file, dump_file = self.get_backup_filename(compress, options.get("output"))
122123

123124
env = os.environ.copy()
124125
env["PGPASSWORD"] = settings.DATABASES["default"]["PASSWORD"]
@@ -133,10 +134,10 @@ def handle(self, *args, **options):
133134

134135
self.stdout.write(
135136
self.style.SUCCESS(
136-
f"Successfully created {'compressed ' if compress else ''}backup for {server.value}: {backup_file}"
137+
f"Successfully created {'compressed ' if compress else ''}backup at: backups/{os.path.basename(backup_file)}" # noqa
137138
)
138139
)
139140
except subprocess.CalledProcessError as e:
140-
self.stdout.write(self.style.ERROR(f"Backup failed on {server.value}: {str(e)}"))
141+
self.stdout.write(self.style.ERROR(f"Backup failed: {str(e)}"))
141142
except Exception as e:
142143
self.stdout.write(self.style.ERROR(f"Error during backup process: {str(e)}"))

sde_collections/management/commands/database_restore.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
Management command to restore PostgreSQL database from backup.
33
44
Usage:
5-
docker-compose -f local.yml run --rm django python manage.py database_restore path/to/backup.sql[.gz]
6-
docker-compose -f production.yml run --rm django python manage.py database_restore path/to/backup.sql[.gz]
5+
docker-compose -f local.yml run --rm django python manage.py database_restore backups/backup.sql[.gz]
6+
docker-compose -f production.yml run --rm django python manage.py database_restore backups/backup.sql[.gz]
7+
8+
The backup file should be located in the /backups directory, which is mounted as a volume in both
9+
local and production environments.
710
"""
811

912
import enum

0 commit comments

Comments
 (0)