diff --git a/benchmate/api/actions/backup_site.py b/benchmate/api/actions/backup_site.py
new file mode 100644
index 0000000..6a0c683
--- /dev/null
+++ b/benchmate/api/actions/backup_site.py
@@ -0,0 +1,182 @@
+import os
+import subprocess
+import time
+
+import frappe
+
+from benchmate.api.utils import get_benchmate_settings
+
+
+def update_backup_log_status(docname, new_text=None, status=None):
+ """
+ Update the BM Log record for a given docname.
+ Appends log text and/or updates the status field, committing immediately.
+ """
+ try:
+ log_doc = frappe.get_doc("BM Log", docname)
+
+ # Append new log text if provided
+ if new_text:
+ updated_log = (log_doc.log or "") + new_text
+ log_doc.db_set("log", updated_log, update_modified=False)
+
+ # Update status if provided
+ if status:
+ log_doc.db_set("status", status, update_modified=False)
+
+ frappe.db.commit()
+ log_doc.reload()
+ except Exception as e:
+ frappe.log_error(f"Error updating BM Log: {e}", "BenchMate SiteBackupLogs")
+
+
+def backup_site_background(bench_name: str, bench_path: str, site_name: str, sudo_password: str):
+ """
+ Background task to take a backup of a Frappe site inside a given bench.
+ Captures real-time logs into BM Log doctype,
+ and cleans up temporary log files after completion.
+ """
+ bench_path = os.path.abspath(bench_path)
+ log_file = os.path.join(bench_path, f"bench_backup_site_{site_name}.log")
+
+ # Create a unique BM Log record for tracking
+ log_timestamp = int(time.time())
+ log_name = f"Backup Site-{log_timestamp}"
+ if not frappe.db.exists("BM Log", log_name):
+ frappe.get_doc(
+ {
+ "doctype": "BM Log",
+ "title": f"Backup Site - {site_name}",
+ "log": "",
+ "log_timestamp": log_timestamp,
+ "status": "In Process",
+ "action": "Backup Site",
+ }
+ ).insert(ignore_permissions=True)
+ frappe.db.commit()
+
+ # Command to run backup with files
+ cmd = [
+ "sudo",
+ "-S",
+ "bench",
+ "--site",
+ site_name,
+ "backup",
+ "--with-files",
+ ]
+
+ try:
+ # Launch subprocess and redirect stdout/stderr into a log file
+ with open(log_file, "w") as f:
+ proc = subprocess.Popen(
+ cmd,
+ cwd=bench_path,
+ stdin=subprocess.PIPE,
+ stdout=f,
+ stderr=subprocess.STDOUT,
+ text=True,
+ )
+ # Send sudo password
+ proc.stdin.write(sudo_password + "\n")
+ proc.stdin.flush()
+ proc.stdin.close()
+
+ # Wait for process completion with timeout (15 mins)
+ try:
+ proc.wait(timeout=900)
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ update_backup_log_status(
+ log_name,
+ new_text="\nTimed out while taking backup!\n",
+ status="Error",
+ )
+ frappe.msgprint(
+ msg=f"Timeout expired while backing up site {site_name}.",
+ title="Site Backup Timeout",
+ alert=True,
+ indicator="red",
+ )
+ return
+
+ # Tail the log file and update BM Log in real-time
+ with open(log_file) as f:
+ f.seek(0, os.SEEK_SET)
+ for line in f:
+ update_backup_log_status(log_name, new_text=line)
+
+ # Update status based on exit code
+ if proc.returncode == 0:
+ update_backup_log_status(log_name, status="Success")
+ else:
+ update_backup_log_status(log_name, status="Error")
+
+ except Exception as e:
+ frappe.msgprint(
+ msg=f"Error while taking backup of site {site_name} in bench {bench_name}",
+ title="Site Backup Error",
+ realtime=True,
+ alert=True,
+ indicator="red",
+ )
+ frappe.log_error(f"Error running bench backup: {e}", "BenchMate SiteBackupLogs")
+ update_backup_log_status(log_name, status="Error")
+
+ else:
+ frappe.msgprint(
+ msg=f"Backup for site {site_name} completed successfully in bench {bench_name}",
+ title="Site Backup Success",
+ realtime=True,
+ alert=True,
+ indicator="green",
+ )
+
+ finally:
+ # Always clean up the temporary log file
+ try:
+ if os.path.exists(log_file):
+ os.remove(log_file)
+ except Exception as cleanup_error:
+ frappe.log_error(
+ f"Failed to remove temp log file {log_file}: {cleanup_error}",
+ "BenchMate SiteBackupLogs",
+ )
+
+
+@frappe.whitelist()
+def execute(bench_name: str, bench_path: str, site_name: str):
+ """
+ Public API method (whitelisted) to enqueue site backup.
+ Validates input and enqueues the background site backup task.
+ """
+ if not bench_path or not site_name:
+ frappe.throw("bench_path and site_name are required", frappe.ValidationError)
+
+ # Fetch global BenchMate settings (sudo password)
+ settings = get_benchmate_settings()
+ sudo_password = settings.get("sudo_password")
+
+ if not sudo_password:
+ frappe.throw("Sudo password not configured", frappe.ValidationError)
+
+ try:
+ frappe.enqueue(
+ backup_site_background,
+ queue="long",
+ timeout=3600,
+ bench_name=bench_name,
+ bench_path=bench_path,
+ site_name=site_name,
+ sudo_password=sudo_password,
+ )
+ except Exception as e:
+ frappe.throw(f"Failed to enqueue site backup: {e!s}")
+
+ return {
+ "success": True,
+ "message": (
+ f"Backing up site {site_name} in the background. Check the BM Log for more details."
+ ),
+ "data": None,
+ }
diff --git a/benchmate/api/actions/create_site.py b/benchmate/api/actions/create_site.py
index e3f3ccf..76014c2 100644
--- a/benchmate/api/actions/create_site.py
+++ b/benchmate/api/actions/create_site.py
@@ -9,11 +9,11 @@
def update_log_status(docname, new_text=None, status=None):
"""
- ? Update the BM Site Creation Logs record for a given docname.
+ ? Update the BM Log record for a given docname.
? Appends log text and/or updates the status field, committing immediately.
"""
try:
- log_doc = frappe.get_doc("BM Site Creation Logs", docname)
+ log_doc = frappe.get_doc("BM Log", docname)
# ? Append new log text if provided
if new_text:
@@ -27,7 +27,7 @@ def update_log_status(docname, new_text=None, status=None):
frappe.db.commit()
log_doc.reload()
except Exception as e:
- frappe.log_error(f"Error updating BM Site Creation Logs: {e}", "BenchMate SiteCreationLogs")
+ frappe.log_error(f"Error updating BM Log: {e}", "BenchMate SiteCreationLogs")
def create_site_background(
@@ -35,23 +35,24 @@ def create_site_background(
):
"""
Background task to create a new Frappe site inside a given bench.
- Captures real-time logs into BM Site Creation Logs doctype,
+ Captures real-time logs into BM Log doctype,
and cleans up temporary log files after completion.
"""
bench_path = os.path.abspath(bench_path)
log_file = os.path.join(bench_path, f"bench_new_site_{site_name}.log")
- # ? Create a unique BM Site Creation Logs record for tracking
+ # ? Create a unique BM Log record for tracking
log_timestamp = int(time.time())
- log_name = f"{site_name}-{log_timestamp}"
- if not frappe.db.exists("BM Site Creation Logs", log_name):
+ log_name = f"Create Site-{log_timestamp}"
+ if not frappe.db.exists("BM Log", log_name):
frappe.get_doc(
{
- "doctype": "BM Site Creation Logs",
- "site_name": site_name,
+ "doctype": "BM Log",
+ "title": f"Create Site - {site_name}",
"log": "",
"log_timestamp": log_timestamp,
"status": "In Process",
+ "action": "Create Site",
}
).insert(ignore_permissions=True)
frappe.db.commit()
@@ -85,7 +86,7 @@ def create_site_background(
proc.stdin.flush()
proc.stdin.close()
- # ? Tail the log file and update BM Site Creation Logs in real-time
+ # ? Tail the log file and update BM Log in real-time
try:
with open(log_file) as f:
f.seek(0, os.SEEK_END) # ? Move to end for live tailing
@@ -233,6 +234,6 @@ def execute(bench_name: str, bench_path: str, site_name: str):
return {
"success": True,
- "message": f"Creating {site_name} in background Check BM Site Creation Logs for more details.",
- "data": {"bench_path": bench_path},
+ "message": f"Creating {site_name} in background Check BM Log for more details.",
+ "data": None,
}
diff --git a/benchmate/api/actions/drop_site.py b/benchmate/api/actions/drop_site.py
index 75bd5a5..639665a 100644
--- a/benchmate/api/actions/drop_site.py
+++ b/benchmate/api/actions/drop_site.py
@@ -9,11 +9,11 @@
def update_deletion_log_status(docname, new_text=None, status=None):
"""
- ? Update the BM Site Deletion Logs record for a given docname.
+ ? Update the BM Log record for a given docname.
? Appends log text and/or updates the status field, committing immediately.
"""
try:
- log_doc = frappe.get_doc("BM Site Deletion Logs", docname)
+ log_doc = frappe.get_doc("BM Log", docname)
# ? Append new log text if provided
if new_text:
@@ -27,7 +27,7 @@ def update_deletion_log_status(docname, new_text=None, status=None):
frappe.db.commit()
log_doc.reload()
except Exception as e:
- frappe.log_error(f"Error updating BM Site Deletion Logs: {e}", "BenchMate SiteDeletionLogs")
+ frappe.log_error(f"Error updating BM Log: {e}", "BenchMate SiteDeletionLogs")
def drop_site_background(
@@ -35,23 +35,24 @@ def drop_site_background(
):
"""
Background task to drop (delete) a Frappe site inside a given bench.
- Captures real-time logs into BM Site Deletion Logs doctype,
+ Captures real-time logs into BM Log doctype,
and cleans up temporary log files after completion.
"""
bench_path = os.path.abspath(bench_path)
log_file = os.path.join(bench_path, f"bench_drop_site_{site_name}.log")
- # ? Create a unique BM Site Deletion Logs record for tracking
+ # ? Create a unique BM Log record for tracking
log_timestamp = int(time.time())
- log_name = f"{site_name}-{log_timestamp}"
- if not frappe.db.exists("BM Site Deletion Logs", log_name):
+ log_name = f"Drop Site-{log_timestamp}"
+ if not frappe.db.exists("BM Log", log_name):
frappe.get_doc(
{
- "doctype": "BM Site Deletion Logs",
- "site_name": site_name,
+ "doctype": "BM Log",
+ "title": f"Drop Site - {site_name}",
"log": "",
"log_timestamp": log_timestamp,
"status": "In Process",
+ "action": "Drop Site",
}
).insert(ignore_permissions=True)
frappe.db.commit()
@@ -103,7 +104,7 @@ def drop_site_background(
)
return
- # ? Tail the log file and update BM Site Deletion Logs in real-time
+ # ? Tail the log file and update BM Log in real-time
with open(log_file) as f:
f.seek(0, os.SEEK_SET)
# ? Stream entire log file content to document
@@ -200,7 +201,7 @@ def execute(bench_name: str, bench_path: str, site_name: str):
"success": True,
"message": (
f"Deleting site {site_name} in the background. "
- f"Check the BM Site Deletion Logs for more details."
+ f"Check the BM Log for more details."
),
- "data": {"bench_path": bench_path},
+ "data": None,
}
diff --git a/benchmate/api/actions/restore_site.py b/benchmate/api/actions/restore_site.py
new file mode 100644
index 0000000..01a0aed
--- /dev/null
+++ b/benchmate/api/actions/restore_site.py
@@ -0,0 +1,216 @@
+import os
+import subprocess
+import time
+
+import frappe
+
+from benchmate.api.utils import get_benchmate_settings
+
+
+def update_restore_log_status(docname, new_text=None, status=None):
+ """
+ Update the BM Log record for a given docname.
+ Appends log text and/or updates the status field, committing immediately.
+ """
+ try:
+ log_doc = frappe.get_doc("BM Log", docname)
+
+ if new_text:
+ updated_log = (log_doc.log or "") + new_text
+ log_doc.db_set("log", updated_log, update_modified=False)
+
+ if status:
+ log_doc.db_set("status", status, update_modified=False)
+
+ frappe.db.commit()
+ log_doc.reload()
+ except Exception as e:
+ frappe.log_error(f"Error updating BM Log: {e}", "BenchMate SiteRestoreLogs")
+
+
+def restore_site_background(
+ bench_name: str,
+ bench_path: str,
+ site_name: str,
+ db_files_path: str,
+ public_files_path: str,
+ private_files_path: str,
+ sudo_password: str,
+ mysql_root_password: str,
+):
+ """
+ Background task to restore a Frappe site from backup files.
+ Captures real-time logs into BM Log doctype,
+ and cleans up temporary log files after completion.
+ """
+ bench_path = os.path.abspath(bench_path)
+ log_file = os.path.join(bench_path, f"bench_restore_site_{site_name}.log")
+
+ # Create BM Log record
+ log_timestamp = int(time.time())
+ log_name = f"Restore Site-{log_timestamp}"
+ if not frappe.db.exists("BM Log", log_name):
+ frappe.get_doc(
+ {
+ "doctype": "BM Log",
+ "title": f"Restore Site - {site_name}",
+ "log": "",
+ "log_timestamp": log_timestamp,
+ "status": "In Process",
+ "action": "Restore Site",
+ }
+ ).insert(ignore_permissions=True)
+ frappe.db.commit()
+
+ # Build restore command with MySQL root password
+ cmd = [
+ "sudo",
+ "-S",
+ "bench",
+ "--site",
+ site_name,
+ "--force",
+ "restore",
+ db_files_path,
+ "--with-public-files",
+ public_files_path,
+ "--with-private-files",
+ private_files_path,
+ "--mariadb-root-password",
+ mysql_root_password, # ✅ Pass MySQL root password
+ ]
+
+ try:
+ with open(log_file, "w") as f:
+ proc = subprocess.Popen(
+ cmd,
+ cwd=bench_path,
+ stdin=subprocess.PIPE,
+ stdout=f,
+ stderr=subprocess.STDOUT,
+ text=True,
+ )
+
+ # Send sudo password
+ proc.stdin.write(sudo_password + "\n")
+ proc.stdin.flush()
+ proc.stdin.close()
+
+ # Wait with timeout (20 mins)
+ try:
+ proc.wait(timeout=1200)
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ update_restore_log_status(
+ log_name,
+ new_text="\nTimed out while restoring site!\n",
+ status="Error",
+ )
+ frappe.msgprint(
+ msg=f"Timeout expired while restoring site {site_name}.",
+ title="Site Restore Timeout",
+ alert=True,
+ indicator="red",
+ )
+ return
+
+ # Stream logs into BM Log
+ with open(log_file) as f:
+ f.seek(0, os.SEEK_SET)
+ for line in f:
+ update_restore_log_status(log_name, new_text=line)
+
+ # Success or error status
+ if proc.returncode == 0:
+ update_restore_log_status(log_name, status="Success")
+ else:
+ update_restore_log_status(log_name, status="Error")
+
+ except Exception as e:
+ frappe.msgprint(
+ msg=f"Error while restoring site {site_name} in bench {bench_name}",
+ title="Site Restore Error",
+ realtime=True,
+ alert=True,
+ indicator="red",
+ )
+ frappe.log_error(f"Error running bench restore: {e}", "BenchMate SiteRestoreLogs")
+ update_restore_log_status(log_name, status="Error")
+
+ else:
+ frappe.msgprint(
+ msg=f"Site {site_name} restored successfully in bench {bench_name}",
+ title="Site Restore Success",
+ realtime=True,
+ alert=True,
+ indicator="green",
+ )
+
+ finally:
+ try:
+ if os.path.exists(log_file):
+ os.remove(log_file)
+ except Exception as cleanup_error:
+ frappe.log_error(
+ f"Failed to remove temp log file {log_file}: {cleanup_error}",
+ "BenchMate SiteRestoreLogs",
+ )
+
+
+@frappe.whitelist()
+def execute(
+ bench_name: str,
+ bench_path: str,
+ site_name: str,
+ db_files_path: str,
+ public_files_path: str,
+ private_files_path: str,
+):
+ """
+ Public API method (whitelisted) to enqueue site restore.
+ Validates input and enqueues the background site restore task.
+ """
+ if not bench_path or not site_name or not db_files_path:
+ frappe.throw("bench_path, site_name and db_files_path are required", frappe.ValidationError)
+
+ # ? Update the paths with full paths as per the current bench and site
+ absolute_site_path = os.path.abspath(frappe.get_site_path())
+ db_files_path = os.path.join(absolute_site_path, db_files_path.lstrip("/"))
+ public_files_path = os.path.join(absolute_site_path, public_files_path.lstrip("/"))
+ private_files_path = os.path.join(absolute_site_path, private_files_path.lstrip("/"))
+
+ settings = get_benchmate_settings()
+ sudo_password = settings.get("sudo_password")
+ mysql_root_password = settings.get("db_password")
+
+ if not sudo_password:
+ frappe.throw("Sudo password not configured", frappe.ValidationError)
+
+ if not mysql_root_password:
+ frappe.throw("MySQL root password not configured", frappe.ValidationError)
+
+ try:
+ frappe.enqueue(
+ restore_site_background,
+ queue="long",
+ timeout=7200,
+ bench_name=bench_name,
+ bench_path=bench_path,
+ site_name=site_name,
+ db_files_path=db_files_path,
+ public_files_path=public_files_path,
+ private_files_path=private_files_path,
+ sudo_password=sudo_password,
+ mysql_root_password=mysql_root_password,
+ )
+
+ except Exception as e:
+ frappe.throw(f"Failed to enqueue site restore: {e!s}")
+
+ return {
+ "success": True,
+ "message": (
+ f"Restoring site {site_name} in the background. Check the BM Log for more details."
+ ),
+ "data": None,
+ }
diff --git a/benchmate/api/sync.py b/benchmate/api/sync.py
index 18fde8e..7b1b0a3 100644
--- a/benchmate/api/sync.py
+++ b/benchmate/api/sync.py
@@ -3,6 +3,7 @@
import json
import os
import subprocess
+import time
from pathlib import Path
import frappe
@@ -153,15 +154,17 @@ def sync_bench_details():
# ? Log sync error
frappe.get_doc(
{
- "doctype": "BM Sync Log",
+ "doctype": "BM Log",
"title": message,
"status": "Error",
"log": json.dumps(data, indent=4),
+ "log_timestamp": int(time.time()),
+ "action": "Sync",
}
).insert()
frappe.log_error(message, str(e))
- # ? Commit the BM Sync Log
+ # ? Commit the BM Log
frappe.db.commit()
return {
@@ -172,7 +175,7 @@ def sync_bench_details():
else:
# ? Prepare success log
- message = "Benches synced successfully."
+ message = "Benches synced successfully"
data = {
"updated_benches": updated_benches,
"updated_apps": updated_apps,
@@ -182,10 +185,12 @@ def sync_bench_details():
# ? Log sync success
frappe.get_doc(
{
- "doctype": "BM Sync Log",
+ "doctype": "BM Log",
"title": message,
"status": "Success",
"log": json.dumps(data, indent=4),
+ "log_timestamp": int(time.time()),
+ "action": "Sync",
}
).insert()
diff --git a/benchmate/benchmate/doctype/bm_bench/bm_bench.js b/benchmate/benchmate/doctype/bm_bench/bm_bench.js
index 4c6eb72..75ae9cc 100644
--- a/benchmate/benchmate/doctype/bm_bench/bm_bench.js
+++ b/benchmate/benchmate/doctype/bm_bench/bm_bench.js
@@ -28,6 +28,24 @@ function addBenchActions(frm) {
__("Actions")
);
+ // ? Add "Backup Site" button and pair it with handler
+ frm.add_custom_button(
+ __("Backup Site"),
+ function () {
+ backupSite(frm);
+ },
+ __("Actions")
+ );
+
+ // ? Add "Restore Site" button and pair it with handler
+ frm.add_custom_button(
+ __("Restore Site"),
+ function () {
+ restoreSite(frm);
+ },
+ __("Actions")
+ );
+
// ? Add "Start Bench" button and pair it with handler
frm.add_custom_button(
__("Start Bench"),
@@ -47,6 +65,7 @@ function addBenchActions(frm) {
);
}
+// ? Function to handle Cerate Site action
function createSite(frm) {
let dialog = new frappe.ui.Dialog({
title: __("Create New Site"),
@@ -180,7 +199,7 @@ function dropSite(frm) {
// ? Fields inside the dialog
fields: [
{
- label: __("Site"), // ? BM Site link field
+ label: __("Site"),
fieldname: "site",
fieldtype: "Link",
options: "BM Site",
@@ -213,7 +232,7 @@ function dropSite(frm) {
},
},
{
- label: __("Site Name"), // ? Read-only field to confirm exact site_name
+ label: __("Site Name"),
fieldname: "site_name",
fieldtype: "Data",
read_only: 1,
@@ -231,9 +250,9 @@ function dropSite(frm) {
frappe.call({
method: "benchmate.api.actions.drop_site.execute",
args: {
- bench_name: frm.doc.name, // ? Current bench name
- bench_path: frm.doc.path, // ? Bench path
- site_name: values.site_name, // ? Site selected to drop
+ bench_name: frm.doc.name,
+ bench_path: frm.doc.path,
+ site_name: values.site_name,
},
freeze: true,
freeze_message: __(`Dropping Site ${values.site_name}...`),
@@ -267,3 +286,227 @@ function dropSite(frm) {
// ? Display the dialog to the user
dialog.show();
}
+
+// ? Function to handle the Backup Site action from BM Bench form
+function backupSite(frm) {
+ // ? Create a dialog box for site selection and backup confirmation
+ let dialog = new frappe.ui.Dialog({
+ title: __("Backup A Site"),
+
+ // ? Fields inside the dialog
+ fields: [
+ {
+ label: __("Site"),
+ fieldname: "site",
+ fieldtype: "Link",
+ options: "BM Site",
+ reqd: 1,
+ get_query: function () {
+ // ? Restrict sites only for the current bench
+ return {
+ filters: [["bench_name", "=", frm.doc.name]],
+ };
+ },
+ onchange: function () {
+ // ? On site selection, fetch the actual site_name
+ let site = dialog.get_value("site");
+ if (site) {
+ frappe.call({
+ method: "frappe.client.get_value",
+ args: {
+ doctype: "BM Site",
+ filters: { name: site },
+ fieldname: ["site_name"],
+ },
+ freeze: true,
+ freeze_message: __(`Fetching Site Name...`),
+ callback: function (response) {
+ // ? Auto-populate the readonly Site Name field
+ dialog.set_value("site_name", response.message.site_name);
+ },
+ });
+ }
+ },
+ },
+ {
+ label: __("Site Name"),
+ fieldname: "site_name",
+ fieldtype: "Data",
+ read_only: 1,
+ reqd: 1,
+ },
+ ],
+
+ // ? Primary action button: Backup Site
+ primary_action_label: __("Backup"),
+ primary_action(values) {
+ // ? Hide dialog after confirmation
+ dialog.hide();
+
+ // ? Call server-side method to backup the site
+ frappe.call({
+ method: "benchmate.api.actions.backup_site.execute",
+ args: {
+ bench_name: frm.doc.name,
+ bench_path: frm.doc.path,
+ site_name: values.site_name,
+ },
+ freeze: true,
+ freeze_message: __(`Taking Backup Of Site ${values.site_name}...`),
+
+ // ? Handle callback after server execution
+ callback: function (r) {
+ if (r.message.success) {
+ // ? Show success message on site drop
+ frappe.show_alert(
+ {
+ message: __(r.message.message),
+ indicator: "green",
+ },
+ 5
+ );
+ } else {
+ // ? Show error message if failed
+ frappe.show_alert(
+ {
+ message: __(r.message.message),
+ indicator: "red",
+ },
+ 5
+ );
+ }
+ },
+ });
+ },
+ });
+
+ // ? Display the dialog to the user
+ dialog.show();
+}
+
+// ? Function to handle the Restore Site action from BM Bench form
+function restoreSite(frm) {
+ // ? Create a dialog box for site selection and restore confirmation
+ let dialog = new frappe.ui.Dialog({
+ title: __("Restore A Site"),
+
+ // ? Fields inside the dialog
+ fields: [
+ {
+ label: __("Site"),
+ fieldname: "site",
+ fieldtype: "Link",
+ options: "BM Site",
+ reqd: 1,
+ get_query: function () {
+ // ? Restrict sites only for the current bench
+ return {
+ filters: [["bench_name", "=", frm.doc.name]],
+ };
+ },
+ onchange: function () {
+ // ? On site selection, fetch the actual site_name
+ let site = dialog.get_value("site");
+ if (site) {
+ frappe.call({
+ method: "frappe.client.get_value",
+ args: {
+ doctype: "BM Site",
+ filters: { name: site },
+ fieldname: ["site_name"],
+ },
+ freeze: true,
+ freeze_message: __(`Fetching Site Name...`),
+ callback: function (response) {
+ // ? Auto-populate the readonly Site Name field
+ dialog.set_value("site_name", response.message.site_name);
+ },
+ });
+ }
+ },
+ },
+ {
+ label: __("Site Name"),
+ fieldname: "site_name",
+ fieldtype: "Data",
+ read_only: 1,
+ reqd: 1,
+ },
+ {
+ label: __("Database Files"),
+ fieldname: "db_files_path",
+ fieldtype: "Attach",
+ description:
+ "Upload the database backup file. Usually file name ends in .sql.gz or .sql",
+ reqd: 1,
+ options: { restrictions: { allowed_file_types: [".sql", ".sql.gz"] } },
+ },
+ {
+ label: __("Public Files"),
+ fieldname: "public_files_path",
+ fieldtype: "Attach",
+ description:
+ "Upload the public files backup. Usually file name ends in -files.tar",
+ reqd: 1,
+ options: { restrictions: { allowed_file_types: [".tar"] } },
+ },
+ {
+ label: __("Private Files"),
+ fieldname: "private_files_path",
+ fieldtype: "Attach",
+ description:
+ "Upload the private files backup. Usually file name ends in -private-files.tar",
+ reqd: 1,
+ options: { restrictions: { allowed_file_types: [".tar"] } },
+ },
+ ],
+
+ // ? Primary action button: Restore Site
+ primary_action_label: __("Restore"),
+ primary_action(values) {
+ // ? Hide dialog after confirmation
+ dialog.hide();
+
+ // ? Call server-side method to restore the site
+ frappe.call({
+ method: "benchmate.api.actions.restore_site.execute",
+ args: {
+ bench_name: frm.doc.name,
+ bench_path: frm.doc.path,
+ site_name: values.site_name,
+ db_files_path: values.db_files_path,
+ public_files_path: values.public_files_path,
+ private_files_path: values.private_files_path,
+ },
+ freeze: true,
+ freeze_message: __(`Restoring Site ${values.site_name}...`),
+
+ // ? Handle callback after server execution
+ callback: function (r) {
+ if (r.message.success) {
+ // ? Show success message on site drop
+ frappe.show_alert(
+ {
+ message: __(r.message.message),
+ indicator: "green",
+ },
+ 5
+ );
+ } else {
+ // ? Show error message if failed
+ frappe.show_alert(
+ {
+ message: __(r.message.message),
+ indicator: "red",
+ },
+ 5
+ );
+ }
+ },
+ });
+ },
+ });
+
+ // ? Display the dialog to the user
+ dialog.show();
+}
diff --git a/benchmate/benchmate/doctype/bm_site_creation_logs/__init__.py b/benchmate/benchmate/doctype/bm_log/__init__.py
similarity index 100%
rename from benchmate/benchmate/doctype/bm_site_creation_logs/__init__.py
rename to benchmate/benchmate/doctype/bm_log/__init__.py
diff --git a/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.js b/benchmate/benchmate/doctype/bm_log/bm_log.js
similarity index 78%
rename from benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.js
rename to benchmate/benchmate/doctype/bm_log/bm_log.js
index 6262056..07e8da0 100644
--- a/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.js
+++ b/benchmate/benchmate/doctype/bm_log/bm_log.js
@@ -1,7 +1,7 @@
// Copyright (c) 2025, Karan Mistry and contributors
// For license information, please see license.txt
-// frappe.ui.form.on("BM Sync Log", {
+// frappe.ui.form.on("BM Log", {
// refresh(frm) {
// },
diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.json b/benchmate/benchmate/doctype/bm_log/bm_log.json
similarity index 77%
rename from benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.json
rename to benchmate/benchmate/doctype/bm_log/bm_log.json
index 5302bd1..c694a72 100644
--- a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.json
+++ b/benchmate/benchmate/doctype/bm_log/bm_log.json
@@ -1,12 +1,13 @@
{
"actions": [],
"allow_rename": 1,
- "autoname": "format:{site_name}-{log_timestamp}",
- "creation": "2025-09-07 23:41:44.865581",
+ "autoname": "format:{action}-{log_timestamp}",
+ "creation": "2025-09-08 00:09:25.552471",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
- "site_name",
+ "title",
+ "action",
"column_break_bkvd",
"status",
"log_timestamp",
@@ -14,14 +15,6 @@
"log"
],
"fields": [
- {
- "fieldname": "site_name",
- "fieldtype": "Data",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Site Name",
- "read_only": 1
- },
{
"fieldname": "column_break_bkvd",
"fieldtype": "Column Break"
@@ -40,7 +33,6 @@
"fieldname": "log_timestamp",
"fieldtype": "Data",
"in_list_view": 1,
- "in_standard_filter": 1,
"label": "Log Timestamp",
"read_only": 1,
"unique": 1
@@ -55,15 +47,32 @@
"in_list_view": 1,
"label": "Log",
"read_only": 1
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Title",
+ "read_only": 1
+ },
+ {
+ "fieldname": "action",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Action",
+ "options": "Other\nSync\nCreate Site\nDrop Site\nBackup Site\nRestore Site\nStart Bench\nStop Bench",
+ "read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2025-09-07 23:41:44.865581",
+ "modified": "2025-09-08 00:20:53.335826",
"modified_by": "Administrator",
"module": "BenchMate",
- "name": "BM Site Deletion Logs",
+ "name": "BM Log",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
@@ -97,6 +106,6 @@
"title": "Error"
}
],
- "title_field": "site_name",
+ "title_field": "title",
"track_seen": 1
}
diff --git a/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.py b/benchmate/benchmate/doctype/bm_log/bm_log.py
similarity index 86%
rename from benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.py
rename to benchmate/benchmate/doctype/bm_log/bm_log.py
index 7d0f3ed..5999550 100644
--- a/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.py
+++ b/benchmate/benchmate/doctype/bm_log/bm_log.py
@@ -5,5 +5,5 @@
from frappe.model.document import Document
-class BMSyncLog(Document):
+class BMLog(Document):
pass
diff --git a/benchmate/benchmate/doctype/bm_sync_log/test_bm_sync_log.py b/benchmate/benchmate/doctype/bm_log/test_bm_log.py
similarity index 85%
rename from benchmate/benchmate/doctype/bm_sync_log/test_bm_sync_log.py
rename to benchmate/benchmate/doctype/bm_log/test_bm_log.py
index 3e49b0e..19380e1 100644
--- a/benchmate/benchmate/doctype/bm_sync_log/test_bm_sync_log.py
+++ b/benchmate/benchmate/doctype/bm_log/test_bm_log.py
@@ -11,9 +11,9 @@
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
-class IntegrationTestBMSyncLog(IntegrationTestCase):
+class IntegrationTestBMLog(IntegrationTestCase):
"""
- Integration tests for BMSyncLog.
+ Integration tests for BMLog.
Use this class for testing interactions between multiple components.
"""
diff --git a/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.js b/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.js
deleted file mode 100644
index 57d60fc..0000000
--- a/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2025, Karan Mistry and contributors
-// For license information, please see license.txt
-
-// frappe.ui.form.on("BM Site Creation Logs", {
-// refresh(frm) {
-
-// },
-// });
diff --git a/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.json b/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.json
deleted file mode 100644
index e8c4c71..0000000
--- a/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.json
+++ /dev/null
@@ -1,102 +0,0 @@
-{
- "actions": [],
- "allow_rename": 1,
- "autoname": "format:{site_name}-{log_timestamp}",
- "creation": "2025-08-27 02:56:40.293176",
- "doctype": "DocType",
- "engine": "InnoDB",
- "field_order": [
- "site_name",
- "column_break_bkvd",
- "status",
- "log_timestamp",
- "section_break_jlrx",
- "log"
- ],
- "fields": [
- {
- "fieldname": "column_break_bkvd",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "section_break_jlrx",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "log",
- "fieldtype": "Long Text",
- "in_list_view": 1,
- "label": "Log",
- "read_only": 1
- },
- {
- "default": "In Process",
- "fieldname": "status",
- "fieldtype": "Select",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Status",
- "options": "\nIn Process\nSuccess\nError",
- "read_only": 1
- },
- {
- "fieldname": "site_name",
- "fieldtype": "Data",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Site Name",
- "read_only": 1
- },
- {
- "fieldname": "log_timestamp",
- "fieldtype": "Data",
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Log Timestamp",
- "read_only": 1,
- "unique": 1
- }
- ],
- "grid_page_length": 50,
- "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2025-08-27 03:39:40.158232",
- "modified_by": "Administrator",
- "module": "BenchMate",
- "name": "BM Site Creation Logs",
- "naming_rule": "Expression",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "row_format": "Dynamic",
- "sort_field": "creation",
- "sort_order": "DESC",
- "states": [
- {
- "color": "Blue",
- "title": "In Process"
- },
- {
- "color": "Green",
- "title": "Success"
- },
- {
- "color": "Red",
- "title": "Error"
- }
- ],
- "title_field": "site_name",
- "track_seen": 1
-}
diff --git a/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.py b/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.py
deleted file mode 100644
index 1639f9c..0000000
--- a/benchmate/benchmate/doctype/bm_site_creation_logs/bm_site_creation_logs.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2025, Karan Mistry and contributors
-# For license information, please see license.txt
-
-# import frappe
-from frappe.model.document import Document
-
-
-class BMSiteCreationLogs(Document):
- pass
diff --git a/benchmate/benchmate/doctype/bm_site_creation_logs/test_bm_site_creation_logs.py b/benchmate/benchmate/doctype/bm_site_creation_logs/test_bm_site_creation_logs.py
deleted file mode 100644
index 8169f2c..0000000
--- a/benchmate/benchmate/doctype/bm_site_creation_logs/test_bm_site_creation_logs.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright (c) 2025, Karan Mistry and Contributors
-# See license.txt
-
-# import frappe
-from frappe.tests import IntegrationTestCase
-
-# On IntegrationTestCase, the doctype test records and all
-# link-field test record dependencies are recursively loaded
-# Use these module variables to add/remove to/from that list
-EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
-IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
-
-
-class IntegrationTestBMSiteCreationLogs(IntegrationTestCase):
- """
- Integration tests for BMSiteCreationLogs.
- Use this class for testing interactions between multiple components.
- """
-
- pass
diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/__init__.py b/benchmate/benchmate/doctype/bm_site_deletion_logs/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.js b/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.js
deleted file mode 100644
index 95cd396..0000000
--- a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2025, Karan Mistry and contributors
-// For license information, please see license.txt
-
-// frappe.ui.form.on("BM Site Deletion Logs", {
-// refresh(frm) {
-
-// },
-// });
diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.py b/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.py
deleted file mode 100644
index 0d96f97..0000000
--- a/benchmate/benchmate/doctype/bm_site_deletion_logs/bm_site_deletion_logs.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2025, Karan Mistry and contributors
-# For license information, please see license.txt
-
-# import frappe
-from frappe.model.document import Document
-
-
-class BMSiteDeletionLogs(Document):
- pass
diff --git a/benchmate/benchmate/doctype/bm_site_deletion_logs/test_bm_site_deletion_logs.py b/benchmate/benchmate/doctype/bm_site_deletion_logs/test_bm_site_deletion_logs.py
deleted file mode 100644
index c239de8..0000000
--- a/benchmate/benchmate/doctype/bm_site_deletion_logs/test_bm_site_deletion_logs.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright (c) 2025, Karan Mistry and Contributors
-# See license.txt
-
-# import frappe
-from frappe.tests import IntegrationTestCase
-
-# On IntegrationTestCase, the doctype test records and all
-# link-field test record dependencies are recursively loaded
-# Use these module variables to add/remove to/from that list
-EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
-IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
-
-
-class IntegrationTestBMSiteDeletionLogs(IntegrationTestCase):
- """
- Integration tests for BMSiteDeletionLogs.
- Use this class for testing interactions between multiple components.
- """
-
- pass
diff --git a/benchmate/benchmate/doctype/bm_sync_log/__init__.py b/benchmate/benchmate/doctype/bm_sync_log/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.json b/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.json
deleted file mode 100644
index 2b3bbdf..0000000
--- a/benchmate/benchmate/doctype/bm_sync_log/bm_sync_log.json
+++ /dev/null
@@ -1,90 +0,0 @@
-{
- "actions": [],
- "allow_rename": 1,
- "autoname": "format:BM-SL-{#}",
- "creation": "2025-08-18 01:20:36.254906",
- "doctype": "DocType",
- "engine": "MyISAM",
- "field_order": [
- "title",
- "column_break_nerx",
- "status",
- "section_break_djza",
- "log"
- ],
- "fields": [
- {
- "fieldname": "title",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Title",
- "read_only": 1
- },
- {
- "fieldname": "log",
- "fieldtype": "Long Text",
- "in_list_view": 1,
- "label": "Log",
- "read_only": 1
- },
- {
- "default": "Error",
- "fieldname": "status",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Status",
- "options": "\nError\nSuccess\nLog",
- "read_only": 1
- },
- {
- "fieldname": "column_break_nerx",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "section_break_djza",
- "fieldtype": "Section Break"
- }
- ],
- "grid_page_length": 50,
- "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2025-08-27 03:00:11.397603",
- "modified_by": "Administrator",
- "module": "BenchMate",
- "name": "BM Sync Log",
- "naming_rule": "Expression",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "row_format": "Dynamic",
- "sort_field": "creation",
- "sort_order": "DESC",
- "states": [
- {
- "color": "Red",
- "title": "Error"
- },
- {
- "color": "Green",
- "title": "Success"
- },
- {
- "color": "Blue",
- "title": "Log"
- }
- ],
- "title_field": "title",
- "track_seen": 1
-}
diff --git a/benchmate/benchmate/workspace/benchmate/benchmate.json b/benchmate/benchmate/workspace/benchmate/benchmate.json
index ad73c54..b387095 100644
--- a/benchmate/benchmate/workspace/benchmate/benchmate.json
+++ b/benchmate/benchmate/workspace/benchmate/benchmate.json
@@ -1,7 +1,7 @@
{
"app": "benchmate",
"charts": [],
- "content": "[{\"id\":\"E4TboPQAiW\",\"type\":\"header\",\"data\":{\"text\":\"BenchMate\",\"col\":12}},{\"id\":\"pckRgVGLCJ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Bench\",\"col\":3}},{\"id\":\"uIKLe2weO2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Site\",\"col\":3}},{\"id\":\"1zPvETmYcy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM App\",\"col\":3}},{\"id\":\"OdX7vGpNVa\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Sync Log\",\"col\":3}},{\"id\":\"CZYPa4MOMK\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Bm Site Creation Logs\",\"col\":3}},{\"id\":\"5GjUZsGiN_\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Bm Site Deletion Logs\",\"col\":3}},{\"id\":\"Zlbft996Gt\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Settings\",\"col\":3}}]",
+ "content": "[{\"id\":\"E4TboPQAiW\",\"type\":\"header\",\"data\":{\"text\":\"BenchMate\",\"col\":12}},{\"id\":\"pckRgVGLCJ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Bench\",\"col\":3}},{\"id\":\"uIKLe2weO2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Site\",\"col\":3}},{\"id\":\"1zPvETmYcy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM App\",\"col\":3}},{\"id\":\"IKOaT4BzS3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Log\",\"col\":3}},{\"id\":\"Zlbft996Gt\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BM Settings\",\"col\":3}}]",
"creation": "2025-08-18 03:30:43.609066",
"custom_blocks": [],
"docstatus": 0,
@@ -15,7 +15,7 @@
"label": "BenchMate",
"link_type": "DocType",
"links": [],
- "modified": "2025-09-07 23:42:31.333942",
+ "modified": "2025-09-08 00:28:19.448700",
"modified_by": "Administrator",
"module": "BenchMate",
"name": "BenchMate",
@@ -27,14 +27,6 @@
"roles": [],
"sequence_id": 23.0,
"shortcuts": [
- {
- "color": "Grey",
- "doc_view": "List",
- "label": "Bm Site Creation Logs",
- "link_to": "BM Site Creation Logs",
- "stats_filter": "[]",
- "type": "DocType"
- },
{
"color": "Grey",
"doc_view": "List",
@@ -47,8 +39,8 @@
{
"color": "Grey",
"doc_view": "List",
- "label": "Bm Site Deletion Logs",
- "link_to": "BM Site Deletion Logs",
+ "label": "BM Log",
+ "link_to": "BM Log",
"stats_filter": "[]",
"type": "DocType"
},
@@ -70,15 +62,6 @@
"stats_filter": "[]",
"type": "DocType"
},
- {
- "color": "Grey",
- "doc_view": "List",
- "format": "{}",
- "label": "BM Sync Log",
- "link_to": "BM Sync Log",
- "stats_filter": "[]",
- "type": "DocType"
- },
{
"color": "Grey",
"doc_view": "List",