feat: Initial commit for OnEver Drive centralized backup system with Windows PyQt6 agent and Proxmox LXC deployment
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import socket
|
||||
import platform
|
||||
import time
|
||||
from pathlib import Path
|
||||
import httpx
|
||||
from colorama import init, Fore, Style
|
||||
|
||||
# Add agent directory to sys.path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from agent.config import load_config, save_config, AgentConfig
|
||||
from agent.uploader import ChunkUploader
|
||||
from agent.service import AgentDaemon
|
||||
|
||||
init(autoreset=True)
|
||||
|
||||
def print_banner():
|
||||
print(Fore.CYAN + Style.BRIGHT + """
|
||||
+------------------------------------------------------------------+
|
||||
| ONEVER DRIVE - WINDOWS AGENT CLI |
|
||||
| Enterprise Resilient Chunk Backup Engine for Windows |
|
||||
+------------------------------------------------------------------+
|
||||
""")
|
||||
|
||||
def cmd_register(args):
|
||||
print_banner()
|
||||
server_url = args.server.rstrip("/")
|
||||
code = args.code.strip().upper()
|
||||
|
||||
hostname = socket.gethostname()
|
||||
os_info = f"{platform.system()} {platform.release()} (Build {platform.version()})"
|
||||
|
||||
print(Fore.YELLOW + f"Connecting to {server_url} with registration code: {code}...")
|
||||
|
||||
payload = {
|
||||
"registration_code": code,
|
||||
"name": args.name or hostname,
|
||||
"hostname": hostname,
|
||||
"os_info": os_info,
|
||||
"agent_version": "1.0.0"
|
||||
}
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
resp = client.post(f"{server_url}/api/clients/register", json=payload)
|
||||
if resp.status_code != 200:
|
||||
print(Fore.RED + f"Registration failed ({resp.status_code}): {resp.text}")
|
||||
sys.exit(1)
|
||||
|
||||
data = resp.json()
|
||||
config = load_config()
|
||||
config.server_url = server_url
|
||||
config.client_code = data["client_code"]
|
||||
config.device_id = data["device_id"]
|
||||
config.device_token = data["device_token"]
|
||||
config.client_name = data["name"]
|
||||
save_config(config)
|
||||
|
||||
print(Fore.GREEN + Style.BRIGHT + "\n[+] Agent registered successfully!")
|
||||
print(Fore.WHITE + f" Client Code : {data['client_code']}")
|
||||
print(Fore.WHITE + f" Device ID : {data['device_id']}")
|
||||
print(Fore.WHITE + f" Client Name : {data['name']}")
|
||||
print(Fore.CYAN + "\nYou can now start the agent daemon or run manual backups.")
|
||||
|
||||
except Exception as ex:
|
||||
print(Fore.RED + f"Error connecting to server: {str(ex)}")
|
||||
sys.exit(1)
|
||||
|
||||
def cmd_status(args):
|
||||
print_banner()
|
||||
config = load_config()
|
||||
if not config.device_id:
|
||||
print(Fore.YELLOW + "Agent is NOT registered yet. Run 'agent_cli.py register' first.")
|
||||
return
|
||||
|
||||
print(Fore.GREEN + "[*] Agent Configuration:")
|
||||
print(f" Server URL : {config.server_url}")
|
||||
print(f" Client Code : {config.client_code}")
|
||||
print(f" Device ID : {config.device_id}")
|
||||
print(f" Client Name : {config.client_name}")
|
||||
print(f" Chunk Size : {config.chunk_size / (1024*1024):.1f} MB")
|
||||
|
||||
print(Fore.CYAN + "\n[*] Testing connection to server...")
|
||||
try:
|
||||
headers = {
|
||||
"X-Device-Id": config.device_id,
|
||||
"X-Device-Token": config.device_token
|
||||
}
|
||||
with httpx.Client(base_url=config.server_url, headers=headers, timeout=10.0) as client:
|
||||
resp = client.get("/api/jobs/agent/assigned")
|
||||
if resp.status_code == 200:
|
||||
jobs = resp.json()
|
||||
print(Fore.GREEN + f" Connection OK. Assigned jobs count: {len(jobs)}")
|
||||
for j in jobs:
|
||||
print(Fore.WHITE + f" - [{j['job_code']}] {j['name']} ({j['source_path']} | {j['file_patterns']})")
|
||||
else:
|
||||
print(Fore.RED + f" Server returned {resp.status_code}: {resp.text}")
|
||||
except Exception as ex:
|
||||
print(Fore.RED + f" Connection failed: {str(ex)}")
|
||||
|
||||
def cmd_backup(args):
|
||||
print_banner()
|
||||
filepath = Path(args.file).resolve()
|
||||
if not filepath.exists():
|
||||
print(Fore.RED + f"File not found: {filepath}")
|
||||
sys.exit(1)
|
||||
|
||||
config = load_config()
|
||||
if not config.device_id:
|
||||
print(Fore.RED + "Agent is not registered. Run registration first.")
|
||||
sys.exit(1)
|
||||
|
||||
print(Fore.CYAN + f"[*] Initiating chunked backup for: {filepath.name}")
|
||||
print(f" File size : {filepath.stat().st_size / (1024*1024):.2f} MB")
|
||||
print(f" Chunk size : {config.chunk_size / (1024*1024):.1f} MB")
|
||||
|
||||
uploader = ChunkUploader(config)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
def print_progress(received, total, pct):
|
||||
bar_len = 30
|
||||
filled = int(bar_len * (pct / 100))
|
||||
bar = "#" * filled + "-" * (bar_len - filled)
|
||||
sys.stdout.write(f"\r{Fore.YELLOW}Progress: [{bar}] {pct:.1f}% ({received}/{total} chunks)")
|
||||
sys.stdout.flush()
|
||||
|
||||
try:
|
||||
result = uploader.upload_file(filepath, job_id=args.job, progress_callback=print_progress)
|
||||
elapsed = max(0.01, time.time() - start_time)
|
||||
mb = filepath.stat().st_size / (1024 * 1024)
|
||||
speed = mb / elapsed
|
||||
|
||||
print(Fore.GREEN + Style.BRIGHT + f"\n\n[+] Backup Complete & Verified!")
|
||||
print(Fore.WHITE + f" Remote Path : {result.get('relative_path')}")
|
||||
print(Fore.WHITE + f" SHA-256 : {result.get('sha256')}")
|
||||
print(Fore.WHITE + f" Transfer : {mb:.2f} MB in {elapsed:.2f}s ({speed:.2f} MB/s)")
|
||||
except Exception as ex:
|
||||
print(Fore.RED + f"\n[-] Backup failed: {str(ex)}")
|
||||
sys.exit(1)
|
||||
|
||||
def cmd_daemon(args):
|
||||
print_banner()
|
||||
config = load_config()
|
||||
daemon = AgentDaemon(config)
|
||||
daemon.start()
|
||||
print(Fore.GREEN + "[*] Agent daemon running. Press Ctrl+C to stop.")
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
daemon.stop()
|
||||
print(Fore.YELLOW + "\nAgent stopped.")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="OnEver Drive Windows Agent CLI")
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
|
||||
# Register
|
||||
p_reg = subparsers.add_parser("register", help="Register agent with central server")
|
||||
p_reg.add_argument("--server", required=True, help="Server URL (e.g. http://192.168.1.100:8000)")
|
||||
p_reg.add_argument("--code", required=True, help="Registration code (e.g. OED-A1B2-C3D4)")
|
||||
p_reg.add_argument("--name", help="Custom name for this client machine")
|
||||
p_reg.set_defaults(func=cmd_register)
|
||||
|
||||
# Status
|
||||
p_stat = subparsers.add_parser("status", help="Show current agent status and server connectivity")
|
||||
p_stat.set_defaults(func=cmd_status)
|
||||
|
||||
# Backup
|
||||
p_bak = subparsers.add_parser("backup", help="Perform manual chunked backup of a file")
|
||||
p_bak.add_argument("--file", required=True, help="Path to file to back up")
|
||||
p_bak.add_argument("--job", type=int, help="Optional Backup Job ID")
|
||||
p_bak.set_defaults(func=cmd_backup)
|
||||
|
||||
# Daemon
|
||||
p_daemon = subparsers.add_parser("daemon", help="Run the background worker loop in foreground")
|
||||
p_daemon.set_defaults(func=cmd_daemon)
|
||||
|
||||
args = parser.parse_args()
|
||||
if hasattr(args, "func"):
|
||||
args.func(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user