1
1
"""
2
- Management command to restore PostgreSQL database.
2
+ Management command to restore PostgreSQL database from backup .
3
3
4
4
Usage:
5
- docker-compose -f local.yml run --rm django python manage.py database_restore path/to/backup.sql
6
- docker-compose -f production.yml run --rm django python manage.py database_restore path/to/backup.sql
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]
7
7
"""
8
8
9
9
import enum
10
+ import gzip
10
11
import os
12
+ import shutil
11
13
import socket
12
14
import subprocess
15
+ from contextlib import contextmanager
13
16
14
17
from django .conf import settings
15
18
from django .core .management .base import BaseCommand , CommandError
@@ -30,46 +33,86 @@ def detect_server() -> Server:
30
33
return Server .UNKNOWN
31
34
32
35
36
+ @contextmanager
37
+ def temp_file_handler (filename : str ):
38
+ """Context manager to handle temporary files, ensuring cleanup."""
39
+ try :
40
+ yield filename
41
+ finally :
42
+ if os .path .exists (filename ):
43
+ os .remove (filename )
44
+
45
+
33
46
class Command (BaseCommand ):
34
- help = "Restores PostgreSQL database from backup file"
47
+ help = "Restores PostgreSQL database from backup file (compressed or uncompressed) "
35
48
36
49
def add_arguments (self , parser ):
37
- parser .add_argument ("backup_path" , type = str , help = "Path to the backup file" )
50
+ parser .add_argument ("backup_path" , type = str , help = "Path to the backup file (.sql or .sql.gz)" )
51
+
52
+ def get_db_settings (self ):
53
+ """Get database connection settings."""
54
+ db = settings .DATABASES ["default" ]
55
+ return {
56
+ "host" : db ["HOST" ],
57
+ "name" : db ["NAME" ],
58
+ "user" : db ["USER" ],
59
+ "password" : db ["PASSWORD" ],
60
+ }
61
+
62
+ def run_psql_command (self , command : str , db_name : str = "postgres" , env : dict = None ) -> None :
63
+ """Execute a psql command."""
64
+ db = self .get_db_settings ()
65
+ cmd = ["psql" , "-h" , db ["host" ], "-U" , db ["user" ], "-d" , db_name , "-c" , command ]
66
+ subprocess .run (cmd , env = env , check = True )
67
+
68
+ def reset_database (self , env : dict ) -> None :
69
+ """Drop and recreate the database."""
70
+ db = self .get_db_settings ()
71
+ self .stdout .write (f"Dropping database { db ['name' ]} ..." )
72
+ self .run_psql_command (f"DROP DATABASE IF EXISTS { db ['name' ]} " , env = env )
73
+
74
+ self .stdout .write (f"Creating database { db ['name' ]} ..." )
75
+ self .run_psql_command (f"CREATE DATABASE { db ['name' ]} " , env = env )
76
+
77
+ def restore_backup (self , backup_file : str , env : dict ) -> None :
78
+ """Restore database from backup file."""
79
+ db = self .get_db_settings ()
80
+ cmd = ["psql" , "-h" , db ["host" ], "-U" , db ["user" ], "-d" , db ["name" ], "-f" , backup_file ]
81
+ self .stdout .write ("Restoring from backup..." )
82
+ subprocess .run (cmd , env = env , check = True )
83
+
84
+ def decompress_file (self , input_file : str , output_file : str ) -> None :
85
+ """Decompress gzipped file to output file."""
86
+ with gzip .open (input_file , "rb" ) as f_in :
87
+ with open (output_file , "wb" ) as f_out :
88
+ shutil .copyfileobj (f_in , f_out )
38
89
39
90
def handle (self , * args , ** options ):
40
91
server = detect_server ()
41
92
backup_path = options ["backup_path" ]
93
+ is_compressed = backup_path .endswith (".gz" )
42
94
43
95
if not os .path .exists (backup_path ):
44
96
raise CommandError (f"Backup file not found: { backup_path } " )
45
97
46
- db_settings = settings .DATABASES ["default" ]
47
- host = db_settings ["HOST" ]
48
- name = db_settings ["NAME" ]
49
- user = db_settings ["USER" ]
50
- password = db_settings ["PASSWORD" ]
51
-
52
- # Drop and recreate database
53
- drop_cmd = ["psql" , "-h" , host , "-U" , user , "-d" , "postgres" , "-c" , f"DROP DATABASE IF EXISTS { name } " ]
54
- create_cmd = ["psql" , "-h" , host , "-U" , user , "-d" , "postgres" , "-c" , f"CREATE DATABASE { name } " ]
55
-
56
- # Restore command
57
- restore_cmd = ["psql" , "-h" , host , "-U" , user , "-d" , name , "-f" , backup_path ]
98
+ env = os .environ .copy ()
99
+ env ["PGPASSWORD" ] = self .get_db_settings ()["password" ]
58
100
59
101
try :
60
- env = os .environ .copy ()
61
- env ["PGPASSWORD" ] = password
62
-
63
- self .stdout .write (f"Dropping database { name } ..." )
64
- subprocess .run (drop_cmd , env = env , check = True )
65
-
66
- self .stdout .write (f"Creating database { name } ..." )
67
- subprocess .run (create_cmd , env = env , check = True )
102
+ # Reset the database first
103
+ self .reset_database (env )
68
104
69
- self .stdout .write ("Restoring from backup..." )
70
- subprocess .run (restore_cmd , env = env , check = True )
105
+ # Handle backup restoration
106
+ if is_compressed :
107
+ with temp_file_handler (backup_path [:- 3 ]) as temp_file :
108
+ self .decompress_file (backup_path , temp_file )
109
+ self .restore_backup (temp_file , env )
110
+ else :
111
+ self .restore_backup (backup_path , env )
71
112
72
113
self .stdout .write (self .style .SUCCESS (f"Successfully restored { server .value } database from { backup_path } " ))
73
114
74
115
except subprocess .CalledProcessError as e :
75
116
self .stdout .write (self .style .ERROR (f"Restore failed on { server .value } : { str (e )} " ))
117
+ except Exception as e :
118
+ self .stdout .write (self .style .ERROR (f"Error during restore process: { str (e )} " ))
0 commit comments