Add /server backup lite, which backups everything except blockdb and undo files.

This commit is contained in:
UnknownShadow200 2016-06-30 13:19:55 +10:00
parent dcece84ad1
commit 2bc639abd8
4 changed files with 422 additions and 25 deletions

View File

@ -85,7 +85,7 @@ namespace MCGalaxy.Commands {
// This means all folders, and files in these folders.
Player.Message(p, "Server backup (Everything) started. Please wait while backup finishes.");
Save(true, true, p);
} else if (type == "db") {
} else if (type == "database" || type == "sql" || type == "db") {
// Backup database only.
// Create SQL statements for this. The SQL will assume the settings for the current configuration are correct.
// This means we use the currently defined port, database, user, password, and pooling.
@ -94,12 +94,15 @@ namespace MCGalaxy.Commands {
// This means all folders, and files in these folders.
Player.Message(p, "Server backup (Database) started. Please wait while backup finishes.");
Save(false, true, p);
} else if (type == "allbutdb") {
} else if (type == "allbutdb" || type == "files" || type == "file") {
// Important to save everything to a .zip file (Though we can rename the extention.)
// When backing up, one option is to save all non-main program files.
// This means all folders, and files in these folders.
Player.Message(p, "Server backup (Everything but Database) started. Please wait while backup finishes.");
Save(true, false, p);
} else if (type == "lite") {
Player.Message(p, "Server backup (Everything but BlockDB tables and undo files) started. Please wait while backup finishes.");
Save(true, true, p, true);
} else if (type == "table") {
if (args.Length == 2) { Player.Message(p, "You need to provide the table name to backup."); return; }
if (!ValidName(p, args[2], "table")) return;
@ -136,16 +139,14 @@ namespace MCGalaxy.Commands {
return p.name.CaselessEq(Server.server_owner);
}
void Save(bool withFiles, bool withDB, Player p) {
ParameterizedThreadStart pts = new ParameterizedThreadStart(CreatePackage);
Thread doWork = new Thread(new ParameterizedThreadStart(CreatePackage));
doWork.Name = "MCG_SaveServer";
List<object> param = new List<object>();
param.Add("MCGalaxy.zip");
param.Add(withFiles);
param.Add(withDB);
param.Add(p);
doWork.Start(param);
static void Save(bool withFiles, bool withDB, Player p, bool lite = false) {
Thread worker = new Thread(Backup.CreatePackage);
worker.Name = "MCG_SaveServer";
Backup.BackupArgs args = new Backup.BackupArgs();
args.p = p; args.Lite = lite;
args.Files = withFiles; args.Database = withDB;
worker.Start(args);
}
void SetToDefault() {
@ -182,17 +183,13 @@ namespace MCGalaxy.Commands {
Player.Message(p, "/server <public> - Make the server public. (Start listening for new connections.)");
Player.Message(p, "/server <private> - Make the server private. (Stop listening for new connections.)");
Player.Message(p, "/server <restore> - Restore the server from a backup.");
Player.Message(p, "/server <backup> [all/db/allbutdb] - Make a backup. (Default is all)");
Player.Message(p, "/server <backup> [all/db/files/lite] - Make a backup. (Default all)");
Player.Message(p, "Backup options:");
Player.Message(p, "all - Make a backup of the server and all SQL data. (Default)");
Player.Message(p, "db - Just backup the database.");
Player.Message(p, "allbutdb - Backup everything BUT the database.");
Player.Message(p, "/server <backup> table [name] - Make a backups of that database table");
}
static void CreatePackage(object par) {
List<object> param = (List<object>)par;
Backup.CreatePackage((string)param[0], (bool)param[1], (bool)param[2], (Player)param[3]);
Player.Message(p, "all - Make a backup of the server and all SQL data.");
Player.Message(p, "db - Just backup the database.");
Player.Message(p, "files - Backup everything BUT the database.");
Player.Message(p, "lite - Backups everything, except BlockDB and undo files.");
Player.Message(p, "/server <backup> table [name] - Backups that database table");
}
}
}

View File

@ -560,8 +560,8 @@
<Compile Include="Games\CTF\Auto_CTF.cs" />
<Compile Include="IRC\ForgeBot.cs" />
<Compile Include="Levels\BlockQueue.cs" />
<Compile Include="Server\Backup\Backup.cs" />
<Compile Include="Server\Backup\BackupDB.cs" />
<Compile Include="Server\Backup.cs" />
<Compile Include="Server\BackupDB.cs" />
<Compile Include="Server\Extra\Checktimer.cs" />
<Compile Include="Server\Extra\UPnP.cs" />
<Compile Include="Server\Colors.cs" />
@ -702,7 +702,6 @@
<Folder Include="Plugins" />
<Folder Include="Plugins\Events" />
<Folder Include="Plugins\Manager" />
<Folder Include="Server\Backup" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

133
Server/Backup.cs Normal file
View File

@ -0,0 +1,133 @@
/*
Copyright 2011 MCForge
Dual-licensed under the Educational Community License, Version 2.0 and
the GNU General Public License, Version 3 (the "Licenses"); you may
not use this file except in compliance with the Licenses. You may
obtain a copy of the Licenses at
http://www.opensource.org/licenses/ecl2.php
http://www.gnu.org/licenses/gpl-3.0.html
Unless required by applicable law or agreed to in writing,
software distributed under the Licenses are distributed on an "AS IS"
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the Licenses for the specific language governing
permissions and limitations under the Licenses.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Packaging;
namespace MCGalaxy {
public static partial class Backup {
const string path = "MCGalaxy.zip";
public class BackupArgs {
public Player p;
public bool Files, Database, Lite;
}
public static void CreatePackage(object p) {
BackupArgs args = (BackupArgs)p;
if (args.Database) {
Server.s.Log("Saving DB...");
using (StreamWriter sql = new StreamWriter("SQL.sql"))
BackupDatabase(sql, args.Lite);
Server.s.Log("Saved DB to SQL.sql");
}
Server.s.Log("Creating package...");
using (ZipPackage package = (ZipPackage)ZipPackage.Open(path, FileMode.Create))
{
if (args.Files) {
Server.s.Log("Collecting Directory structure...");
string currDir = Directory.GetCurrentDirectory() + "\\";
List<Uri> partURIs = GetAllFiles(new DirectoryInfo("./"), new Uri(currDir));
Server.s.Log("Structure complete");
Server.s.Log("Saving data...");
foreach (Uri loc in partURIs) {
string file = Uri.UnescapeDataString(loc.ToString());
if (args.Lite && file.Contains("extra/undo/")) continue;
if (args.Lite && file.Contains("extra/undoPrevious")) continue;
if (!file.Contains(path)) {
// Add the part to the Package
ZipPackagePart packagePart = (ZipPackagePart)package.CreatePart(loc, "");
// Copy the data to the Document Part
using (FileStream stream = new FileStream("./" + file, FileMode.Open, FileAccess.Read))
CopyStream(stream, packagePart.GetStream());
}
}// end:foreach(Uri loc)
}
if (args.Database) { // If we don't want to back up database, we don't do this part.
Server.s.Log("Compressing Database...");
ZipPackagePart packagePart =
(ZipPackagePart)package.CreatePart(new Uri("/SQL.sql", UriKind.Relative), "", CompressionOption.Normal);
CopyStream(File.OpenRead("SQL.sql"), packagePart.GetStream());
Server.s.Log("Database compressed.");
}// end:if(withFiles)
Server.s.Log("Data saved!");
}// end:using (Package package) - Close and dispose package.
Player.Message(args.p, "Server backup (" + (args.Files ? "Everything" + (args.Database ? "" : " but Database") : "Database") + "): Complete!");
Server.s.Log("Server backed up!");
}// end:CreatePackage()
static List<Uri> GetAllFiles(DirectoryInfo currDir, Uri baseUri) {
List<Uri> uriList = new List<Uri>();
foreach (FileSystemInfo entry in currDir.GetFileSystemInfos()) {
if (entry is FileInfo) {
// Make a relative URI
Uri fullURI = new Uri(((FileInfo)entry).FullName);
Uri relURI = baseUri.MakeRelativeUri(fullURI);
if (relURI.ToString().IndexOfAny("/\\".ToCharArray()) > 0) {
uriList.Add(PackUriHelper.CreatePartUri(relURI));
}
} else {
uriList.AddRange(GetAllFiles((DirectoryInfo)entry, baseUri));
}
}
return uriList;
}
static void CopyStream(Stream source, Stream target) {
const int bufSize = 0x1000;
byte[] buf = new byte[bufSize];
int bytesRead = 0;
while ((bytesRead = source.Read(buf, 0, bufSize)) > 0)
target.Write(buf, 0, bytesRead);
}
public static void ExtractPackage(object p) {
int errors = 0;
using (ZipPackage zip = (ZipPackage)ZipPackage.Open(File.OpenRead(path))) {
PackagePartCollection pc = zip.GetParts();
foreach (ZipPackagePart item in pc) {
try {
CopyStream(item.GetStream(), File.Create("./" + Uri.UnescapeDataString(item.Uri.ToString())));
} catch {
try {
Directory.CreateDirectory("./" + item.Uri.ToString().Substring(0, item.Uri.ToString().LastIndexOfAny("\\/".ToCharArray())));
CopyStream(item.GetStream(), File.Create("./" + Uri.UnescapeDataString(item.Uri.ToString())));
} catch (IOException e) {
Server.ErrorLog(e);
Server.s.Log("Caught ignoreable Error. See log for more details. Will continue with rest of files.");
errors++;
}
}
// To make life easier, we reload settings now, to maker it less likely to need restart
Command.all.Find("server").Use(null, "reload"); //Reload, as console
if (item.Uri.ToString().ToLower().Contains("sql.sql"))
{ // If it's in there, they backed it up, meaning they want it restored
Backup.fillDatabase(item.GetStream());
}
}
}
Player.Message((Player)p, "Server restored" + (errors > 0 ? " with errors. May be a partial restore" : "") + ". Restart is reccommended, though not required.");
}
}
}

268
Server/BackupDB.cs Normal file
View File

@ -0,0 +1,268 @@
/*
Copyright 2011 MCForge
Dual-licensed under the Educational Community License, Version 2.0 and
the GNU General Public License, Version 3 (the "Licenses"); you may
not use this file except in compliance with the Licenses. You may
obtain a copy of the Licenses at
http://www.opensource.org/licenses/ecl2.php
http://www.gnu.org/licenses/gpl-3.0.html
Unless required by applicable law or agreed to in writing,
software distributed under the Licenses are distributed on an "AS IS"
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the Licenses for the specific language governing
permissions and limitations under the Licenses.
*/
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using MCGalaxy.SQL;
namespace MCGalaxy {
public static partial class Backup {
public static void BackupDatabase(StreamWriter sql, bool lite) {
//We technically know all tables in the DB... But since this is MySQL, we can also get them all with a MySQL command
//So we show the tables, and store the result.
//Also output information data (Same format as phpMyAdmin's dump)
//Important note: This does NOT account for foreign keys, BLOB's etc. It only works for what we actually put in the db.
sql.WriteLine("-- MCGalaxy SQL Database Dump");
sql.WriteLine("-- version 1.5");
sql.WriteLine("-- http://mcgalaxy.ml");
sql.WriteLine("--");
sql.WriteLine("-- Host: {0}", Server.MySQLHost);
sql.WriteLine("-- Generation Time: {0} at {1}", DateTime.Now.Date, DateTime.Now.TimeOfDay);
sql.WriteLine("-- MCGalaxy Version: {0}", Server.Version);
sql.WriteLine();
sql.WriteLine();
List<string> sqlTables = GetTables();
foreach (string name in sqlTables) {
if (lite && name.CaselessStarts("Block")) continue;
BackupTable(name, sql);
}
}
public static void BackupTable(string tableName, StreamWriter sql) {
//For each table, we iterate through all rows, (and save them)
sql.WriteLine("-- --------------------------------------------------------");
sql.WriteLine();
sql.WriteLine("--");
sql.WriteLine("-- Table structure for table `{0}`", tableName);
sql.WriteLine("--");
sql.WriteLine();
List<string[]> tableSchema = WriteTableSchema(tableName, sql);
using (DataTable data = Database.fillData("SELECT * FROM `" + tableName + "`")) {
if (data.Rows.Count == 0) {
sql.WriteLine("-- No data in table `{0}`!", tableName);
sql.WriteLine();
return;
}
sql.WriteLine("--");
sql.WriteLine("-- Dumping data for table `{0}`", tableName);
sql.WriteLine("--");
sql.WriteLine();
List<DataColumn> allCols = new List<DataColumn>();
foreach (DataColumn col in data.Columns)
allCols.Add(col);
foreach (DataRow row in data.Rows) { //We rely on the correct datatype being given here.
sql.WriteLine();
sql.Write("INSERT INTO `{0}` (`", tableName);
foreach (string[] rParams in tableSchema) {
sql.Write(rParams[0]);
sql.Write((tableSchema[tableSchema.Count - 1].Equals(rParams) ? "`) VALUES" : "`, `"));
}
sql.WriteLine();
sql.Write("(");
for (int col = 0; col < row.ItemArray.Length; col++) {
//The values themselves can be integers or strings, or null
Type eleType = allCols[col].DataType;
if (row.IsNull(col)) {
sql.Write("NULL");
} else if (eleType.Name.Equals("DateTime")) { // special format
DateTime dt = row.Field<DateTime>(col);
sql.Write("'{0:yyyy-MM-dd HH:mm:ss.ffff}'", dt);
} else if (eleType.Name.Equals("Boolean")) {
sql.Write(row.Field<Boolean>(col) ? "1" : "0");
} else if (eleType.Name.Equals("String")) { // Requires ''
sql.Write("'{0}'", row.Field<string>(col));
} else {
sql.Write(row.Field<Object>(col)); // We assume all other data is left as-is
//This includes numbers, blobs, etc. (As well as objects, but we don't save them into the database)
}
sql.Write((col < row.ItemArray.Length - 1 ? ", " : ");"));
}
}
sql.WriteLine();
}
}
static List<string[]> WriteTableSchema(string tableName, StreamWriter sql) {
List<string[]> tableSchema = new List<string[]>();
if (Server.useMySQL) {
string[] rowParams;
string pri;
sql.WriteLine("CREATE TABLE IF NOT EXISTS `{0}` (", tableName);
using (DataTable schema = Database.fillData("DESCRIBE " + tableName)) {
rowParams = new string[schema.Columns.Count];
pri = "";
foreach (DataRow row in schema.Rows) {
//Save the info contained to file
List<string> tmp = new List<string>();
for (int col = 0; col < schema.Columns.Count; col++)
tmp.Add(row.Field<string>(col));
rowParams = tmp.ToArray();
rowParams[2] = (rowParams[2].ToLower().Equals("no") ? "NOT " : "DEFAULT ") + "NULL";
pri += (rowParams[3].ToLower().Equals("pri") ? rowParams[0] + ";" : "");
sql.WriteLine("`{0}` {1} {2}" + (rowParams[5].Equals("") ? "" : " {5}") + (pri.Equals("") && row == schema.Rows[schema.Rows.Count - 1] ? "" : ","), rowParams);
tableSchema.Add(rowParams);
}
}
if (pri != "") {
string[] tmp = pri.Substring(0, pri.Length - 1).Split(';');
sql.Write("PRIMARY KEY (`");
foreach (string prim in tmp) {
sql.Write(prim);
sql.Write("`" + (tmp[tmp.Length - 1].Equals(prim) ? ")" : ", `"));
}
}
sql.WriteLine(");");
} else {
using (DataTable tableSQL = Database.fillData("SELECT sql FROM" +
" (SELECT * FROM sqlite_master UNION ALL" +
" SELECT * FROM sqlite_temp_master)" +
"WHERE tbl_name LIKE '" + tableName + "'" +
" AND type!='meta' AND sql NOT NULL AND name NOT LIKE 'sqlite_%'" +
"ORDER BY substr(type,2,1), name"))
{
//just print out the data in the table.
foreach (DataRow row in tableSQL.Rows) {
string tableSQLString = row.Field<string>(0);
sql.WriteLine(tableSQLString.Replace(" " + tableName, " `" + tableName + "`").Replace("CREATE TABLE `" + tableName + "`", "CREATE TABLE IF NOT EXISTS `" + tableName + "`") + ";");
//We parse this ourselves to find the actual types.
tableSchema = getSchema(tableSQLString);
}
}
}
sql.WriteLine();
return tableSchema;
}
static List<string[]> getSchema(string tableSQLString) {
// All SQL for creating tables looks like "CREATE TABLE [IF NOT EXISTS] <TableName> (<ColumnDef>[, ... [, PRIMARY KEY (<ColumnName>[, ...])]])
// <ColumnDef> = <name> <type> [[NOT|DEFAULT] NULL] [PRIMARY KEY] [AUTO_INCREMENT]
List<string[]> schema = new List<string[]>();
int foundStart = tableSQLString.IndexOf("(") + 1;
int foundLength = tableSQLString.LastIndexOf(")") - foundStart;
tableSQLString = tableSQLString.Substring(foundStart, foundLength);
// Now we have everything inside the parenthisies.
string[] column = tableSQLString.Split(',');
foreach (string col in column)
{
if (!col.ToUpper().StartsWith("PRIMARY KEY"))
{
string[] split = col.TrimStart('\n', '\r', '\t').Split(" ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
//Just to make it the same as the MySQL schema.
schema.Add(new string[] { split[0].Trim('`'), split[1].Trim('\t', '`'),
( split.Count() > 2 ? (split[2].Trim('\t', '`').ToUpper() == "NOT" ? "NOT NULL" : "DEFAULT NULL") : ""),
( split.Count() > 2 ? (split[split.Count() - 2].Trim('\t', '`').ToUpper() == "PRIMARY" && split[split.Count() - 1].Trim('\t', '`').ToUpper() == "KEY" ? "PRI" : "") : ""),
"NULL",
(split.Contains("AUTO_INCREMENT") || split.Contains("AUTOINCREMENT") ? "AUTO_INCREMENT" : "")});
}
}
return schema;
}
static List<string> GetTables() {
List<string> tableNames = new List<string>();
string syntax = Server.useMySQL ? "SHOW TABLES" : "SELECT * FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'";
using (DataTable tables = Database.fillData(syntax)) {
foreach (DataRow row in tables.Rows) {
string tableName = row.Field<string>((Server.useMySQL ? 0 : 1));
tableNames.Add(tableName);
}
}
return tableNames;
}
internal static void fillDatabase(Stream stream) {
//Backup
using (FileStream backup = File.Create("backup.sql"))
BackupDatabase(new StreamWriter(backup), false);
//Delete old
List<string> tables = GetTables();
foreach (string name in tables)
Database.executeQuery(String.Format("DROP TABLE `{0}`", name));
//Make new
string script = new StreamReader(stream).ReadToEnd();
string[] cmds = script.Split(';');
StringBuilder sb = new StringBuilder();
using (BulkTransaction helper = BulkTransaction.Create()) {
foreach (string cmd in cmds) {
string newCmd = cmd.Trim(" \r\n\t".ToCharArray());
int index = newCmd.ToUpper().IndexOf("CREATE TABLE");
if (index > -1) {
newCmd = newCmd.Remove(0, index);
//Do we have a primary key?
try
{
if (Server.useMySQL)
{
int priIndex = newCmd.ToUpper().IndexOf(" PRIMARY KEY AUTOINCREMENT");
int priCount = " PRIMARY KEY AUTOINCREMENT".Length;
int attIdx = newCmd.Substring(0, newCmd.Substring(0, priIndex - 1).LastIndexOfAny("` \n".ToCharArray())).LastIndexOfAny("` \n".ToCharArray()) + 1;
int attIdxEnd = newCmd.IndexOfAny("` \n".ToCharArray(), attIdx) - 1;
string attName = newCmd.Substring(attIdx, attIdxEnd - attIdx + 1).Trim(' ', '\n', '`', '\r');
//For speed, we just delete this, and add it to the attribute name, then delete the auto_increment clause.
newCmd = newCmd.Remove(priIndex, priCount);
newCmd = newCmd.Insert(newCmd.LastIndexOf(")"), ", PRIMARY KEY (`" + attName + "`)");
newCmd = newCmd.Insert(newCmd.IndexOf(',', priIndex), " AUTO_INCREMENT");
}
else
{
int priIndex = newCmd.ToUpper().IndexOf(",\r\nPRIMARY KEY");
int priIndexEnd = newCmd.ToUpper().IndexOf(')', priIndex);
int attIdx = newCmd.IndexOf("(", priIndex) + 1;
int attIdxEnd = priIndexEnd - 1;
string attName = newCmd.Substring(attIdx, attIdxEnd - attIdx + 1);
newCmd = newCmd.Remove(priIndex, priIndexEnd - priIndex + 1);
int start = newCmd.IndexOf(attName) + attName.Length;
int end = newCmd.IndexOf(',');
newCmd = newCmd.Remove(start, end - start);
newCmd = newCmd.Insert(newCmd.IndexOf(attName) + attName.Length, " INTEGER PRIMARY KEY AUTOINCREMENT");
newCmd = newCmd.Replace(" auto_increment", "").Replace(" AUTO_INCREMENT", "").Replace("True", "1").Replace("False", "0");
}
}
catch (ArgumentOutOfRangeException) { } // If we don't, just ignore it.
}
//Run the command in the transaction.
helper.Execute(newCmd.Replace(" unsigned", "").Replace(" UNSIGNED", "") + ";");
// sb.Append(newCmd).Append(";\n");
}
helper.Commit();
}
//Not sure if order matters.
//AUTO_INCREMENT is changed to AUTOINCREMENT for MySQL -> SQLite
//AUTOINCREMENT is changed to AUTO_INCREMENT for SQLite -> MySQL
// All we should have in the script file is CREATE TABLE and INSERT INTO commands.
//executeQuery(sb.ToString().Replace(" unsigned", "").Replace(" UNSIGNED", ""));
}
}
}