In this thorough tutorial, we'll show you how to use MySqlBackup.NET to create stunning, real-time progress reports for database backup and restore processes. This lesson will assist you in creating polished progress indicators that tell your users at every stage of the development process, whether you're creating a web interface or a desktop program.
What is MySqlBackup.NET?
MySqlBackup.NET is a C# open source tool used for backup and restore of MySQL database. Find out more at the github repository.
Live Demo
Before we dive into the code, check out this visual demonstration of what we’ll be building:
This video showcases the real-time progress reporting system in action, complete with multiple beautiful CSS themes that you can customize for your application.
An overall of the Lifecycle of Export Progress
Everything is happening all at once:

Steps to Understanding the Foundation
In the export process, MySqlBackup.NET provides the progress status through the event mb.ExportProgressChanged:
void Backup()
{
string fileBackup = Server.MapPath("~/backup.sql");
using (var conn = new MySqlConnection(constr))
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
mb.ExportInfo.IntervalForProgressReport = 200; // 5 updates per second
mb.ExportProgressChanged += Mb_ExportProgressChanged;
mb.ExportToFile(fileBackup);
}
}
private void Mb_ExportProgressChanged(object sender, ExportProgressArgs e)
{
long totalRows = e.TotalRowsInAllTables;
long currentRow = e.CurrentRowIndexInAllTables;
int percent = CalculatePercent(taskInfo.TotalRows, taskInfo.CurrentRow);
}
Notice that the above code is running in a single thread (the main thread), but in order to have a progress report, we need to move the main process of MySqlBackup.NET to another secondary thread, so-called asynchronous in C#.
Dedicating the Export Process to another thread (asynchronous), by using System.Threading.Task
void Backup()
{
// Asynchronous
// Export process is splitted into another thread
// firing the task in another thread
_ = Task.Run(() => { BeginBackup(); });
}
void BeginBackup()
{
using (var conn = new MySqlConnection(constr))
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
mb.ExportInfo.IntervalForProgressReport = 200; // 5 updates per second
mb.ExportProgressChanged += Mb_ExportProgressChanged;
mb.ExportToFile(fileBackup);
}
}
C#
Next, creating the Intermediary Caching Data Centre.
Write a simple class to hold the values:
public class TaskInfo
{
public bool IsComplete { get; set;} = false;
public int Percent { get; set; } = 0;
public long TotalRows { get; set; } = 0L;
public long CurrentRow { get; set; } = 0L;
}
Declare a global static ConcurrentDictionary to hold the class. ConcurrentDictionary is a specialized thread safe variable. It allows multiple different threads to safely read and write the values.
We are marking it static so that the caching values can be accessed by all threads (all request) of the whole application.
public class api
{
// accesible by any thread concurrently
static ConcurrentDictionary<int, TaskInfo> dicTask = new ConcurrentDictionary<int, TaskInfo>();
void Backup()
{
_ = Task.Run(() => { BeginBackup(); });
}
void BeginBackup()
{
...
}
private void Mb_ExportProgressChanged(object sender, ExportProgressArgs e)
{
...
}
}
Next, we are going to receive the progress status values provided by MySqlBackup.NET:
public class api
{
static ConcurrentDictionary<int, TaskInfo> dicTask = new ConcurrentDictionary<int, TaskInfo>();
void Backup()
{
// get a new taskid, can be a random number
int newTaskId = GetNewTaskId();
_ = Task.Run(() => { BeginBackup(newTaskId); });
}
// caching the taskid in this thread
int taskid = 0;
void BeginBackup(int newTaskId)
{
// cache the task id
taskid = newTaskId;
string fileBackup = Server.MapPath("~/backup.sql");
// Create a new task info to collect data
TaskInfo taskInfo = new TaskInfo();
// Cache the task info into the global dictionary
dicTask[newTaskId] = taskInfo;
using (var conn = new MySqlConnection(constr))
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
mb.ExportInfo.IntervalForProgressReport = 200; // 5 updates per second
mb.ExportProgressChanged += Mb_ExportProgressChanged;
mb.ExportToFile(fileBackup);
}
// Passing this point, mark the completion of the process
// Informing the frontend about the completion
taskInfo.IsComplete = true;
}
private void Mb_ExportProgressChanged(object sender, ExportProgressArgs e)
{
// Retrieve the task info belongs to this task id
if (dicTask.TryGetValue(taskid, out var taskInfo))
{
// Collect the data provided by the export process
taskInfo.TotalRows = e.TotalRowsInAllTables;
taskInfo.CurrentRow = e.CurrentRowIndexInAllTables;
taskInfo.Percent = CalculatePercent(taskInfo.TotalRows, taskInfo.CurrentRow);
}
}
}
Now, let’s integrate this logic into the application.
In this article, we’ll be using ASP.NET Web Forms as our demonstration platform to show how progress reporting integrates seamlessly with web applications.
In this demo of ASP.NET Web Forms, we’ll be using:
- ZERO VIEWSTATE – The secret to modern Web Forms
- Zero server control
- No custom user control
- No postback
- No UpdatePanel
- Pure HTML, CSS, and JavaScript / FetchApi
Important Note: Once you understand the core working mechanism and patterns demonstrated in this walkthrough, you can easily replicate these same ideas to build your own UI progress reporting system in any application framework. Whether you’re developing with:
- Windows Forms (WinForms) applications
- .NET Core web applications
- ASP.NET MVC projects
- Blazor applications
- Mono cross-platform apps
- Console applications with custom UI
- WPF desktop applications
The fundamental principles remain the same: create an intermediary caching layer, implement the two-thread pattern, and provide real-time UI updates. The specific implementation details will vary by framework, but the core architecture and MySqlBackup.NET integration patterns are universally applicable.
Let’s begin by building our backend API to handle the backup and restore logic. We’ll create a new blank ASP.NET Web Forms page. A typical new blank web form page will look something like this:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="apiBackup.aspx.cs" Inherits="myweb.apiBackup" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
</div>
</form>
</body>
</html>
Delete everything from the frontend markup, leaving only the page directive declaration at the top:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="api.aspx.cs" Inherits="myweb.api" %>
Now, go to the code behind, which typically look like this:
namespace myweb
{
public partial class apiBackup : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
}
}
Patch the code that we’ve written previously to the backend of this web form page:
// backend C#
public partial class apiBackup : System.Web.UI.Page
{
string constr = "server=localhost;user=root;pwd=1234;database=mydatabase;";
static ConcurrentDictionary<int, TaskInfo> dicTask = new ConcurrentDictionary<int, TaskInfo>();
protected void Page_Load(object sender, EventArgs e)
{
// get the action for the request from the frontend
string action = Request["action"] + "";
switch (action)
{
case "backup":
Backup();
break;
}
}
int taskid = 0;
void Backup()
{
int newTaskId = GetNewTaskId();
// run the task in the background
// no waiting
_ = Task.Run(() => { BeginBackup(newTaskId); });
// immediately tell the task id to the frontend
Response.Write(newTaskId.ToString());
}
void BeginBackup(int newTaskId)
{
taskid = newTaskId;
string fileBackup = Server.MapPath("~/backup.sql");
TaskInfo taskInfo = new TaskInfo();
dicTask[newTaskId] = taskInfo;
using (var conn = new MySqlConnection(constr))
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
mb.ExportInfo.IntervalForProgressReport = 200; // 5 updates per second
mb.ExportProgressChanged += Mb_ExportProgressChanged;
mb.ExportToFile(fileBackup);
}
taskInfo.IsComplete = true;
}
private void Mb_ExportProgressChanged(object sender, ExportProgressArgs e)
{
if (dicTask.TryGetValue(taskid, out var taskInfo))
{
taskInfo.TotalRows = e.TotalRowsInAllTables;
taskInfo.CurrentRow = e.CurrentRowIndexInAllTables;
taskInfo.Percent = CalculatePercent(taskInfo.TotalRows, taskInfo.CurrentRow);
}
}
Now, we have built an api endpoint that allows the frontend to tell the server to start a backup task.
There are two main ways that the frontend can do this with:
- Get Request or
- Post Request
Using Get Request:
// frontend javascript
// cache the task id globally at the frontend
let taskid = 0;
async function backup() {
const response = await fetch('/api?action=backup', {
method: 'GET',
credentials: 'include'
});
// obtain the task id returned from the backend
// convert the return response into text
const responseText = await response.text();
// convert the text into number (task id)
taskid = parseInt(responseText);
}
Using Post Request:
// frontend javascript
async function backup() {
const formData = new FormData();
formData.append('action', 'backup');
const response = await fetch('/api', {
method: 'POST',
body: formData,
credentials: 'include'
});
// obtain the task id returned from the backend
// convert the return response into text
const responseText = await response.text();
// convert the text into number (task id)
taskid = parseInt(responseText);
}
In this context, it is recommended to use a POST request, as the query string will not be exposed to the server log.
Next, at the frontend, write a new function getStatus() to get the progress status from the backend:
// frontend javascript
async function getStatus() {
const formData = new FormData();
formData.append('action', 'getstatus');
formData.append('taskid', taskid);
const response = await fetch('/api', {
method: 'POST',
body: formData,
credentials: 'include'
});
// response = progress status
}
Now, back to the backend api, handle a new action for “getstatus”:
protected void Page_Load(object sender, EventArgs e)
{
string action = Request["action"] + "";
switch (action)
{
case "backup":
Backup();
break;
case "getstatus":
GetStatus();
break;
}
}
void GetStatus()
{
string requestTaskId = Request["taskid"] + "";
int _taskid = Convert.ToInt32(requestTaskId);
// get the task info from the global dictionary
if (dicTask.TryGetValue(_taskid, out var taskInfo))
{
// convert the class object into json string
// Install the Nuget Package of "System.Text.JSON" to enable this function
// You also need to add an 'using' line at the top of your code to use this:
// using System.Text.JSON;
string json = JsonSerializer.Serialize(taskInfo);
Response.ContentType = "application/json";
// send the progress status back to the frontend
Response.Write(json);
}
}
Example of responding text (JSON):
// json
{
"IsComplete": false,
"Percent": 50,
"TotalRows": 10000,
"CurrentRow": 5000
}
Go back to the frontend, continue to handle the request at the frontend:
// frontend javascript
async function getStatus() {
const formData = new FormData();
formData.append('action', 'getstatus');
formData.append('taskid', taskid);
const response = await fetch('/api', {
method: 'POST',
body: formData,
credentials: 'include'
});
// Convert the response to text
let result = await response.text();
// Convert the result to JSON object
let jsonObject = JSON.parse(result);
// You can now work with progress value:
document.getElementById("spanIsComplete").textContent = jsonObject.IsComplete;
document.getElementById("spanPercent").textContent = jsonObject.Percent + "%";
document.getElementById("spanTotalRows").textContent = jsonObject.TotalRows;
document.getElementById("spanCurrentRow").textContent = jsonObject.CurrentRow;
}
Which at the frontpage you will have the following html span containers for the above JavaScript to fill in with:
// frontend html
<p>Is Complete: <span id="spanIsComplete"></span></p>
<p>Percent: <span id="spanPercent"></span></p>
<p>Total Rows: <span id="spanTotalRows"></span></p>
<p>Current Row: <span id="spanCurrentRow"></span></p>
Now, we’ll be using an interval timer to repeatedly call the function getStatus() Every 200ms to continuously update the frontend UI.
// frontend javascript
let intervalTimer = null;
// Start monitoring
startMonitoring() {
stopMonitoring();
intervalTimer = setInterval(
() => getStatus(),
200);
},
// Stop monitoring
stopMonitoring() {
if (intervalTimer) {
// stop the timer
clearInterval(intervalTimer);
// remove the timer
intervalTimer = null;
}
},
Now, after the backup is successfully called to work, we’ll begin monitoring the progress by calling the interval timer:
// frontend javascript
async function backup() {
const formData = new FormData();
formData.append('action', 'backup');
const response = await fetch('/api', {
method: 'POST',
body: formData,
credentials: 'include'
});
const responseText = await response.text();
taskid = parseInt(responseText);
// by this point the backup task has already begun
// start monitoring the progress status
startMonitoring();
}
Now, the interval is set to call 5 times per second… until the task is completed.
How to call it to stop when the task is completed? by calling the function:
stopMonitoring();
Where is a good location to call the timer to stop?
You want to pause for a moment to think?
Yes, right after the value returned from the backend, where we can check the completion status. If it is completed, simply call the timer to stop:
// frontend javascript
async function getStatus() {
const formData = new FormData();
formData.append('action', 'getstatus');
formData.append('taskid', taskid);
const response = await fetch('/api', {
method: 'POST',
body: formData,
credentials: 'include'
});
let result = await response.text();
let jsonObject = JSON.parse(result);
document.getElementById("spanIsComplete").textContent = jsonObject.IsComplete;
document.getElementById("spanPercent").textContent = jsonObject.Percent + "%";
document.getElementById("spanTotalRows").textContent = jsonObject.TotalRows;
document.getElementById("spanCurrentRow").textContent = jsonObject.CurrentRow;
// now check for the completion status here
if (jsonObject.IsComplete) {
// yes, it's completed, call the timer stop now
stopMonitoring();
}
}
Now, before we proceed to the full walkthrough section. Let’s do one more function.
What if we want to stop the export before it gets finished?
Let’s create another javascript function to call the backend to stop the process:
// frontend javascript
async function stopTask() {
const formData = new FormData();
formData.append("action", "stoptask");
formData.append("taskid", taskid);
// sending stop request to backend
const response = await fetch('/api', {
method: 'POST',
body: formData,
credentials: 'include'
});
}
Back to C# backend api endpoint.
First edit the task info object to handle the stop request:
// backend C#
public class TaskInfo
{
public bool IsComplete { get; set;} = false;
public int Percent { get; set; } = 0;
public long TotalRows { get; set; } = 0L;
public long CurrentRow { get; set; } = 0L;
// add a boolean flag for marking the task to be cancelled
public bool RequestCancel { get; set; } = false;
}
Let's handle the new stop request:
```csharp
// backend C#
protected void Page_Load(object sender, EventArgs e)
{
string action = Request["action"] + "";
switch (action)
{
case "backup":
Backup();
break;
case "getstatus":
GetStatus();
break;
// handle stop request
case "stoptask":
Stop();
break;
}
}
void Stop()
{
string requestTaskId = Request["taskid"] + "";
int _taskid = Convert.ToInt32(requestTaskId);
// get the task info
if (dicTask.TryGetValue(_taskid, out var taskInfo))
{
// set the boolean flag to signal the request to cancel task
taskInfo.RequestCancel = true;
}
}
As you can see, the additional handling of stop request does not immediately stop the process. It only set a boolean flag. We cannot directly call the MySqlBackup to stop, but instead we set the boolean to notify MySqlBackup.NET to stop.
According to the previous interval time setup, MySqlBackup.NET will interact with the backend every 200ms, that is the moment, we have the opportunity to communicate with the export process.
Here is the code that we have already written in the previous section, at the very end of this code, do the real stopping action:
// backend C#
void BeginBackup(int newTaskId)
{
taskid = newTaskId;
string fileBackup = Server.MapPath("~/backup.sql");
TaskInfo taskInfo = new TaskInfo();
dicTask[newTaskId] = taskInfo;
using (var conn = new MySqlConnection(constr))
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
mb.ExportInfo.IntervalForProgressReport = 200; // 5 updates per second
mb.ExportProgressChanged += Mb_ExportProgressChanged;
mb.ExportToFile(fileBackup);
}
taskInfo.IsComplete = true;
}
private void Mb_ExportProgressChanged(object sender, ExportProgressArgs e)
{
if (dicTask.TryGetValue(taskid, out var taskInfo))
{
taskInfo.TotalRows = e.TotalRowsInAllTables;
taskInfo.CurrentRow = e.CurrentRowIndexInAllTables;
taskInfo.Percent = CalculatePercent(taskInfo.TotalRows, taskInfo.CurrentRow);
// MySqlBackup.NET will contact the backend every 200ms
// Here is the oppurtunity that we can talk to MySqlBackup
// Check the cancellation boolean flag
if (taskInfo.RequestCancel)
{
// Now, here we directly control MySqlBackup to stop
// all it's inner processes.
// Do the real "stop" here
((MySqlBackup)sender).StopAllProcess();
// Note: When a task is cancelled, you may want to clean up the partially created file
// This will be handled in the full implementation section
}
}
}
Above is the basic walkthrough.
If you’ve followed along this far, congratulations! You now understand the core concepts. What follows is the production-ready implementation with all the bells and whistles – error handling, cancellation, file management, and more. Don’t worry if it looks complex – it’s built on the same foundation you just learned.
Full Walkthrough
MySqlBackup.NET provides two essential classes that make progress reporting possible:
1. ExportProgressArgs.cs
This class provides detailed information during backup operations, including:
- Current table being processed
- Total number of tables
- Row counts (current and total)
- Progress percentages
2. ImportProgressArgs.cs
This class tracks restore operations with:
- Bytes processed vs. total bytes
- Import completion percentage
- Real-time progress updates
Getting Started: Basic Progress Events
The magic happens when you subscribe to progress change events. Here’s how to set up the fundamental progress reporting:
Backup Progress Monitoring
void Backup()
{
using (var conn = new MySqlConnection(connectionString))
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
// Set update frequency (100ms = 10 updates per second)
mb.ExportInfo.IntervalForProgressReport = 100;
// Subscribe to progress events
mb.ExportProgressChanged += Mb_ExportProgressChanged;
mb.ExportToFile(filePathSql);
}
}
private void Mb_ExportProgressChanged(object sender, ExportProgressArgs e)
{
// Rich progress information available
string currentTable = e.CurrentTableName;
int totalTables = e.TotalTables;
int currentTableIndex = e.CurrentTableIndex;
long totalRows = e.TotalRowsInAllTables;
long currentRows = e.CurrentRowIndexInAllTables;
long currentTableRows = e.TotalRowsInCurrentTable;
long currentTableProgress = e.CurrentRowIndexInCurrentTable;
// Calculate completion percentage
int percentCompleted = 0;
if (e.CurrentRowIndexInAllTables > 0L && e.TotalRowsInAllTables > 0L)
{
if (e.CurrentRowIndexInAllTables >= e.TotalRowsInAllTables)
{
percentCompleted = 100;
}
else
{
percentCompleted = (int)(e.CurrentRowIndexInAllTables * 100L / e.TotalRowsInAllTables);
}
}
}
Restore Progress Monitoring
void Restore()
{
using (var conn = config.GetNewConnection())
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
// Set update frequency
mb.ImportInfo.IntervalForProgressReport = 100;
// Subscribe to progress events
mb.ImportProgressChanged += Mb_ImportProgressChanged;
mb.ImportFromFile(filePathSql);
}
}
private void Mb_ImportProgressChanged(object sender, ImportProgressArgs e)
{
// Byte-based progress tracking
long totalBytes = e.TotalBytes;
long currentBytes = e.CurrentBytes;
// Calculate completion percentage
int percentCompleted = 0;
if (e.CurrentBytes > 0L && e.TotalBytes > 0L)
{
if (e.CurrentBytes >= e.TotalBytes)
{
percentCompleted = 100;
}
else
{
percentCompleted = (int)(e.CurrentBytes * 100L / e.TotalBytes);
}
}
}
Choosing the Right Update Frequency
The update interval significantly impacts your application’s responsiveness:
// Configure update frequency based on your needs:
// Desktop/Local Applications (low latency)
mb.ExportInfo.IntervalForProgressReport = 100; // 10 updates/second
mb.ImportInfo.IntervalForProgressReport = 250; // 4 updates/second
// Web Applications (higher latency)
mb.ExportInfo.IntervalForProgressReport = 500; // 2 updates/second
mb.ImportInfo.IntervalForProgressReport = 1000; // 1 update/second
Performance Guidelines:
- 100ms: Perfect for desktop applications – very responsive
- 250ms: Great balance for most applications
- 500ms: Ideal for web applications with moderate traffic
- 1000ms: Best for high-traffic web applications
Architecture: The Two-Thread Pattern
Understanding the architecture is crucial for building robust progress reporting:
MySqlBackup.NET Internal Threads
- Main Process Thread: Executes actual backup/restore operations
- Progress Reporting Thread: Periodically reports progress via timer events
Your Application Threads
- Backend Thread: Handles MySqlBackup.NET operations and caches progress data
- UI Thread: Retrieves cached data and updates the user interface
Creating the Progress Data Model
In your application, you need to write your own class to hold the progress values at your side. Here’s a comprehensive class to cache all progress information
class ProgressReportTask
{
public int ApiCallIndex { get; set; }
public int TaskId { get; set; }
public int TaskType { get; set; } // 1 = backup, 2 = restore
public string FileName { get; set; }
public string SHA256 { get; set; }
// Task status tracking
public bool IsStarted { get; set; }
public bool IsCompleted { get; set; } = false;
public bool IsCancelled { get; set; } = false;
public bool RequestCancel { get; set; } = false;
public bool HasError { get; set; } = false;
public string ErrorMsg { get; set; } = "";
// Time tracking
public DateTime TimeStart { get; set; } = DateTime.MinValue;
public DateTime TimeEnd { get; set; } = DateTime.MinValue;
public TimeSpan TimeUsed { get; set; } = TimeSpan.Zero;
// Backup-specific progress data
public int TotalTables { get; set; } = 0;
public int CurrentTableIndex { get; set; } = 0;
public string CurrentTableName { get; set; } = "";
public long TotalRowsCurrentTable { get; set; } = 0;
public long CurrentRowCurrentTable { get; set; } = 0;
public long TotalRows { get; set; } = 0;
public long CurrentRowIndex { get; set; } = 0;
// Restore-specific progress data
public long TotalBytes { get; set; } = 0;
public long CurrentBytes { get; set; } = 0;
public int PercentCompleted { get; set; } = 0;
// UI-friendly properties
[JsonPropertyName("TaskTypeName")]
public string TaskTypeName
{
get
{
return TaskType switch
{
1 => "Backup",
2 => "Restore",
_ => "Unknown"
};
}
}
[JsonPropertyName("TimeStartDisplay")]
public string TimeStartDisplay => TimeStart.ToString("yyyy-MM-dd HH:mm:ss");
[JsonPropertyName("TimeEndDisplay")]
public string TimeEndDisplay => TimeEnd.ToString("yyyy-MM-dd HH:mm:ss");
[JsonPropertyName("TimeUsedDisplay")]
public string TimeUsedDisplay => TimeDisplayHelper.TimeSpanToString(TimeUsed);
[JsonPropertyName("FileDownloadUrl")]
public string FileDownloadUrl
{
get
{
if (!string.IsNullOrEmpty(FileName))
{
return $"/apiFiles?folder=backup&filename={FileName}";
}
return "";
}
}
}
Building the User Interface – Framework Agnostic Approach
Create a new blank ASP.NET Web Forms page and delete everything from the frontend markup, leaving only the page directive declaration at the top:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="apiProgressReport.aspx.cs" Inherits="System.pages.apiProgressReport" %>
Route this page to /apiProgressReport using your preferred routing method.
Main API Controller
// Thread-safe progress cache
static ConcurrentDictionary<int, ProgressReportTask> dicTask = new ConcurrentDictionary<int, ProgressReportTask>();
protected void Page_Load(object sender, EventArgs e)
{
if (!IsUserAuthorized())
{
Response.StatusCode = 401;
Response.Write("0|Unauthorized access");
Response.End();
return;
}
string action = (Request["action"] + "").ToLower();
switch (action)
{
case "backup":
Backup();
break;
case "restore":
Restore();
break;
case "stoptask":
StopTask();
break;
case "gettaskstatus":
GetTaskStatus();
break;
default:
Response.StatusCode = 400;
Response.Write("0|Invalid action");
break;
}
}
bool IsUserAuthorized()
{
// Implement your authentication logic here
// Check user login and backup permissions
return true; // Simplified for demo
}
With this setup, the front end can access the api in 2 main ways
Example 1: by using query string (the get request), simple direct:
/apiProgressReport?action=backup
/apiProgressReport?action=restore
/apiProgressReport?action=stoptask
/apiProgressReport?action=gettaskstatus
Example 2: by using post request through fetchapi or xmlhttprequest in a nutshell, the javascript fetchapi will look something like this:
// Javascript
// backup
const formData = new FormData();
formData.append('action', 'backup');
const response = await fetch('/apiProgressReport', {
method: 'POST',
body: formData,
credentials: 'include'
});
// restore
const formData = new FormData();
formData.append('action', 'restore');
// attach and send the file to server
formData.append('fileRestore', fileRestore.files[0]);
const response = await fetch('/apiProgressReport', {
method: 'POST',
body: formData,
credentials: 'include'
});
Backup Implementation
void Backup()
{
var taskId = GetNewTaskId();
// Start task asynchronously (fire and forget pattern)
_ = Task.Run(() => { BeginExport(taskId); });
// Immediately return task ID to frontend
Response.Write(taskId.ToString());
}
int thisTaskId = 0;
void BeginExport(int newTaskId)
{
thisTaskId = newTaskId;
ProgressReportTask task = new ProgressReportTask()
{
TaskId = newTaskId,
TaskType = 1, // backup
TimeStart = DateTime.Now,
IsStarted = true
};
dicTask[newTaskId] = task;
try
{
string folder = Server.MapPath("~/App_Data/backup");
Directory.CreateDirectory(folder);
string fileName = $"backup-{DateTime.Now:yyyy-MM-dd_HHmmss}.sql";
string filePath = Path.Combine(folder, fileName);
using (var conn = config.GetNewConnection())
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
mb.ExportInfo.IntervalForProgressReport = 100;
mb.ExportProgressChanged += Mb_ExportProgressChanged;
mb.ExportToFile(filePath);
}
// Handle cancellation or completion
if (task.RequestCancel)
{
task.IsCancelled = true;
try
{
if (File.Exists(filePath))
File.Delete(filePath);
}
catch { }
}
else
{
// Successfully completed
task.FileName = fileName;
task.SHA256 = Sha256.Compute(filePath);
}
task.TimeEnd = DateTime.Now;
task.TimeUsed = DateTime.Now - task.TimeStart;
task.IsCompleted = true;
}
catch (Exception ex)
{
task.HasError = true;
task.ErrorMsg = ex.Message;
task.TimeEnd = DateTime.Now;
task.IsCompleted = true;
}
}
private void Mb_ExportProgressChanged(object sender, ExportProgressArgs e)
{
if (dicTask.TryGetValue(thisTaskId, out var task))
{
// Update all progress information
task.CurrentTableName = e.CurrentTableName;
task.TotalTables = e.TotalTables;
task.CurrentTableIndex = e.CurrentTableIndex;
task.TotalRows = e.TotalRowsInAllTables;
task.CurrentRowIndex = e.CurrentRowIndexInAllTables;
task.TotalRowsCurrentTable = e.TotalRowsInCurrentTable;
task.CurrentRowCurrentTable = e.CurrentRowIndexInCurrentTable;
// Calculate percentage
if (e.CurrentRowIndexInAllTables > 0L && e.TotalRowsInAllTables > 0L)
{
if (e.CurrentRowIndexInAllTables >= e.TotalRowsInAllTables)
{
task.PercentCompleted = 100;
}
else
{
task.PercentCompleted = (int)(e.CurrentRowIndexInAllTables * 100L / e.TotalRowsInAllTables);
}
}
else
{
task.PercentCompleted = 0;
}
// Handle cancellation requests
if (task.RequestCancel)
{
((MySqlBackup)sender).StopAllProcess();
}
}
}
Restore Implementation
void Restore()
{
var taskId = GetNewTaskId();
ProgressReportTask task = new ProgressReportTask()
{
TaskId = taskId,
TaskType = 2, // restore
TimeStart = DateTime.Now,
IsStarted = true
};
dicTask[taskId] = task;
// Validate uploaded file
if (Request.Files.Count == 0)
{
task.IsCompleted = true;
task.HasError = true;
task.ErrorMsg = "No file uploaded";
task.TimeEnd = DateTime.Now;
return;
}
string fileExtension = Request.Files[0].FileName.ToLower().Trim();
if (!fileExtension.EndsWith(".zip") && !fileExtension.EndsWith(".sql"))
{
task.IsCompleted = true;
task.HasError = true;
task.ErrorMsg = "Invalid file type. Only .zip or .sql files are supported.";
task.TimeEnd = DateTime.Now;
return;
}
// Save uploaded file
string folder = Server.MapPath("~/App_Data/backup");
Directory.CreateDirectory(folder);
string fileName = $"restore-{DateTime.Now:yyyy-MM-dd_HHmmss}";
string filePath = Path.Combine(folder, fileName + ".sql");
if (fileExtension.EndsWith(".zip"))
{
string zipPath = Path.Combine(folder, fileName + ".zip");
Request.Files[0].SaveAs(zipPath);
ZipHelper.ExtractFile(zipPath, filePath);
task.FileName = fileName + ".zip";
}
else
{
Request.Files[0].SaveAs(filePath);
task.FileName = fileName + ".sql";
}
// Start restore process asynchronously
_ = Task.Run(() => { BeginRestore(taskId, filePath); });
Response.Write(taskId.ToString());
}
void BeginRestore(int newTaskId, string filePath)
{
thisTaskId = newTaskId;
if (dicTask.TryGetValue(thisTaskId, out ProgressReportTask task))
{
try
{
task.FileName = Path.GetFileName(filePath);
task.SHA256 = Sha256.Compute(filePath);
using (var conn = config.GetNewConnection())
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
mb.ImportInfo.IntervalForProgressReport = 100;
mb.ImportProgressChanged += Mb_ImportProgressChanged;
mb.ImportFromFile(filePath);
}
if (task.RequestCancel)
{
task.IsCancelled = true;
}
task.TimeEnd = DateTime.Now;
task.TimeUsed = DateTime.Now - task.TimeStart;
task.IsCompleted = true;
}
catch (Exception ex)
{
task.HasError = true;
task.ErrorMsg = ex.Message;
task.TimeEnd = DateTime.Now;
task.TimeUsed = DateTime.Now - task.TimeStart;
task.IsCompleted = true;
}
}
}
private void Mb_ImportProgressChanged(object sender, ImportProgressArgs e)
{
if (dicTask.TryGetValue(thisTaskId, out var task))
{
task.TotalBytes = e.TotalBytes;
task.CurrentBytes = e.CurrentBytes;
// Calculate percentage
if (e.CurrentBytes > 0L && e.TotalBytes > 0L)
{
if (e.CurrentBytes >= e.TotalBytes)
{
task.PercentCompleted = 100;
}
else
{
task.PercentCompleted = (int)(e.CurrentBytes * 100L / e.TotalBytes);
}
}
else
{
task.PercentCompleted = 0;
}
// Handle cancellation requests
if (task.RequestCancel)
{
((MySqlBackup)sender).StopAllProcess();
}
}
}
Task Control Methods
void StopTask()
{
if (int.TryParse(Request["taskid"] + "", out int taskId))
{
if (dicTask.TryGetValue(taskId, out ProgressReportTask task))
{
task.RequestCancel = true;
Response.Write("1");
}
else
{
Response.Write("0|Task not found");
}
}
else
{
Response.Write("0|Invalid task ID");
}
}
void GetTaskStatus()
{
if (int.TryParse(Request["apicallid"] + "", out int apiCallId))
{
if (int.TryParse(Request["taskid"] + "", out int taskId))
{
if (dicTask.TryGetValue(taskId, out ProgressReportTask task))
{
task.ApiCallIndex = apiCallId;
string json = JsonSerializer.Serialize(task);
Response.Clear();
Response.ContentType = "application/json";
Response.Write(json);
}
}
}
}
int GetNewTaskId()
{
// Use Guid to prevent collisions
return Math.Abs(Guid.NewGuid().GetHashCode());
}
Handling Late Echo Responses
The apiCallId system prevents UI corruption from network latency issues:
Normal scenario:
Call 1 → Response 1: 5%
Call 2 → Response 2: 10%
Call 3 → Response 3: 15%
Latency scenario:
Call 1 → (delayed)
Call 2 → Response 2: 10%
Call 3 → Response 3: 15%
↓
Response 1: 5% (ignored due to old apiCallId)
Building the Frontend
Basic Value Container
You can use <span> as the container for the values:
// data that is manually handled by user
<span id="labelTaskId"></span>
<span id="labelPercent">0</span>
<span id="lableTimeStart"></span>
<span id="lableTimeEnd"></span>
<span id="lableTimeElapse"></span>
<span id="labelSqlFilename"></span> // the download webpath for the generated sql dump file
<span id="labelSha256"></span> // the SHA 256 checksum for the generate file
// typical status: running, completed, error, cancelled
<span id="labelTaskStatus"></span>
// the following data fields provided by mysqlbackup.net during the progress change events:
// fields for backup used
<span id="labelCurTableName"></span>
<span id="labelCurTableIndex"></span>
<span id="labelTotalTables"></span>
<span id="labelCurrentRowsAllTables"></span>
<span id="labelTotalRowsAllTable">0</span>
<span id="labelCurrentRowsCurrentTables"></span>
<span id="labelTotalRowsCurrentTable">0</span>
// fields for restore used
<span id="labelCurrentBytes"></span>
<span id="lableTotalBytes"></span>
HTML Structure
Example of a comprehensive progress display:
<!-- Progress Bar -->
<div id="progress_bar_container">
<div id="progress_bar_indicator">
<span id="labelPercent">0</span> %
</div>
</div>
<!-- Control Buttons -->
<div class="controls">
<button type="button" onclick="backup();">Backup Database</button>
<button type="button" onclick="restore();">Restore Database</button>
<button type="button" onclick="stopTask();">Stop Current Task</button>
<label for="fileRestore">Select Restore File:</label>
<input type="file" id="fileRestore" accept=".sql,.zip" />
</div>
<!-- Detailed Status Display -->
<div class="task-status">
<table>
<tr>
<td>Task ID</td>
<td><span id="labelTaskId">--</span></td>
</tr>
<tr>
<td>Status</td>
<td>
<span id="labelTaskStatus">Ready</span>
<span id="labelTaskMessage"></span>
</td>
</tr>
<tr>
<td>Time</td>
<td>
Start: <span id="labelTimeStart">--</span> |
End: <span id="labelTimeEnd">--</span> |
Duration: <span id="labelTimeElapsed">--</span>
</td>
</tr>
<tr>
<td>File</td>
<td>
<span id="labelSqlFilename">--</span><br>
SHA256: <span id="labelSha256">--</span>
</td>
</tr>
<!-- Backup-specific fields -->
<tr class="backup-only">
<td>Current Table</td>
<td>
<span id="labelCurrentTableName">--</span>
(<span id="labelCurrentTableIndex">--</span> / <span id="labelTotalTables">--</span>)
</td>
</tr>
<tr class="backup-only">
<td>All Tables Progress</td>
<td>
<span id="labelCurrentRowsAllTables">--</span> / <span id="labelTotalRowsAllTables">--</span>
</td>
</tr>
<tr class="backup-only">
<td>Current Table Progress</td>
<td>
<span id="labelCurrentRowsCurrentTable">--</span> / <span id="labelTotalRowsCurrentTable">--</span>
</td>
</tr>
<!-- Restore-specific fields -->
<tr class="restore-only">
<td>Progress</td>
<td>
<span id="labelCurrentBytes">--</span> / <span id="labelTotalBytes">--</span> bytes
</td>
</tr>
</table>
</div>
JavaScript Implementation
Initialization and Variables
// Global variables
let taskId = 0;
let apiCallId = 0;
let intervalTimer = null;
let intervalMs = 1000;
// Cache DOM elements for better performance
const elements = {
fileRestore: document.querySelector("#fileRestore"),
progressBar: document.querySelector("#progress_bar_indicator"),
labelPercent: document.querySelector("#labelPercent"),
labelTaskId: document.querySelector("#labelTaskId"),
labelTimeStart: document.querySelector("#labelTimeStart"),
labelTimeEnd: document.querySelector("#labelTimeEnd"),
labelTimeElapsed: document.querySelector("#labelTimeElapsed"),
labelTaskStatus: document.querySelector("#labelTaskStatus"),
labelTaskMessage: document.querySelector("#labelTaskMessage"),
labelSqlFilename: document.querySelector("#labelSqlFilename"),
labelSha256: document.querySelector("#labelSha256"),
// Backup-specific elements
labelCurrentTableName: document.querySelector("#labelCurrentTableName"),
labelCurrentTableIndex: document.querySelector("#labelCurrentTableIndex"),
labelTotalTables: document.querySelector("#labelTotalTables"),
labelCurrentRowsAllTables: document.querySelector("#labelCurrentRowsAllTables"),
labelTotalRowsAllTables: document.querySelector("#labelTotalRowsAllTables"),
labelCurrentRowsCurrentTable: document.querySelector("#labelCurrentRowsCurrentTable"),
labelTotalRowsCurrentTable: document.querySelector("#labelTotalRowsCurrentTable"),
// Restore-specific elements
labelCurrentBytes: document.querySelector("#labelCurrentBytes"),
labelTotalBytes: document.querySelector("#labelTotalBytes")
};
Core Functions
Backup, Restore, Stop
async function backup() {
resetUIValues();
const formData = new FormData();
formData.append('action', 'backup');
const result = await fetchData(formData);
if (result.ok) {
taskId = result.thisTaskid;
intervalMs = 1000;
startIntervalTimer();
showSuccessMessage("Backup Started", "Database backup has begun successfully");
} else {
showErrorMessage("Backup Failed", result.errMsg);
stopIntervalTimer();
}
}
async function restore() {
resetUIValues();
if (!elements.fileRestore.files || elements.fileRestore.files.length === 0) {
showErrorMessage("No File Selected", "Please select a backup file to restore");
return;
}
const formData = new FormData();
formData.append('action', 'restore');
formData.append('fileRestore', elements.fileRestore.files[0]);
const result = await fetchData(formData);
if (result.ok) {
taskId = result.thisTaskid;
intervalMs = 1000;
startIntervalTimer();
showSuccessMessage("Restore Started", "Database restore has begun successfully");
} else {
showErrorMessage("Restore Failed", result.errMsg);
stopIntervalTimer();
}
}
async function stopTask() {
if (!taskId || taskId === 0) {
showErrorMessage("No Active Task", "There is no running task to stop");
return;
}
const formData = new FormData();
formData.append("action", "stoptask");
formData.append("taskid", taskId);
const result = await fetchData(formData);
if (result.ok) {
showSuccessMessage("Stop Requested", "The task is being cancelled...");
} else {
showErrorMessage("Stop Failed", result.errMsg);
stopIntervalTimer();
}
}
Progress Monitoring
async function fetchTaskStatus() {
apiCallId++;
const formData = new FormData();
formData.append('action', 'gettaskstatus');
formData.append('taskid', taskId);
formData.append('apicallid', apiCallId);
const result = await fetchData(formData);
if (result.ok) {
// Ignore late responses
if (result.jsonObject.ApiCallIndex !== apiCallId) {
return;
}
updateUIValues(result.jsonObject);
} else {
showErrorMessage("Status Fetch Failed", result.errMsg);
stopIntervalTimer();
}
}
function updateUIValues(data) {
// Optimize update frequency when task starts
if (data.PercentCompleted > 0 && intervalMs === 1000) {
// change the timer interval time
intervalMs = 100;
stopIntervalTimer();
setTimeout(startIntervalTimer, 500);
}
// Stop monitoring when task completes
if (data.IsCompleted || data.HasError || data.IsCancelled) {
stopIntervalTimer();
}
// Update basic information
elements.labelTaskId.textContent = data.TaskId || "--";
elements.labelTimeStart.textContent = data.TimeStartDisplay || "--";
elements.labelTimeEnd.textContent = data.TimeEndDisplay || "--";
elements.labelTimeElapsed.textContent = data.TimeUsedDisplay || "--";
// Update progress bar
const percent = data.PercentCompleted || 0;
elements.labelPercent.style.display = "block";
elements.labelPercent.textContent = percent;
elements.progressBar.style.width = percent + '%';
// Update status with visual indicators
const statusContainer = elements.labelTaskStatus.closest('td');
if (data.HasError) {
elements.labelTaskStatus.textContent = "Error";
elements.labelTaskMessage.textContent = data.ErrorMsg || "";
statusContainer.className = "status-error";
showErrorMessage("Task Failed", data.ErrorMsg || "Unknown error occurred");
} else if (data.IsCancelled) {
elements.labelTaskStatus.textContent = "Cancelled";
elements.labelTaskMessage.textContent = "";
statusContainer.className = "status-cancelled";
showWarningMessage("Task Cancelled", "The operation was cancelled by user request");
} else if (data.IsCompleted) {
elements.labelTaskStatus.textContent = "Completed";
elements.labelTaskMessage.textContent = "";
statusContainer.className = "status-complete";
showSuccessMessage("Task Completed", "Operation finished successfully!");
} else {
elements.labelTaskStatus.textContent = "Running";
elements.labelTaskMessage.textContent = "";
statusContainer.className = "status-running";
}
// Update file information
if (data.FileName && data.FileName.length > 0) {
elements.labelSqlFilename.innerHTML =
`File: <a href='${data.FileDownloadUrl}' class='download-link' target='_blank'>Download ${data.FileName}</a>`;
} else {
elements.labelSqlFilename.textContent = data.FileName || "--";
}
elements.labelSha256.textContent = data.SHA256 || "--";
// Update backup-specific information
if (data.TaskType === 1) {
elements.labelCurrentTableName.textContent = data.CurrentTableName || "--";
elements.labelCurrentTableIndex.textContent = data.CurrentTableIndex || "--";
elements.labelTotalTables.textContent = data.TotalTables || "--";
elements.labelCurrentRowsAllTables.textContent = data.CurrentRowIndex || "--";
elements.labelTotalRowsAllTables.textContent = data.TotalRows || "--";
elements.labelCurrentRowsCurrentTable.textContent = data.CurrentRowCurrentTable || "--";
elements.labelTotalRowsCurrentTable.textContent = data.TotalRowsCurrentTable || "--";
// Hide restore-specific fields
elements.labelCurrentBytes.textContent = "--";
elements.labelTotalBytes.textContent = "--";
}
// Update restore-specific information
if (data.TaskType === 2) {
elements.labelCurrentBytes.textContent = formatBytes(data.CurrentBytes) || "--";
elements.labelTotalBytes.textContent = formatBytes(data.TotalBytes) || "--";
// Hide backup-specific fields
elements.labelCurrentTableName.textContent = "--";
elements.labelCurrentTableIndex.textContent = "--";
elements.labelTotalTables.textContent = "--";
elements.labelCurrentRowsAllTables.textContent = "--";
elements.labelTotalRowsAllTables.textContent = "--";
elements.labelCurrentRowsCurrentTable.textContent = "--";
elements.labelTotalRowsCurrentTable.textContent = "--";
}
}
Utility Functions
// Centralized API handler
async function fetchData(formData) {
try {
let action = formData.get("action") || "";
const response = await fetch('/apiProgressReport', {
method: 'POST',
body: formData,
credentials: 'include'
});
if (response.ok) {
const responseText = await response.text();
if (responseText.startsWith("0|")) {
let err = responseText.substring(2);
return { ok: false, errMsg: err };
}
else {
if (!responseText || responseText.trim() === '') {
let err = "Empty response from server";
return { ok: false, errMsg: err };
}
else {
// Success
if (action == "backup" || action == "restore") {
let _thisTaskid = parseInt(responseText);
if (isNaN(_thisTaskid)) {
let err = `Invalid Task ID: ${_thisTaskid}`;
return { ok: false, errMsg: err };
}
else {
return { ok: true, thisTaskid: _thisTaskid };
}
}
else if (action == "stoptask") {
if (responseText == "1") {
return { ok: true };
}
else {
let err = `Unable to stop task`;
return { ok: false, errMsg: err };
}
}
else if (action == "gettaskstatus") {
try {
let thisJsonObject = JSON.parse(responseText);
return { ok: true, jsonObject: thisJsonObject };
}
catch (err) {
return { ok: false, errMsg: err };
}
}
}
}
}
else {
const err = await response.text();
return { ok: false, errMsg: err };
}
}
catch (err) {
return { ok: false, errMsg: err };
}
}
function resetUIValues() {
// Reset all display elements
Object.values(elements).forEach(element => {
if (element && element.textContent !== undefined) {
element.textContent = "--";
}
});
// Reset progress bar
elements.progressBar.style.width = '0%';
elements.labelPercent.style.display = "none";
elements.labelPercent.textContent = "0";
// Reset status styling
const statusContainer = elements.labelTaskStatus.closest('td');
if (statusContainer) {
statusContainer.className = "";
}
}
function startIntervalTimer() {
stopIntervalTimer();
intervalTimer = setInterval(fetchTaskStatus, intervalMs);
}
function stopIntervalTimer() {
if (intervalTimer) {
clearInterval(intervalTimer);
intervalTimer = null;
}
}
function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 Bytes';
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
// Message display functions (implement based on your UI framework)
function showSuccessMessage(title, message) {
console.log(` ${title}: ${message}`);
// Implement your success notification here
}
function showErrorMessage(title, message) {
console.error(` ${title}: ${message}`);
// Implement your error notification here
}
function showWarningMessage(title, message) {
console.warn(` ${title}: ${message}`);
// Implement your warning notification here
}