Backup command fails when the backup directory is missing
[mdk.git] / mdk / backup.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 """
5 Moodle Development Kit
6
7 Copyright (c) 2012 Frédéric Massart - FMCorz.net
8
9 This program is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation, either version 3 of the License, or
12 (at your option) any later version.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22 http://github.com/FMCorz/mdk
23 """
24
25 import os
26 import json
27 import time
28 import logging
29 from distutils.dir_util import copy_tree
30
31 from .tools import chmodRecursive, mkdir
32 from .db import DB
33 from .config import Conf
34 from .workplace import Workplace
35 from .exceptions import *
36
37 C = Conf()
38 jason = 'info.json'
39 sqlfile = 'dump.sql'
40
41
42 class BackupManager(object):
43
44 def __init__(self):
45 self.path = os.path.expanduser(os.path.join(C.get('dirs.moodle'), 'backup'))
46 if not os.path.exists(self.path):
47 mkdir(self.path, 0777)
48
49 def create(self, M):
50 """Creates a new backup of M"""
51
52 if M.isInstalled() and M.get('dbtype') not in ('mysqli', 'mariadb'):
53 raise BackupDBEngineNotSupported('Cannot backup database engine %s' % M.get('dbtype'))
54
55 name = M.get('identifier')
56 if name == None:
57 raise Exception('Cannot backup instance without identifier!')
58
59 now = int(time.time())
60 backup_identifier = self.createIdentifier(name)
61 Wp = Workplace()
62
63 # Copy whole directory, shutil will create topath
64 topath = os.path.join(self.path, backup_identifier)
65 path = Wp.getPath(name)
66 logging.info('Copying instance directory')
67 copy_tree(path, topath, preserve_symlinks=1)
68
69 # Dump the whole database
70 if M.isInstalled():
71 logging.info('Dumping database')
72 dumpto = os.path.join(topath, sqlfile)
73 fd = open(dumpto, 'w')
74 M.dbo().selectdb(M.get('dbname'))
75 M.dbo().dump(fd)
76 else:
77 logging.info('Instance not installed. Do not dump database.')
78
79 # Create a JSON file containing all known information
80 logging.info('Saving instance information')
81 jsonto = os.path.join(topath, jason)
82 info = M.info()
83 info['backup_origin'] = path
84 info['backup_identifier'] = backup_identifier
85 info['backup_time'] = now
86 json.dump(info, open(jsonto, 'w'), sort_keys=True, indent=4)
87
88 return True
89
90 def createIdentifier(self, name):
91 """Creates an identifier"""
92 for i in range(1, 100):
93 identifier = '{0}_{1:0>2}'.format(name, i)
94 if not self.exists(identifier):
95 break
96 identifier = None
97 if not identifier:
98 raise Exception('Could not generate a backup identifier! How many backup did you do?!')
99 return identifier
100
101 def exists(self, name):
102 """Checks whether a backup exists under this name or not"""
103 d = os.path.join(self.path, name)
104 f = os.path.join(d, jason)
105 if not os.path.isdir(d):
106 return False
107 return os.path.isfile(f)
108
109 def get(self, name):
110 return Backup(self.getPath(name))
111
112 def getPath(self, name):
113 return os.path.join(self.path, name)
114
115 def list(self):
116 """Returns a list of backups with their information"""
117 dirs = os.listdir(self.path)
118 backups = {}
119 for name in dirs:
120 if name == '.' or name == '..': continue
121 if not self.exists(name): continue
122 try:
123 backups[name] = Backup(self.getPath(name))
124 except:
125 # Must successfully retrieve information to be a valid backup
126 continue
127 return backups
128
129
130 class Backup(object):
131
132 def __init__(self, path):
133 self.path = path
134 self.jason = os.path.join(path, jason)
135 self.sqlfile = os.path.join(path, sqlfile)
136 if not os.path.isdir(path):
137 raise Exception('Could not find backup in %s' % path)
138 elif not os.path.isfile(self.jason):
139 raise Exception('Backup information file unfound!')
140 self.load()
141
142 def get(self, name):
143 """Returns a info on the backup"""
144 try:
145 return self.infos[name]
146 except:
147 return None
148
149 def load(self):
150 """Loads the backup information"""
151 if not os.path.isfile(self.jason):
152 raise Exception('Backup information file not found!')
153 try:
154 self.infos = json.load(open(self.jason, 'r'))
155 except:
156 raise Exception('Could not load information from JSON file')
157
158 def restore(self, destination=None):
159 """Restores the backup"""
160
161 identifier = self.get('identifier')
162 if not identifier:
163 raise Exception('Identifier is invalid! Cannot proceed.')
164
165 Wp = Workplace()
166 if destination == None:
167 destination = self.get('backup_origin')
168 if not destination:
169 raise Exception('Wrong path to perform the restore!')
170
171 if os.path.isdir(destination):
172 raise BackupDirectoryExistsException('Destination directory already exists!')
173
174 # Restoring database
175 if self.get('installed') and os.path.isfile(self.sqlfile):
176 dbname = self.get('dbname')
177 dbo = DB(self.get('dbtype'), C.get('db.%s' % self.get('dbtype')))
178 if dbo.dbexists(dbname):
179 raise BackupDBExistsException('Database already exists!')
180
181 # Copy tree to destination
182 try:
183 logging.info('Restoring instance directory')
184 copy_tree(self.path, destination, preserve_symlinks=1)
185 M = Wp.get(identifier)
186 chmodRecursive(Wp.getPath(identifier, 'data'), 0777)
187 except Exception as e:
188 raise Exception('Error while restoring directory\n%s\nto %s. Exception: %s' % (self.path, destination, e))
189
190 # Restoring database
191 if self.get('installed') and os.path.isfile(self.sqlfile):
192 logging.info('Restoring database')
193 content = ''
194 f = open(self.sqlfile, 'r')
195 for l in f:
196 content += l
197 queries = content.split(';\n')
198 content = None
199 logging.info("%d queries to execute" % (len(queries)))
200
201 dbo.createdb(dbname)
202 dbo.selectdb(dbname)
203 done = 0
204 for query in queries:
205 if len(query.strip()) == 0: continue
206 try:
207 dbo.execute(query)
208 except:
209 logging.error('Query failed! You will have to fix this mually. %s', query)
210 done += 1
211 if done % 500 == 0:
212 logging.debug("%d queries done" % done)
213 logging.info('%d queries done' % done)
214 dbo.close()
215
216 # Restoring symbolic link
217 linkDir = os.path.join(Wp.www, identifier)
218 wwwDir = Wp.getPath(identifier, 'www')
219 if os.path.islink(linkDir):
220 os.remove(linkDir)
221 if os.path.isfile(linkDir) or os.path.isdir(linkDir): # No elif!
222 logging.warning('Could not create symbolic link. Please manually create: ln -s %s %s' % (wwwDir, linkDir))
223 else:
224 os.symlink(wwwDir, linkDir)
225
226 return M