using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
namespace MesDbBackup
{
///
/// MES 数据库备份桌面客户端:窗口内完成连接、选目录和备份,不打开浏览器。
/// 实际导出仍调用 backup-db.ps1,不改原备份逻辑。
///
internal static class Program
{
internal const string MutexName = "Global\\MES_Cloud_DB_Backup_UI";
internal const string MarkerName = ".packed-by-mes-backup";
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
bool createdNew;
using (var mutex = new Mutex(true, MutexName, out createdNew))
{
if (!createdNew)
{
MessageBox.Show(
"备份客户端已经在运行,请查看任务栏。",
"MES 数据库备份",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
return;
}
Application.Run(new MainForm());
}
}
}
///
/// 深空 AI 主题:暗底玻璃卡片 + 青绿/紫罗兰高光。
///
internal static class Theme
{
public static readonly Color Bg = Color.FromArgb(7, 8, 13);
public static readonly Color Header = Color.FromArgb(9, 12, 20);
public static readonly Color HeaderText = Color.FromArgb(238, 244, 255);
public static readonly Color HeaderMuted = Color.FromArgb(139, 149, 171);
public static readonly Color Cyan = Color.FromArgb(94, 234, 212);
public static readonly Color CyanDeep = Color.FromArgb(34, 211, 238);
public static readonly Color Violet = Color.FromArgb(129, 140, 248);
public static readonly Color Ink = Color.FromArgb(6, 16, 24);
public static readonly Color Gold = Color.FromArgb(94, 234, 212);
public static readonly Color Card = Color.FromArgb(18, 22, 32);
public static readonly Color Line = Color.FromArgb(48, 80, 92, 108);
public static readonly Color GhostBorder = Color.FromArgb(70, 255, 255, 255);
public static readonly Color Text = Color.FromArgb(238, 244, 255);
public static readonly Color Muted = Color.FromArgb(139, 149, 171);
public static readonly Color Accent = Color.FromArgb(34, 211, 238);
public static readonly Color AccentHover = Color.FromArgb(94, 234, 212);
public static readonly Color Ok = Color.FromArgb(52, 211, 153);
public static readonly Color Warn = Color.FromArgb(251, 191, 36);
public static readonly Color Bad = Color.FromArgb(251, 113, 133);
public static readonly Color Input = Color.FromArgb(8, 10, 16);
public static readonly Color InputMuted = Color.FromArgb(12, 16, 24);
public static readonly Color Go = Color.FromArgb(34, 211, 238);
public static readonly Color LogBg = Color.FromArgb(5, 7, 12);
public static readonly Color LogText = Color.FromArgb(159, 232, 220);
public static readonly Color Shadow = Color.FromArgb(90, 0, 0, 0);
public static readonly Color Mesh = Color.FromArgb(18, 148, 163, 184);
public static readonly Font Ui = new Font("Microsoft YaHei UI", 10F);
public static readonly Font Title = new Font("Microsoft YaHei UI", 16F, FontStyle.Bold);
public static readonly Font Caption = new Font("Microsoft YaHei UI", 8.5F);
public static readonly Font Section = new Font("Microsoft YaHei UI", 11F, FontStyle.Bold);
public static readonly Font Mono = new Font("Consolas", 9F);
public static readonly Font Mark = new Font("Segoe UI", 16F, FontStyle.Bold);
public static GraphicsPath RoundRect(Rectangle bounds, int radius)
{
var path = new GraphicsPath();
int d = Math.Max(2, radius * 2);
path.AddArc(bounds.X, bounds.Y, d, d, 180, 90);
path.AddArc(bounds.Right - d, bounds.Y, d, d, 270, 90);
path.AddArc(bounds.Right - d, bounds.Bottom - d, d, d, 0, 90);
path.AddArc(bounds.X, bounds.Bottom - d, d, d, 90, 90);
path.CloseFigure();
return path;
}
}
///
/// 暗色玻璃卡片,顶部一道青绿高光。
///
internal sealed class CardPanel : Panel
{
public CardPanel()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
BackColor = Color.Transparent;
Padding = new Padding(8);
Margin = new Padding(0, 0, 0, 12);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
var rect = new Rectangle(8, 4, Math.Max(8, Width - 18), Math.Max(8, Height - 12));
using (GraphicsPath shadow = Theme.RoundRect(new Rectangle(rect.X + 3, rect.Y + 6, rect.Width, rect.Height), 16))
using (var brush = new SolidBrush(Theme.Shadow))
{
e.Graphics.FillPath(brush, shadow);
}
using (GraphicsPath path = Theme.RoundRect(rect, 16))
using (var fill = new SolidBrush(Theme.Card))
using (var pen = new Pen(Color.FromArgb(55, 94, 234, 212)))
{
e.Graphics.FillPath(fill, path);
e.Graphics.DrawPath(pen, path);
}
using (var glow = new Pen(Color.FromArgb(140, 94, 234, 212), 1F))
{
e.Graphics.DrawLine(glow, rect.X + 18, rect.Y + 1, rect.Right - 18, rect.Y + 1);
}
}
}
///
/// 青绿渐变进度条,替代系统默认绿条。
///
internal sealed class AiProgressBar : Control
{
int _value;
int _phase;
ProgressBarStyle _style = ProgressBarStyle.Continuous;
readonly System.Windows.Forms.Timer _tick;
public int Minimum;
public int Maximum = 100;
public AiProgressBar()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
Height = 8;
_tick = new System.Windows.Forms.Timer();
_tick.Interval = 28;
_tick.Tick += delegate
{
_phase = (_phase + 5) % 400;
Invalidate();
};
}
public int Value
{
get { return _value; }
set
{
_value = Math.Max(Minimum, Math.Min(Maximum, value));
Invalidate();
}
}
public ProgressBarStyle Style
{
get { return _style; }
set
{
_style = value;
if (_style == ProgressBarStyle.Marquee)
{
_tick.Start();
}
else
{
_tick.Stop();
}
Invalidate();
}
}
protected override void OnPaint(PaintEventArgs e)
{
if (Width < 4 || Height < 4)
{
return;
}
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
var bg = new Rectangle(0, 0, Math.Max(1, Width - 1), Math.Max(1, Height - 1));
using (GraphicsPath path = Theme.RoundRect(bg, Math.Max(2, Height / 2)))
using (var fill = new SolidBrush(Color.FromArgb(255, 12, 16, 24)))
{
e.Graphics.FillPath(fill, path);
}
if (_style == ProgressBarStyle.Marquee)
{
int w = Math.Max(48, Width / 3);
int x = (_phase * (Width + w) / 400) - w;
using (var lg = new LinearGradientBrush(new Rectangle(x, 0, w, Height), Theme.CyanDeep, Theme.Violet, 0f))
{
e.Graphics.FillRectangle(lg, new Rectangle(x, 1, w, Math.Max(1, Height - 2)));
}
}
else if (_value > 0 && Maximum > 0)
{
int w = Math.Max(4, (int)((Width - 2) * (_value / (double)Maximum)));
using (var lg = new LinearGradientBrush(new Rectangle(0, 0, w, Height), Theme.CyanDeep, Theme.Violet, 0f))
using (GraphicsPath path = Theme.RoundRect(new Rectangle(0, 0, w, Math.Max(1, Height - 1)), Math.Max(2, Height / 2)))
{
e.Graphics.FillPath(lg, path);
}
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_tick.Dispose();
}
base.Dispose(disposing);
}
}
internal sealed class DbConfig
{
public string Engine = "mysql";
public string Host = "127.0.0.1";
public int Port = 3306;
public string Database = "mes_cloud";
public string User = "mes_cloud";
public string Password = "s2ENj7KaEySTC7dW";
}
internal static class DbConfigStore
{
public static string ConfigPath(string workDir)
{
return Path.Combine(workDir, "db-config.json");
}
public static DbConfig Load(string workDir)
{
string path = ConfigPath(workDir);
if (!File.Exists(path))
{
return new DbConfig();
}
try
{
string raw = File.ReadAllText(path, Encoding.UTF8);
var lines = new List();
foreach (string line in raw.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None))
{
if (!line.TrimStart().StartsWith("#"))
{
lines.Add(line);
}
}
string json = string.Join("\n", lines.ToArray()).Trim();
int start = json.IndexOf('{');
int end = json.LastIndexOf('}');
if (start >= 0 && end > start)
{
json = json.Substring(start, end - start + 1);
}
var cfg = new DbConfig();
cfg.Host = ReadString(json, "host", cfg.Host);
cfg.Database = ReadString(json, "database", cfg.Database);
cfg.User = ReadString(json, "user", cfg.User);
cfg.Password = ReadString(json, "password", cfg.Password);
int port = ReadInt(json, "port", cfg.Port);
if (port < 1 || port > 65535)
{
throw new Exception("端口必须在 1-65535");
}
cfg.Port = port;
cfg.Host = NormalizeHost(cfg.Host);
if (string.IsNullOrWhiteSpace(cfg.User))
{
throw new Exception("请填写账号");
}
return cfg;
}
catch
{
return new DbConfig();
}
}
public static void Save(string workDir, DbConfig cfg)
{
cfg.Host = NormalizeHost(cfg.Host);
if (string.IsNullOrWhiteSpace(cfg.User))
{
throw new Exception("请填写账号");
}
if (cfg.Port < 1 || cfg.Port > 65535)
{
throw new Exception("端口必须在 1-65535");
}
string json =
"{\r\n" +
" \"engine\": \"mysql\",\r\n" +
" \"host\": \"" + Escape(cfg.Host) + "\",\r\n" +
" \"port\": " + cfg.Port + ",\r\n" +
" \"database\": \"" + Escape(cfg.Database) + "\",\r\n" +
" \"user\": \"" + Escape(cfg.User) + "\",\r\n" +
" \"password\": \"" + Escape(cfg.Password) + "\"\r\n" +
"}";
File.WriteAllText(ConfigPath(workDir), json, new UTF8Encoding(false));
}
static string NormalizeHost(string host)
{
if (string.IsNullOrWhiteSpace(host))
{
return "127.0.0.1";
}
host = host.Trim();
Match jdbc = Regex.Match(host, @"jdbc:mysql://([^:/]+)");
if (jdbc.Success)
{
host = jdbc.Groups[1].Value;
}
if (host.StartsWith("mysql://", StringComparison.OrdinalIgnoreCase))
{
host = host.Substring(8);
}
if (Regex.IsMatch(host, @"\s|/"))
{
throw new Exception("IP/主机名不合法");
}
return host;
}
static string ReadString(string json, string key, string fallback)
{
Match m = Regex.Match(json, "\"" + key + "\"\\s*:\\s*\"((?:\\\\.|[^\"\\\\])*)\"");
if (!m.Success)
{
return fallback;
}
return Unescape(m.Groups[1].Value);
}
static int ReadInt(string json, string key, int fallback)
{
Match m = Regex.Match(json, "\"" + key + "\"\\s*:\\s*(-?\\d+)");
int n;
if (m.Success && int.TryParse(m.Groups[1].Value, out n))
{
return n;
}
return fallback;
}
static string Escape(string s)
{
if (s == null)
{
return "";
}
return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r").Replace("\n", "\\n");
}
static string Unescape(string s)
{
var sb = new StringBuilder();
for (int i = 0; i < s.Length; i++)
{
if (s[i] == '\\' && i + 1 < s.Length)
{
char n = s[++i];
if (n == '"') { sb.Append('"'); }
else if (n == '\\') { sb.Append('\\'); }
else if (n == 'n') { sb.Append('\n'); }
else if (n == 'r') { sb.Append('\r'); }
else if (n == 't') { sb.Append('\t'); }
else { sb.Append(n); }
}
else
{
sb.Append(s[i]);
}
}
return sb.ToString();
}
}
internal static class MysqlUtil
{
static readonly Dictionary Cache = new Dictionary(StringComparer.OrdinalIgnoreCase);
public static string FindTool(string name)
{
if (Cache.ContainsKey(name))
{
return Cache[name];
}
string exeName = name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? name : name + ".exe";
string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? "";
foreach (string dir in pathEnv.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
try
{
string p = Path.Combine(dir.Trim().Trim('"'), exeName);
if (File.Exists(p))
{
Cache[name] = p;
return p;
}
}
catch { }
}
string[] roots = new[]
{
@"C:\Program Files\MySQL",
@"C:\Program Files (x86)\MySQL",
@"C:\mysql",
@"D:\MySQL",
@"E:\MySQL",
@"C:\Program Files\MariaDB"
};
foreach (string root in roots)
{
if (!Directory.Exists(root))
{
continue;
}
try
{
foreach (string file in Directory.GetFiles(root, exeName, SearchOption.AllDirectories))
{
string bin = Path.GetFileName(Path.GetDirectoryName(file));
if (string.Equals(bin, "bin", StringComparison.OrdinalIgnoreCase))
{
Cache[name] = file;
return file;
}
}
}
catch { }
}
return null;
}
public static void WriteCnf(DbConfig cfg, string cnfPath)
{
string pwd = cfg.Password ?? "";
pwd = pwd.Replace("\\", "\\\\").Replace("\"", "\\\"");
string text =
"[client]\r\n" +
"host=" + cfg.Host + "\r\n" +
"port=" + cfg.Port + "\r\n" +
"user=" + cfg.User + "\r\n" +
"password=\"" + pwd + "\"\r\n" +
"default-character-set=utf8mb4\r\n";
File.WriteAllText(cnfPath, text, Encoding.ASCII);
}
public static string TestConnection(DbConfig cfg)
{
string mysql = FindTool("mysql");
if (mysql == null)
{
throw new Exception("找不到 mysql.exe,请安装 MySQL 客户端");
}
string cnf = Path.Combine(Path.GetTempPath(), "mes_ui_test_" + Process.GetCurrentProcess().Id + ".cnf");
WriteCnf(cfg, cnf);
try
{
string output;
int code = Run(mysql, new[]
{
"--defaults-extra-file=" + cnf,
"--connect-timeout=5",
"-N", "-s", "-e", "SELECT 1"
}, 15000, out output);
if (code != 0)
{
throw new Exception("连接失败: " + (output ?? "").Trim());
}
}
finally
{
TryDelete(cnf);
}
return "MySQL 连接成功: " + cfg.Host + ":" + cfg.Port + " / " + cfg.Database;
}
public static long GetDbSizeBytes(DbConfig cfg)
{
try
{
string mysql = FindTool("mysql");
if (mysql == null)
{
return 0;
}
string cnf = Path.Combine(Path.GetTempPath(), "mes_ui_size_" + Process.GetCurrentProcess().Id + ".cnf");
WriteCnf(cfg, cnf);
try
{
string db = (cfg.Database ?? "").Replace("'", "''");
string sql = "SELECT IFNULL(SUM(data_length+index_length),0) FROM information_schema.tables WHERE table_schema='" + db + "'";
string output;
int code = Run(mysql, new[]
{
"--defaults-extra-file=" + cnf,
"--connect-timeout=3",
"-N", "-s", "-e", sql
}, 8000, out output);
if (code != 0)
{
return 0;
}
long n;
if (long.TryParse((output ?? "").Trim(), out n))
{
return n;
}
}
finally
{
TryDelete(cnf);
}
}
catch { }
return 0;
}
static int Run(string file, string[] args, int timeoutMs, out string output)
{
var psi = new ProcessStartInfo();
psi.FileName = file;
psi.Arguments = QuoteArgs(args);
psi.UseShellExecute = false;
psi.CreateNoWindow = true;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
psi.StandardOutputEncoding = Encoding.UTF8;
psi.StandardErrorEncoding = Encoding.UTF8;
using (var proc = new Process())
{
proc.StartInfo = psi;
var sb = new StringBuilder();
proc.OutputDataReceived += delegate(object s, DataReceivedEventArgs e)
{
if (!string.IsNullOrEmpty(e.Data)) { sb.AppendLine(e.Data); }
};
proc.ErrorDataReceived += delegate(object s, DataReceivedEventArgs e)
{
if (!string.IsNullOrEmpty(e.Data)) { sb.AppendLine(e.Data); }
};
proc.Start();
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
if (!proc.WaitForExit(timeoutMs))
{
try { proc.Kill(); } catch { }
throw new Exception("等待超时");
}
proc.WaitForExit();
output = sb.ToString();
return proc.ExitCode;
}
}
static string QuoteArgs(string[] args)
{
var sb = new StringBuilder();
for (int i = 0; i < args.Length; i++)
{
if (i > 0) { sb.Append(' '); }
string a = args[i] ?? "";
sb.Append('"').Append(a.Replace("\"", "\\\"")).Append('"');
}
return sb.ToString();
}
static void TryDelete(string path)
{
try { if (File.Exists(path)) { File.Delete(path); } } catch { }
}
}
internal sealed class MainForm : Form
{
static readonly string[] PackedFiles = new string[]
{
"backup-ui-server.ps1",
"backup-db.ps1",
"db-config.ps1",
"backup-ui.html",
"restore-db.ps1",
"install-backup-task.bat",
"使用说明.txt"
};
readonly string _workDir;
readonly TextBox _host;
readonly TextBox _port;
readonly TextBox _database;
readonly TextBox _user;
readonly TextBox _password;
readonly Label _cfgStatus;
readonly Label _picked;
readonly Label _spaceHint;
readonly Label _progHint;
readonly Label _runStatus;
readonly CheckBox _keepOld;
readonly AiProgressBar _bar;
readonly TextBox _log;
readonly Button _btnTest;
readonly Button _btnSave;
readonly Button _btnBrowse;
readonly Button _btnBackup;
readonly System.Windows.Forms.Timer _poll;
string _selectedPath = "";
Process _backupProc;
string _outFile = "";
DateTime _startedAt;
long _dbBytes;
bool _running;
public MainForm()
{
string exeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
if (string.IsNullOrEmpty(exeDir))
{
exeDir = AppDomain.CurrentDomain.BaseDirectory;
}
_workDir = PrepareWorkDir(exeDir);
Text = "MES 智能备份控制台";
Font = Theme.Ui;
BackColor = Theme.Bg;
ForeColor = Theme.Text;
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(1020, 680);
Size = new Size(1140, 740);
DoubleBuffered = true;
var header = new Panel
{
Dock = DockStyle.Top,
Height = 96,
BackColor = Theme.Header
};
header.Paint += delegate(object s, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
using (var g = new LinearGradientBrush(header.ClientRectangle,
Color.FromArgb(10, 14, 24), Color.FromArgb(18, 12, 36), LinearGradientMode.Horizontal))
{
e.Graphics.FillRectangle(g, header.ClientRectangle);
}
using (var glow = new Pen(Color.FromArgb(180, 94, 234, 212), 2F))
{
e.Graphics.DrawLine(glow, 0, 0, header.Width, 0);
}
using (var line = new Pen(Color.FromArgb(60, 129, 140, 248)))
{
e.Graphics.DrawLine(line, 0, header.Height - 1, header.Width, header.Height - 1);
}
var markRect = new Rectangle(26, 24, 48, 48);
using (GraphicsPath path = Theme.RoundRect(markRect, 14))
using (var g = new LinearGradientBrush(markRect, Theme.Cyan, Theme.Violet, 145f))
{
e.Graphics.FillPath(g, path);
}
TextRenderer.DrawText(e.Graphics, "M", Theme.Mark, markRect, Theme.Ink,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
TextRenderer.DrawText(e.Graphics, "NEURAL ARCHIVE", Theme.Caption, new Point(86, 16), Theme.Cyan);
TextRenderer.DrawText(e.Graphics, "MES 智能备份控制台", Theme.Title, new Point(84, 34), Theme.HeaderText);
TextRenderer.DrawText(e.Graphics, "独立工具 · 仅 MySQL · 不接入生产系统", Theme.Caption, new Point(86, 66), Theme.HeaderMuted);
string[] chips = new[] { "AIR-GAPPED", "MYSQL", "LOCAL ONLY" };
int x = header.Width - 22;
for (int i = 0; i < chips.Length; i++)
{
Size sz = TextRenderer.MeasureText(chips[i], Theme.Caption);
int w = sz.Width + 16;
x -= w + 8;
var chipRect = new Rectangle(x, 36, w, 24);
using (GraphicsPath path = Theme.RoundRect(chipRect, 12))
using (var fill = new SolidBrush(Color.FromArgb(40, 255, 255, 255)))
using (var pen = new Pen(i == 2 ? Color.FromArgb(120, 94, 234, 212) : Color.FromArgb(50, 255, 255, 255)))
{
e.Graphics.FillPath(fill, path);
e.Graphics.DrawPath(pen, path);
}
TextRenderer.DrawText(e.Graphics, chips[i], Theme.Caption, chipRect,
i == 2 ? Theme.Cyan : Theme.Muted,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter);
}
};
var grid = new TableLayoutPanel();
grid.Dock = DockStyle.Fill;
grid.ColumnCount = 2;
grid.RowCount = 1;
grid.BackColor = Theme.Bg;
grid.Padding = new Padding(18, 12, 14, 12);
grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 56F));
grid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 44F));
grid.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
grid.Paint += PaintMesh;
_host = MakeInput();
_port = MakeInput();
_database = MakeInput();
_user = MakeInput();
_password = MakeInput();
_password.UseSystemPasswordChar = true;
var engine = MakeInput();
engine.Text = "MySQL";
engine.ReadOnly = true;
engine.BackColor = Theme.InputMuted;
engine.ForeColor = Theme.Muted;
engine.TabStop = false;
var connCard = MakeCard();
var connBody = MakeBody();
connBody.RowCount = 4;
connBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
connBody.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
connBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 42));
connBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 24));
var connHead = MakeHead("01", "连接矩阵", "先测试,再保存", null);
var form = new TableLayoutPanel();
form.Dock = DockStyle.Fill;
form.ColumnCount = 2;
form.RowCount = 3;
form.BackColor = Theme.Card;
form.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
form.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
for (int i = 0; i < 3; i++)
{
form.RowStyles.Add(new RowStyle(SizeType.Percent, 33.3F));
}
form.Controls.Add(MakeField("类型", engine), 0, 0);
form.Controls.Add(MakeField("端口", _port), 1, 0);
form.Controls.Add(MakeField("IP / 主机", _host), 0, 1);
form.Controls.Add(MakeField("库名", _database), 1, 1);
form.Controls.Add(MakeField("账号", _user), 0, 2);
form.Controls.Add(MakeField("密码", _password), 1, 2);
var connActions = new Panel { Dock = DockStyle.Fill, BackColor = Theme.Card };
_btnTest = MakeButton("测试连接", false);
_btnTest.Location = new Point(4, 4);
_btnSave = MakeGoldButton("保存配置");
_btnSave.Location = new Point(122, 4);
_btnTest.Click += OnTest;
_btnSave.Click += OnSave;
connActions.Controls.Add(_btnTest);
connActions.Controls.Add(_btnSave);
_cfgStatus = new Label
{
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft,
ForeColor = Theme.Muted,
BackColor = Theme.Card,
Padding = new Padding(6, 0, 0, 0),
Text = "填写 IP、账号、密码后点「测试连接」"
};
connBody.Controls.Add(connHead, 0, 0);
connBody.Controls.Add(form, 0, 1);
connBody.Controls.Add(connActions, 0, 2);
connBody.Controls.Add(_cfgStatus, 0, 3);
connCard.Controls.Add(connBody);
var bakCard = MakeCard();
var bakBody = MakeBody();
bakBody.RowCount = 7;
bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 40));
bakBody.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 22));
bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 28));
bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 12));
bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 22));
bakBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 48));
_btnBrowse = MakeButton("浏览文件夹", false);
_btnBrowse.Width = 128;
_btnBrowse.Click += OnBrowse;
var bakHead = MakeHead("02", "归档目标", null, _btnBrowse);
_picked = new Label
{
Dock = DockStyle.Fill,
BackColor = Theme.Input,
ForeColor = Theme.Muted,
Text = " 尚未选择保存位置",
TextAlign = ContentAlignment.MiddleLeft,
Padding = new Padding(12, 0, 12, 0)
};
_picked.Paint += delegate(object s, PaintEventArgs e)
{
using (var p = new Pen(Color.FromArgb(90, 94, 234, 212)) { DashStyle = DashStyle.Dash })
{
e.Graphics.DrawRectangle(p, 1, 1, _picked.Width - 3, _picked.Height - 3);
}
};
_spaceHint = new Label
{
Dock = DockStyle.Fill,
ForeColor = Theme.Muted,
BackColor = Theme.Card,
TextAlign = ContentAlignment.MiddleLeft,
Text = "插入 U 盘或移动硬盘后选择目录,无需手输路径。"
};
_keepOld = new CheckBox
{
Text = "同时删除该目录中超过 7 天的旧备份(U 盘建议不勾选)",
ForeColor = Theme.Muted,
Dock = DockStyle.Fill,
BackColor = Theme.Card,
FlatStyle = FlatStyle.Flat,
TextAlign = ContentAlignment.MiddleLeft
};
_bar = new AiProgressBar
{
Dock = DockStyle.Fill,
Minimum = 0,
Maximum = 100
};
_progHint = new Label
{
Dock = DockStyle.Fill,
ForeColor = Theme.Muted,
BackColor = Theme.Card,
TextAlign = ContentAlignment.MiddleLeft,
Text = ""
};
var bakGo = new Panel { Dock = DockStyle.Fill, BackColor = Theme.Card };
_btnBackup = MakeButton("开始备份", true);
_btnBackup.Width = 148;
_btnBackup.Height = 36;
_btnBackup.Font = new Font("Microsoft YaHei UI", 11F, FontStyle.Bold);
_btnBackup.Location = new Point(4, 4);
_btnBackup.Click += OnBackup;
_runStatus = new Label
{
AutoSize = false,
Location = new Point(164, 10),
Size = new Size(360, 24),
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
ForeColor = Theme.Muted,
BackColor = Theme.Card,
Text = ""
};
bakGo.Controls.Add(_btnBackup);
bakGo.Controls.Add(_runStatus);
bakBody.Controls.Add(bakHead, 0, 0);
bakBody.Controls.Add(_picked, 0, 1);
bakBody.Controls.Add(_spaceHint, 0, 2);
bakBody.Controls.Add(_keepOld, 0, 3);
bakBody.Controls.Add(_bar, 0, 4);
bakBody.Controls.Add(_progHint, 0, 5);
bakBody.Controls.Add(bakGo, 0, 6);
bakCard.Controls.Add(bakBody);
var logCard = MakeCard();
var logBody = MakeBody();
logBody.RowCount = 2;
logBody.RowStyles.Add(new RowStyle(SizeType.Absolute, 36));
logBody.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
logBody.Controls.Add(MakeHead("03", "运行控制台", "实时输出", null), 0, 0);
_log = new TextBox
{
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Vertical,
BorderStyle = BorderStyle.None,
Dock = DockStyle.Fill,
BackColor = Theme.LogBg,
ForeColor = Theme.LogText,
Font = Theme.Mono,
Text = "等待操作…"
};
logBody.Controls.Add(_log, 0, 1);
logCard.Controls.Add(logBody);
var left = new TableLayoutPanel();
left.Dock = DockStyle.Fill;
left.BackColor = Color.Transparent;
left.ColumnCount = 1;
left.RowCount = 2;
left.RowStyles.Add(new RowStyle(SizeType.Percent, 54F));
left.RowStyles.Add(new RowStyle(SizeType.Percent, 46F));
left.Controls.Add(connCard, 0, 0);
left.Controls.Add(bakCard, 0, 1);
grid.Controls.Add(left, 0, 0);
grid.Controls.Add(logCard, 1, 0);
Controls.Add(grid);
Controls.Add(header);
_poll = new System.Windows.Forms.Timer();
_poll.Interval = 1000;
_poll.Tick += OnPoll;
Load += delegate { LoadConfigToUi(); };
FormClosing += OnClosing;
}
static string PrepareWorkDir(string exeDir)
{
string serverScript = Path.Combine(exeDir, "backup-db.ps1");
string marker = Path.Combine(exeDir, Program.MarkerName);
bool original = File.Exists(serverScript) && !File.Exists(marker);
if (!original)
{
ExtractPackedFiles(exeDir);
File.WriteAllText(marker, "packed", Encoding.ASCII);
}
return exeDir;
}
static void ExtractPackedFiles(string dir)
{
Assembly asm = Assembly.GetExecutingAssembly();
foreach (string name in PackedFiles)
{
Stream src = asm.GetManifestResourceStream(name);
if (src == null)
{
continue;
}
using (src)
using (FileStream fs = File.Create(Path.Combine(dir, name)))
{
src.CopyTo(fs);
}
}
}
static CardPanel MakeCard()
{
return new CardPanel { Dock = DockStyle.Fill };
}
static TableLayoutPanel MakeBody()
{
var body = new TableLayoutPanel();
body.Dock = DockStyle.Fill;
body.ColumnCount = 1;
body.BackColor = Theme.Card;
body.Padding = new Padding(10, 6, 12, 8);
body.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
return body;
}
///
/// 卡片标题行:序号 + 标题 + 说明,右侧可放按钮。
///
static Panel MakeHead(string step, string title, string hint, Control right)
{
var head = new Panel { Dock = DockStyle.Fill, BackColor = Theme.Card };
var st = MakeStep(step);
st.Location = new Point(2, 8);
var t = SectionLabel(title, 38, 8);
head.Controls.Add(st);
head.Controls.Add(t);
if (!string.IsNullOrEmpty(hint))
{
int titleW = TextRenderer.MeasureText(title, Theme.Section).Width;
var h = new Label
{
Text = hint,
Font = Theme.Caption,
ForeColor = Theme.Muted,
AutoSize = true,
Location = new Point(46 + titleW, 12),
BackColor = Theme.Card
};
head.Controls.Add(h);
}
if (right != null)
{
right.Anchor = AnchorStyles.Top | AnchorStyles.Right;
head.Controls.Add(right);
EventHandler place = delegate
{
right.Location = new Point(Math.Max(120, head.Width - right.Width - 4), 4);
};
head.Resize += place;
place(head, EventArgs.Empty);
}
return head;
}
///
/// 标签在上、输入在下,两列等宽,避免右侧字段被挤没。
///
static Panel MakeField(string caption, Control input)
{
var box = new Panel();
box.Dock = DockStyle.Fill;
box.BackColor = Theme.Card;
box.Margin = new Padding(4, 2, 8, 2);
box.Padding = new Padding(0, 0, 4, 2);
var lb = new Label();
lb.Text = caption;
lb.ForeColor = Theme.Muted;
lb.Font = Theme.Caption;
lb.Dock = DockStyle.Top;
lb.Height = 20;
lb.BackColor = Theme.Card;
lb.TextAlign = ContentAlignment.BottomLeft;
input.Dock = DockStyle.Fill;
box.Controls.Add(input);
box.Controls.Add(lb);
return box;
}
static Label MakeStep(string no)
{
return new Label
{
Text = no,
Font = new Font("Segoe UI", 8.5F, FontStyle.Bold),
ForeColor = Theme.Ink,
BackColor = Theme.Cyan,
Size = new Size(28, 22),
Location = new Point(0, 6),
TextAlign = ContentAlignment.MiddleCenter
};
}
static void PaintMesh(object sender, PaintEventArgs e)
{
var c = sender as Control;
if (c == null)
{
return;
}
using (var p = new Pen(Theme.Mesh))
{
for (int x = 0; x < c.Width; x += 42)
{
e.Graphics.DrawLine(p, x, 0, x, c.Height);
}
for (int y = 0; y < c.Height; y += 42)
{
e.Graphics.DrawLine(p, 0, y, c.Width, y);
}
}
}
static Label SectionLabel(string text, int x, int y)
{
return new Label
{
Text = text,
Font = Theme.Section,
ForeColor = Theme.Text,
AutoSize = true,
Location = new Point(x, y),
BackColor = Theme.Card
};
}
static Label FieldLabel(string text)
{
return new Label
{
Text = text,
ForeColor = Theme.Muted,
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft,
BackColor = Theme.Card
};
}
static TextBox MakeInput()
{
return new TextBox
{
Dock = DockStyle.Fill,
BorderStyle = BorderStyle.FixedSingle,
BackColor = Theme.Input,
ForeColor = Theme.Text,
Margin = new Padding(0)
};
}
static Button MakeButton(string text, bool primary)
{
var b = new Button();
b.Text = text;
b.Width = 110;
b.Height = 32;
b.FlatStyle = FlatStyle.Flat;
b.Cursor = Cursors.Hand;
if (primary)
{
b.FlatAppearance.BorderSize = 0;
b.BackColor = Theme.CyanDeep;
b.ForeColor = Theme.Ink;
b.MouseEnter += delegate { if (b.Enabled) { b.BackColor = Theme.Cyan; } };
b.MouseLeave += delegate { b.BackColor = Theme.CyanDeep; };
}
else
{
b.FlatAppearance.BorderSize = 1;
b.FlatAppearance.BorderColor = Color.FromArgb(70, 255, 255, 255);
b.BackColor = Color.FromArgb(18, 22, 32);
b.ForeColor = Theme.Text;
b.MouseEnter += delegate { if (b.Enabled) { b.BackColor = Color.FromArgb(28, 36, 52); } };
b.MouseLeave += delegate { b.BackColor = Color.FromArgb(18, 22, 32); };
}
return b;
}
static Button MakeGoldButton(string text)
{
var b = new Button();
b.Text = text;
b.Width = 110;
b.Height = 32;
b.FlatStyle = FlatStyle.Flat;
b.FlatAppearance.BorderSize = 0;
b.BackColor = Theme.Cyan;
b.ForeColor = Theme.Ink;
b.Cursor = Cursors.Hand;
b.MouseEnter += delegate { if (b.Enabled) { b.BackColor = Theme.CyanDeep; } };
b.MouseLeave += delegate { b.BackColor = Theme.Cyan; };
return b;
}
DbConfig ReadUiConfig()
{
var cfg = new DbConfig();
cfg.Host = (_host.Text ?? "").Trim();
int port;
if (!int.TryParse((_port.Text ?? "").Trim(), out port))
{
port = 3306;
}
cfg.Port = port;
cfg.Database = (_database.Text ?? "").Trim();
cfg.User = (_user.Text ?? "").Trim();
cfg.Password = _password.Text ?? "";
return cfg;
}
void LoadConfigToUi()
{
DbConfig cfg = DbConfigStore.Load(_workDir);
_host.Text = cfg.Host;
_port.Text = cfg.Port.ToString();
_database.Text = cfg.Database;
_user.Text = cfg.User;
_password.Text = cfg.Password;
}
void OnTest(object sender, EventArgs e)
{
_btnTest.Enabled = false;
SetCfg("正在测试连接,请稍候…", Theme.Warn);
DbConfig cfg = ReadUiConfig();
ThreadPool.QueueUserWorkItem(delegate
{
try
{
string msg = MysqlUtil.TestConnection(cfg);
BeginInvoke(new Action(delegate
{
SetCfg(msg, Theme.Ok);
_btnTest.Enabled = true;
}));
}
catch (Exception ex)
{
BeginInvoke(new Action(delegate
{
SetCfg(ex.Message, Theme.Bad);
_btnTest.Enabled = true;
}));
}
});
}
void OnSave(object sender, EventArgs e)
{
try
{
DbConfig cfg = ReadUiConfig();
DbConfigStore.Save(_workDir, cfg);
SetCfg("已保存 MySQL 配置", Theme.Ok);
}
catch (Exception ex)
{
SetCfg(ex.Message, Theme.Bad);
}
}
void OnBrowse(object sender, EventArgs e)
{
using (var dlg = new FolderBrowserDialog())
{
dlg.Description = "选择备份文件保存位置(可选手动插入的 U 盘或移动硬盘)";
dlg.ShowNewFolderButton = true;
dlg.RootFolder = Environment.SpecialFolder.MyComputer;
if (!string.IsNullOrEmpty(_selectedPath) && Directory.Exists(_selectedPath))
{
dlg.SelectedPath = _selectedPath;
}
if (dlg.ShowDialog(this) != DialogResult.OK)
{
return;
}
try
{
_selectedPath = GetSafePath(dlg.SelectedPath);
_picked.Text = " " + _selectedPath;
_picked.ForeColor = Theme.Text;
long free = GetFreeBytes(_selectedPath);
if (free > 0)
{
_spaceHint.Text = "该磁盘剩余 " + Gb(free) + " GB。";
}
SetRun("已选择保存位置", Theme.Ok);
}
catch (Exception ex)
{
SetRun(ex.Message, Theme.Bad);
}
}
}
void OnBackup(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(_selectedPath))
{
SetRun("请先点「浏览文件夹」选择保存位置", Theme.Bad);
return;
}
string script = Path.Combine(_workDir, "backup-db.ps1");
if (!File.Exists(script))
{
SetRun("找不到 backup-db.ps1", Theme.Bad);
return;
}
try
{
DbConfig cfg = ReadUiConfig();
DbConfigStore.Save(_workDir, cfg);
string dir = GetSafePath(_selectedPath);
long free = GetFreeBytes(dir);
_dbBytes = MysqlUtil.GetDbSizeBytes(cfg);
if (_dbBytes > 0 && free > 0 && free < (long)(_dbBytes * 1.2))
{
throw new Exception("目标盘剩余空间不足:约需 " + Gb((long)(_dbBytes * 1.2)) +
" GB,当前剩余 " + Gb(free) + " GB。请换更大的 U 盘/硬盘。");
}
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
int keep = _keepOld.Checked ? 7 : 0;
string powershell = Path.Combine(Environment.SystemDirectory, @"WindowsPowerShell\v1.0\powershell.exe");
if (!File.Exists(powershell))
{
powershell = "powershell.exe";
}
_outFile = Path.Combine(Path.GetTempPath(), "mes_backup_ui_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".log");
string errFile = _outFile + ".err";
TryDelete(_outFile);
TryDelete(errFile);
var psi = new ProcessStartInfo();
psi.FileName = powershell;
psi.Arguments = "-NoProfile -STA -ExecutionPolicy Bypass -File \"" + script +
"\" -BackupDir \"" + dir + "\" -KeepDaysParam " + keep;
psi.WorkingDirectory = _workDir;
psi.UseShellExecute = false;
psi.CreateNoWindow = true;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
psi.StandardOutputEncoding = Encoding.UTF8;
psi.StandardErrorEncoding = Encoding.UTF8;
_backupProc = new Process();
_backupProc.StartInfo = psi;
_backupProc.EnableRaisingEvents = true;
_backupProc.OutputDataReceived += OnBackupOutput;
_backupProc.ErrorDataReceived += OnBackupOutput;
if (!_backupProc.Start())
{
throw new Exception("无法启动备份进程");
}
_backupProc.BeginOutputReadLine();
_backupProc.BeginErrorReadLine();
_running = true;
_startedAt = DateTime.Now;
_selectedPath = dir;
_btnBackup.Enabled = false;
_bar.Style = ProgressBarStyle.Marquee;
_bar.Value = 0;
SetLog(new[] { "开始备份到: " + dir });
if (_dbBytes > 0)
{
AppendLog("库数据约 " + Gb(_dbBytes) + " GB,采用边导出边压缩,请耐心等待,勿拔盘");
}
SetRun("正在备份,数据量大时可能需要较长时间...", Theme.Warn);
_poll.Start();
}
catch (Exception ex)
{
SetRun(ex.Message, Theme.Bad);
_btnBackup.Enabled = true;
}
}
void OnBackupOutput(object sender, DataReceivedEventArgs e)
{
if (string.IsNullOrEmpty(e.Data))
{
return;
}
BeginInvoke(new Action(delegate { AppendLog(e.Data); }));
}
void OnPoll(object sender, EventArgs e)
{
if (!_running)
{
return;
}
long written;
string fileName;
GetWrittenBackup(_selectedPath, out written, out fileName);
if (_dbBytes > 0 && written > 1024)
{
_bar.Style = ProgressBarStyle.Continuous;
int pct = (int)Math.Max(2, Math.Min(99, written * 100 / _dbBytes));
_bar.Value = pct;
}
_progHint.Text = "已写入 " + Mb(written) + " MB" + (string.IsNullOrEmpty(fileName) ? "" : "(" + fileName + ")") + ",请勿拔盘";
Process p = _backupProc;
bool exited = false;
int code = -1;
if (p == null)
{
exited = true;
}
else
{
try
{
p.Refresh();
exited = p.HasExited;
if (exited)
{
try { p.WaitForExit(500); } catch { }
try { code = p.ExitCode; } catch { code = -1; }
}
}
catch
{
exited = true;
}
}
if (!exited)
{
return;
}
_poll.Stop();
_running = false;
_btnBackup.Enabled = true;
_bar.Style = ProgressBarStyle.Continuous;
bool fileOk = TestBackupFileOk(_selectedPath, _startedAt);
bool success = (code == 0) || fileOk;
if (success)
{
_bar.Value = 100;
GetWrittenBackup(_selectedPath, out written, out fileName);
_progHint.Text = "完成,文件约 " + Mb(written) + " MB:" + (fileName ?? "");
AppendLog("备份完成");
SetRun("备份完成", Theme.Ok);
}
else
{
string codeText = code < 0 ? "未知" : code.ToString();
string err = "备份失败,退出码 " + codeText;
AppendLog(err);
SetRun(err, Theme.Bad);
}
_backupProc = null;
}
void OnClosing(object sender, FormClosingEventArgs e)
{
if (_running)
{
DialogResult r = MessageBox.Show(this,
"备份正在进行,关闭窗口会中断备份。确定退出吗?",
"MES 数据库备份",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (r != DialogResult.Yes)
{
e.Cancel = true;
return;
}
}
_poll.Stop();
Process p = _backupProc;
_backupProc = null;
if (p != null)
{
try
{
if (!p.HasExited)
{
KillProcessTree(p.Id);
}
}
catch { }
try { p.Dispose(); } catch { }
}
}
static void GetWrittenBackup(string dir, out long bytes, out string name)
{
bytes = 0;
name = "";
if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir))
{
return;
}
FileInfo pick = null;
try
{
var files = new List(new DirectoryInfo(dir).GetFiles("mes_cloud_*.sql.gz*"));
files.Sort(delegate(FileInfo a, FileInfo b) { return b.LastWriteTime.CompareTo(a.LastWriteTime); });
foreach (FileInfo f in files)
{
if (f.Name.IndexOf(".tmp", StringComparison.OrdinalIgnoreCase) < 0)
{
pick = f;
break;
}
}
if (pick == null && files.Count > 0)
{
pick = files[0];
}
}
catch { }
if (pick != null)
{
name = pick.Name;
bytes = pick.Length;
}
}
static bool TestBackupFileOk(string dir, DateTime started)
{
if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir))
{
return false;
}
FileInfo gz = null;
try
{
foreach (FileInfo f in new DirectoryInfo(dir).GetFiles("mes_cloud_*.sql.gz"))
{
if (gz == null || f.LastWriteTime > gz.LastWriteTime)
{
gz = f;
}
}
}
catch { return false; }
if (gz == null || gz.Length <= 100)
{
return false;
}
if (gz.LastWriteTime < started.AddMinutes(-2))
{
return false;
}
return true;
}
static string GetSafePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new Exception("请先选择保存位置");
}
string p = path.Trim().Trim('"');
if (!Regex.IsMatch(p, @"^[A-Za-z]:\\"))
{
throw new Exception("只允许本机磁盘路径");
}
string full = Path.GetFullPath(p);
if (Regex.IsMatch(full, @"^[A-Za-z]:\\?$"))
{
return char.ToUpperInvariant(full[0]) + @":\";
}
return full.TrimEnd('\\');
}
static long GetFreeBytes(string path)
{
try
{
string full = GetSafePath(path);
string root = Path.GetPathRoot(full);
var di = new DriveInfo(root);
if (di.IsReady)
{
return di.AvailableFreeSpace;
}
}
catch { }
return 0;
}
static void KillProcessTree(int pid)
{
try
{
var psi = new ProcessStartInfo();
psi.FileName = "taskkill";
psi.Arguments = "/PID " + pid + " /T /F";
psi.CreateNoWindow = true;
psi.UseShellExecute = false;
using (Process k = Process.Start(psi))
{
if (k != null)
{
k.WaitForExit(5000);
}
}
}
catch { }
}
static void TryDelete(string path)
{
try { if (File.Exists(path)) { File.Delete(path); } } catch { }
}
static string Gb(long n)
{
return (n / 1073741824.0).ToString("0.00");
}
static string Mb(long n)
{
return (n / 1048576.0).ToString("0.0");
}
void SetCfg(string text, Color color)
{
_cfgStatus.Text = text ?? "";
_cfgStatus.ForeColor = color;
}
void SetRun(string text, Color color)
{
_runStatus.Text = text ?? "";
_runStatus.ForeColor = color;
}
void SetLog(string[] lines)
{
_log.Text = (lines != null && lines.Length > 0) ? string.Join(Environment.NewLine, lines) : "等待操作…";
_log.SelectionStart = _log.TextLength;
_log.ScrollToCaret();
}
void AppendLog(string line)
{
if (_log.Text == "等待操作…" || _log.Text == "等待操作...")
{
_log.Text = "";
}
if (_log.TextLength > 60000)
{
_log.Text = _log.Text.Substring(_log.TextLength - 40000);
}
_log.AppendText(line + Environment.NewLine);
}
}
}