Can backup a MySQL Moodle instance
authorFrederic Massart <fred@moodle.com>
Thu, 9 Aug 2012 09:40:17 +0000 (17:40 +0800)
committerFrederic Massart <fred@moodle.com>
Fri, 10 Aug 2012 02:04:51 +0000 (10:04 +0800)
lib/db.py
moodle-backup.py [new file with mode: 0755]

index 7c95e5e..e2c153c 100644 (file)
--- a/lib/db.py
+++ b/lib/db.py
@@ -39,6 +39,17 @@ class DB(object):
                else:
                        raise Exception('DB engine %s not supported' % engine)
 
+       def columns(self, table):
+
+               columns = []
+
+               if self.engine == 'mysqli':
+                       self.cur.execute('DESCRIBE %s' % table)
+                       for column in self.cur.fetchall():
+                               columns.append(column[0])
+
+               return columns
+
        def createdb(self, db):
 
                if self.engine == 'mysqli':
@@ -46,10 +57,6 @@ class DB(object):
                elif self.engine == 'pgsql':
                        self.cur.execute('CREATE DATABASE %s WITH ENCODING \'UNICODE\'' % db)
 
-       def dropdb(self, db):
-
-               self.cur.execute('DROP DATABASE %s' % db)
-
        def dbexists(self, db):
 
                count = None
@@ -63,6 +70,44 @@ class DB(object):
 
                return count > 0
 
+       def dropdb(self, db):
+
+               self.cur.execute('DROP DATABASE %s' % db)
+
+       def dump(self, fd, prefix = ''):
+               """Dump a database to the file descriptor passed"""
+
+               if self.engine != 'mysqli':
+                       raise Exception('Function dump not supported by %s' % self.engine)
+               if not type(fd) == file:
+                       raise Exception('Passed parameter is not a file object')
+
+               # Looping over selected tables
+               tables = self.tables()
+               for table in tables:
+                       if prefix != '' and not table.startswith(prefix):
+                               continue
+
+                       self.cur.execute('SHOW CREATE TABLE %s' % table)
+                       schema = self.cur.fetchone()[1]
+                       fd.write(schema + ';\n')
+
+                       # Get the columns
+                       columns = self.columns(table)
+
+                       # Get the field values
+                       self.cur.execute('SELECT %s FROM %s' % (','.join(columns), table))
+                       for row in self.cur.fetchall():
+                               values = []
+                               for value in row:
+                                       if value == None:
+                                               value = 'null'
+                                       else:
+                                               value = str(self.conn.escape(value))
+                                       values.append(value)
+                               insert = 'INSERT INTO %s (%s) VALUES(%s)' % (table, ','.join(columns), ','.join(values))
+                               fd.write(insert + ';\n')
+
        def selectdb(self, db):
 
                if self.engine == 'mysqli':
@@ -82,3 +127,14 @@ class DB(object):
                                dbname=str(db)
                        )
                        self.cur = self.conn.cursor()
+
+       def tables(self):
+
+               tables = []
+
+               if self.engine == 'mysqli':
+                       self.cur.execute('SHOW TABLES')
+                       for row in self.cur.fetchall():
+                               tables.append(row[0])
+
+               return tables
diff --git a/moodle-backup.py b/moodle-backup.py
new file mode 100755 (executable)
index 0000000..2b15b3f
--- /dev/null
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import sys
+import os
+import argparse
+import json
+import time
+from distutils.dir_util import copy_tree
+from lib import config, workplace, moodle, tools
+from lib.tools import debug
+
+C = config.Conf().get
+Wp = workplace.Workplace()
+
+# Arguments
+parser = argparse.ArgumentParser(description='Backup a Moodle instance')
+parser.add_argument('-l', '--list', action='store_true', help='list the backups', dest='list')
+parser.add_argument('name', metavar='name', default=None, nargs='?', help='name of the instance')
+args = parser.parse_args()
+
+name = args.name
+backupdir = os.path.join(C('dirs.moodle'), 'backup')
+
+# List the backups
+if args.list:
+    dirs = os.listdir(backupdir)
+    backups = {}
+
+    for d in dirs:
+       path = os.path.join(backupdir, d)
+       jsonfile = os.path.join(path, 'info.json')
+        if d == '.' or d == '..': continue
+        if not os.path.isdir(path): continue
+        if not os.path.isfile(jsonfile): continue
+        infos = json.load(open(jsonfile, 'r'))
+        backups[d] = infos
+
+    for name, info in backups.iteritems():
+       backuptime = time.ctime(info['backup_time'])
+       print '{0:<25}: {1:<30} {2}'.format(name, info['release'], backuptime)
+
+    sys.exit(0)
+
+# Resolve the instance
+M = Wp.resolve(name)
+if not M:
+    debug('This is not a Moodle instance')
+    sys.exit(1)
+
+if M.get('dbtype') != 'mysqli':
+       debug('Does not support backup for this DB engine %s yet, sorry!' % M.get('dbtype'))
+
+debug('Backuping %s' % name)
+now = int(time.time())
+backup_identifier = '%s_%s' % (name, now)
+
+# Copy whole directory, shutil will create topath
+topath = os.path.join(backupdir, backup_identifier)
+path = Wp.getPath(name)
+try:
+       debug('Copying instance directory')
+       copy_tree(path, topath, preserve_symlinks = 1)
+except Exception as e:
+       debug('Error while backuping directory %s to %s' % (path, topath))
+       debug(e)
+       sys.exit(1)
+
+# Dump the whole database
+if M.isInstalled():
+    debug('Dumping database')
+    dumpto = os.path.join(topath, 'dump.sql')
+    fd = open(dumpto, 'w')
+    M.dbo().selectdb(M.get('dbname'))
+    M.dbo().dump(fd)
+else:
+    debug('Instance not installed. Do not dump database.')
+
+# Create a JSON file containing all known information
+debug('Saving instance information')
+jsonto = os.path.join(topath, 'info.json')
+info = M.info()
+info['backup_identifier'] = backup_identifier
+info['backup_time'] = now
+json.dump(info, open(jsonto, 'w'), sort_keys = True, indent = 4)
+
+debug('Done.')