2 # -*- coding: utf-8 -*-
7 Copyright (c) 2012 Frédéric Massart - FMCorz.net
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.
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.
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/>.
22 http://github.com/FMCorz/mdk
29 from distutils
.dir_util
import copy_tree
31 from .tools
import chmodRecursive
, mkdir
33 from .config
import Conf
34 from .workplace
import Workplace
35 from .exceptions
import *
42 class BackupManager(object):
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)
50 """Creates a new backup of M"""
52 if M
.isInstalled() and M
.get('dbtype') not in ('mysqli', 'mariadb'):
53 raise BackupDBEngineNotSupported('Cannot backup database engine %s' % M
.get('dbtype'))
55 name
= M
.get('identifier')
57 raise Exception('Cannot backup instance without identifier!')
59 now
= int(time
.time())
60 backup_identifier
= self
.createIdentifier(name
)
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)
69 # Dump the whole database
71 logging
.info('Dumping database')
72 dumpto
= os
.path
.join(topath
, sqlfile
)
73 fd
= open(dumpto
, 'w')
74 M
.dbo().selectdb(M
.get('dbname'))
77 logging
.info('Instance not installed. Do not dump database.')
79 # Create a JSON file containing all known information
80 logging
.info('Saving instance information')
81 jsonto
= os
.path
.join(topath
, jason
)
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)
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
):
98 raise Exception('Could not generate a backup identifier! How many backup did you do?!')
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
):
107 return os
.path
.isfile(f
)
110 return Backup(self
.getPath(name
))
112 def getPath(self
, name
):
113 return os
.path
.join(self
.path
, name
)
116 """Returns a list of backups with their information"""
117 dirs
= os
.listdir(self
.path
)
120 if name
== '.' or name
== '..': continue
121 if not self
.exists(name
): continue
123 backups
[name
] = Backup(self
.getPath(name
))
125 # Must successfully retrieve information to be a valid backup
130 class Backup(object):
132 def __init__(self
, 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!')
143 """Returns a info on the backup"""
145 return self
.infos
[name
]
150 """Loads the backup information"""
151 if not os
.path
.isfile(self
.jason
):
152 raise Exception('Backup information file not found!')
154 self
.infos
= json
.load(open(self
.jason
, 'r'))
156 raise Exception('Could not load information from JSON file')
158 def restore(self
, destination
=None):
159 """Restores the backup"""
161 identifier
= self
.get('identifier')
163 raise Exception('Identifier is invalid! Cannot proceed.')
166 if destination
== None:
167 destination
= self
.get('backup_origin')
169 raise Exception('Wrong path to perform the restore!')
171 if os
.path
.isdir(destination
):
172 raise BackupDirectoryExistsException('Destination directory already exists!')
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!')
181 # Copy tree to destination
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
))
191 if self
.get('installed') and os
.path
.isfile(self
.sqlfile
):
192 logging
.info('Restoring database')
194 f
= open(self
.sqlfile
, 'r')
197 queries
= content
.split(';\n')
199 logging
.info("%d queries to execute" %
(len(queries
)))
204 for query
in queries
:
205 if len(query
.strip()) == 0: continue
209 logging
.error('Query failed! You will have to fix this mually. %s', query
)
212 logging
.debug("%d queries done" % done
)
213 logging
.info('%d queries done' % done
)
216 # Restoring symbolic link
217 linkDir
= os
.path
.join(Wp
.www
, identifier
)
218 wwwDir
= Wp
.getPath(identifier
, 'www')
219 if os
.path
.islink(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
))
224 os
.symlink(wwwDir
, linkDir
)