feat: Introduce initial application structure, SQL database tools, and license management.
This commit is contained in:
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Distribution / Build
|
||||
build/
|
||||
dist/
|
||||
# *.spec # Keep spec file for reproducible builds
|
||||
|
||||
# IDE / Editors
|
||||
.vscode/
|
||||
.idea/
|
||||
.gemini/
|
||||
|
||||
# Project Specific - Sensitive Data
|
||||
config.dat
|
||||
config.key
|
||||
license.dat
|
||||
|
||||
# Large Binaries / Tools
|
||||
# sql_tools/ # Diizinkan untuk distribusi
|
||||
sql_tools_zip/
|
||||
|
||||
# Backup Files
|
||||
*.sql
|
||||
*.zip
|
||||
*.rar
|
||||
*.bak
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
36
Keygen.spec
Normal file
36
Keygen.spec
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['keygen.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='KeyProBackup', # Name of the executable
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False, # Important for XP
|
||||
upx=False,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False, # GUI Mode
|
||||
icon='icon.ico',
|
||||
)
|
||||
108
ProBackup.spec
Normal file
108
ProBackup.spec
Normal file
@@ -0,0 +1,108 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
import PyQt5
|
||||
|
||||
# --- MONKEYPATCH PyInstaller QT Detection (Force correct path) ---
|
||||
try:
|
||||
from PyInstaller.utils.hooks import qt
|
||||
|
||||
# 1. Find the real plugin path
|
||||
qt5_dir = os.path.dirname(PyQt5.__file__)
|
||||
possible_plugin_paths = [
|
||||
os.path.join(qt5_dir, 'Qt', 'plugins'),
|
||||
os.path.join(qt5_dir, 'plugins'),
|
||||
]
|
||||
real_plugin_path = None
|
||||
for p in possible_plugin_paths:
|
||||
if os.path.exists(p):
|
||||
real_plugin_path = p
|
||||
break
|
||||
|
||||
# 2. Override the failing function
|
||||
if real_plugin_path:
|
||||
print(">>> FORCING QT PLUGINS PATH: {}".format(real_plugin_path))
|
||||
|
||||
# Save original just in case, though we ignore it
|
||||
original_qt_plugins_dir = qt.qt_plugins_dir
|
||||
|
||||
def new_qt_plugins_dir(namespace=None):
|
||||
return real_plugin_path
|
||||
|
||||
qt.qt_plugins_dir = new_qt_plugins_dir
|
||||
|
||||
# Also set env var as backup
|
||||
os.environ['QT_PLUGIN_PATH'] = real_plugin_path
|
||||
else:
|
||||
print(">>> ERROR: Could not find PyQt5 plugins to monkeypatch!")
|
||||
|
||||
except ImportError:
|
||||
print(">>> WARNING: Could not import PyInstaller.utils.hooks.qt for patching")
|
||||
except Exception as e:
|
||||
print(">>> WARNING: Monkeypatch failed: {}".format(e))
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
# Bundle tools to sql_tools subfolder
|
||||
datas=[
|
||||
('icon.png', '.'),
|
||||
('sql_tools/pg_dump.exe', 'sql_tools'),
|
||||
('sql_tools/mysqldump.exe', 'sql_tools')
|
||||
],
|
||||
hiddenimports=['PyQt5', 'pymysql', 'psycopg2', 'apscheduler'],
|
||||
hookspath=[],
|
||||
runtime_hooks=['rthook_qt_fix.py'],
|
||||
# Context7 Optimization: Exclude unused modules to reduce size
|
||||
excludes=['FixTk', 'tcl', 'tk', '_tkinter', 'tkinter', 'Tkinter', 'matplotlib', 'numpy', 'scipy', 'pandas', 'PIL.ImageTk'],
|
||||
win_no_prefer_redirects=False,
|
||||
cipher=block_cipher,
|
||||
)
|
||||
|
||||
# --- MANUAL BUNDLING OF PLATFORMS (Fix "platform plugin windows not found") ---
|
||||
# Re-locate the plugins directory (redundant but safe)
|
||||
qt5_dir = os.path.dirname(PyQt5.__file__)
|
||||
plugin_sources = [
|
||||
os.path.join(qt5_dir, 'Qt', 'plugins', 'platforms'),
|
||||
os.path.join(qt5_dir, 'plugins', 'platforms'),
|
||||
]
|
||||
found_plugin = None
|
||||
for p in plugin_sources:
|
||||
if os.path.exists(p):
|
||||
found_plugin = p
|
||||
break
|
||||
|
||||
if found_plugin:
|
||||
print(">>> MANUALLY BUNDLING PLATFORMS FROM: {}".format(found_plugin))
|
||||
a.datas += Tree(found_plugin, prefix='platforms') # Bundle as root/platforms/
|
||||
|
||||
# Also bundle as root/qt/plugins/platforms just to match common fallback paths
|
||||
# a.datas += Tree(found_plugin, prefix='qt/plugins/platforms')
|
||||
else:
|
||||
print(">>> ERROR: Could not find 'platforms' folder for bundling!")
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
# ONEFILE Mode: Everything bundled into single EXE
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='ProBackup',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=False,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
icon='icon.ico',
|
||||
)
|
||||
BIN
PyQt5-5.5-gpl-Py3.4-Qt5.5.0-x32.exe
Normal file
BIN
PyQt5-5.5-gpl-Py3.4-Qt5.5.0-x32.exe
Normal file
Binary file not shown.
679
backup_manager.py
Normal file
679
backup_manager.py
Normal file
@@ -0,0 +1,679 @@
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import zipfile
|
||||
import time
|
||||
import datetime
|
||||
import subprocess
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
# Try importing DB libraries, but don't fail if they aren't installed yet
|
||||
# (UI might just disable those options or show error on run)
|
||||
try:
|
||||
import pymysql
|
||||
except ImportError:
|
||||
pymysql = None
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
psycopg2 = None
|
||||
|
||||
|
||||
def get_mysql_info(host, port, user, password, skip_ssl=True):
|
||||
"""
|
||||
Connect to MySQL server and get version + database list.
|
||||
|
||||
Returns:
|
||||
tuple: (version_string, [database_list]) or (None, []) on error
|
||||
"""
|
||||
if pymysql is None:
|
||||
return None, [], "pymysql library not installed"
|
||||
|
||||
try:
|
||||
# Build connection kwargs
|
||||
conn_kwargs = {
|
||||
'host': host,
|
||||
'port': int(port),
|
||||
'user': user,
|
||||
'password': password,
|
||||
'connect_timeout': 10,
|
||||
}
|
||||
|
||||
# Add SSL settings
|
||||
if skip_ssl:
|
||||
conn_kwargs['ssl'] = None
|
||||
conn_kwargs['ssl_disabled'] = True
|
||||
|
||||
conn = pymysql.connect(**conn_kwargs)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get version
|
||||
cursor.execute("SELECT VERSION()")
|
||||
version = cursor.fetchone()[0]
|
||||
|
||||
# Get database list
|
||||
cursor.execute("SHOW DATABASES")
|
||||
databases = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# Filter out system databases
|
||||
system_dbs = ['information_schema', 'performance_schema', 'mysql', 'sys']
|
||||
user_dbs = [db for db in databases if db.lower() not in system_dbs]
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return version, user_dbs, None
|
||||
except Exception as e:
|
||||
return None, [], str(e)
|
||||
|
||||
|
||||
def get_postgres_info(host, port, user, password):
|
||||
"""
|
||||
Connect to PostgreSQL server and get version + database list.
|
||||
|
||||
Returns:
|
||||
tuple: (version_string, [database_list], error_msg) or (None, [], error) on error
|
||||
"""
|
||||
if psycopg2 is None:
|
||||
return None, [], "psycopg2 library not installed"
|
||||
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=host,
|
||||
port=int(port),
|
||||
user=user,
|
||||
password=password,
|
||||
dbname='postgres', # Connect to default db first
|
||||
connect_timeout=10
|
||||
)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get version
|
||||
cursor.execute("SELECT version()")
|
||||
full_version = cursor.fetchone()[0]
|
||||
# Extract just version number (e.g., "PostgreSQL 15.2")
|
||||
version = full_version.split(',')[0] if ',' in full_version else full_version.split(' on ')[0]
|
||||
|
||||
# Get database list
|
||||
cursor.execute("SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname")
|
||||
databases = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# Filter out system databases
|
||||
system_dbs = ['postgres']
|
||||
user_dbs = [db for db in databases if db.lower() not in system_dbs]
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return version, user_dbs, None
|
||||
except Exception as e:
|
||||
return None, [], str(e)
|
||||
|
||||
|
||||
def format_size(size_bytes):
|
||||
"""Format bytes to human readable size."""
|
||||
if size_bytes < 1024:
|
||||
return "{} B".format(size_bytes)
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return "{:.1f} KB".format(size_bytes / 1024)
|
||||
elif size_bytes < 1024 * 1024 * 1024:
|
||||
return "{:.2f} MB".format(size_bytes / (1024 * 1024))
|
||||
else:
|
||||
return "{:.2f} GB".format(size_bytes / (1024 * 1024 * 1024))
|
||||
|
||||
|
||||
def find_sql_tool(tool_name, server_type=None, server_version=None):
|
||||
"""
|
||||
Find SQL tool executable with version support.
|
||||
|
||||
Priority:
|
||||
1. Exact match in sql_tools/{type}/{version}/tool.exe
|
||||
2. Best match (same major version) in sql_tools/{type}/
|
||||
3. Root sql_tools/tool.exe (legacy/generic)
|
||||
4. Bundled sql_tools
|
||||
|
||||
Args:
|
||||
tool_name: e.g., 'mysqldump.exe'
|
||||
server_type: 'mysql', 'mariadb', 'postgres'
|
||||
server_version: e.g., '10.6.12', '8.0.32'
|
||||
"""
|
||||
# Define tool variants
|
||||
tool_variants = {
|
||||
'mysqldump.exe': ['mysqldump.exe', 'mariadb-dump.exe', 'mariadump.exe', 'mysql_dump.exe'],
|
||||
'pg_dump.exe': ['pg_dump.exe', 'pgdump.exe'],
|
||||
'pg_dumpall.exe': ['pg_dumpall.exe', 'pgdumpall.exe']
|
||||
}
|
||||
|
||||
names_to_search = tool_variants.get(tool_name, [tool_name])
|
||||
|
||||
# Base directories
|
||||
if getattr(sys, 'frozen', False):
|
||||
base_dir = os.path.dirname(sys.executable)
|
||||
bundle_dir = getattr(sys, '_MEIPASS', os.path.dirname(sys.executable))
|
||||
else:
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
bundle_dir = base_dir
|
||||
|
||||
search_roots = [
|
||||
os.path.join(base_dir, 'sql_tools'),
|
||||
os.path.join(bundle_dir, 'sql_tools')
|
||||
]
|
||||
|
||||
# Helper to check file existence
|
||||
def check_exists(path):
|
||||
for name in names_to_search:
|
||||
full_path = os.path.join(path, name)
|
||||
if os.path.exists(full_path):
|
||||
return full_path
|
||||
return None
|
||||
|
||||
# 1. Version Specific Search
|
||||
if server_type and server_version:
|
||||
import re
|
||||
# Extract major.minor (e.g. 8.0 from 8.0.33)
|
||||
v_match = re.match(r'(\d+\.\d+)', str(server_version))
|
||||
if v_match:
|
||||
short_ver = v_match.group(1)
|
||||
|
||||
for root in search_roots:
|
||||
# Try exact version folder: sql_tools/mysql/8.0/
|
||||
version_path = os.path.join(root, server_type, short_ver)
|
||||
found = check_exists(version_path)
|
||||
if found: return found
|
||||
|
||||
# Try closest match (simplified: just list dirs and find same major)
|
||||
# e.g. looking for 8.0, but only 8.4 is there -> might be okay for upgrade?
|
||||
# Better: exact match first. If not found, look for newer compatible?
|
||||
# For now let's stick to simple fallbacks if specific version not found.
|
||||
|
||||
# 2. Root Search (Legacy/Fallback)
|
||||
for root in search_roots:
|
||||
found = check_exists(root)
|
||||
if found: return found
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class BackupStrategy(ABC):
|
||||
def __init__(self, update_signal=None, log_signal=None):
|
||||
self.update_signal = update_signal
|
||||
self.log_signal = log_signal
|
||||
self._cancel_flag = False
|
||||
|
||||
def log(self, message):
|
||||
if self.log_signal:
|
||||
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
||||
self.log_signal.emit("[{}] {}".format(timestamp, message))
|
||||
else:
|
||||
print(message)
|
||||
|
||||
def progress(self, value):
|
||||
if self.update_signal:
|
||||
self.update_signal.emit(value)
|
||||
|
||||
def cancel(self):
|
||||
self._cancel_flag = True
|
||||
self.log("Pembatalan (diminta/tertunda)...")
|
||||
|
||||
def is_cancelled(self):
|
||||
return self._cancel_flag
|
||||
|
||||
def cleanup_old_backups(self, dest, retention_days):
|
||||
if retention_days <= 0:
|
||||
return
|
||||
|
||||
self.log("Menjalankan pembersihan (Retensi: {} hari)...".format(retention_days))
|
||||
now = time.time()
|
||||
cutoff = now - (retention_days * 86400)
|
||||
|
||||
if not os.path.exists(dest):
|
||||
return
|
||||
|
||||
count = 0
|
||||
try:
|
||||
for filename in os.listdir(dest):
|
||||
filepath = os.path.join(dest, filename)
|
||||
if os.path.isfile(filepath):
|
||||
# Check modified time or created time
|
||||
file_time = os.path.getmtime(filepath)
|
||||
if file_time < cutoff:
|
||||
try:
|
||||
os.remove(filepath)
|
||||
self.log("Menghapus backup lama: {}".format(filename))
|
||||
count += 1
|
||||
except Exception as e:
|
||||
self.log("Gagal menghapus {}: {}".format(filename, e))
|
||||
except Exception as e:
|
||||
self.log("Error saat pembersihan: {}".format(e))
|
||||
|
||||
if count > 0:
|
||||
self.log("Pembersihan selesai. Menghapus {} file lama.".format(count))
|
||||
else:
|
||||
self.log("Pembersihan selesai. Tidak ada file lama ditemukan.")
|
||||
|
||||
@abstractmethod
|
||||
def run_backup(self, config):
|
||||
pass
|
||||
|
||||
|
||||
class FileBackup(BackupStrategy):
|
||||
def run_backup(self, config):
|
||||
source = config.get('source')
|
||||
dest = config.get('dest')
|
||||
archive = config.get('archive', False)
|
||||
|
||||
if not os.path.exists(source):
|
||||
raise FileNotFoundError("Sumber tidak ditemukan: {}".format(source))
|
||||
|
||||
if not os.path.exists(dest):
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
|
||||
self.log("Memulai Backup File: {} -> {}".format(source, dest))
|
||||
|
||||
name = os.path.basename(source.rstrip(os.sep))
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
if archive:
|
||||
# ZIP MODE
|
||||
zip_filename = "{}_{}.zip".format(name, timestamp)
|
||||
zip_path = os.path.join(dest, zip_filename)
|
||||
self.log("Membuat arsip: {}".format(zip_path))
|
||||
|
||||
# Count total files for progress
|
||||
total_files = 0
|
||||
for root, dirs, files in os.walk(source):
|
||||
total_files += len(files)
|
||||
|
||||
processed_files = 0
|
||||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
if os.path.isfile(source):
|
||||
zf.write(source, os.path.basename(source))
|
||||
self.progress(100)
|
||||
else:
|
||||
for root, dirs, files in os.walk(source):
|
||||
if self.is_cancelled():
|
||||
self.log("Backup dibatalkan.")
|
||||
return
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
arcname = os.path.relpath(file_path, os.path.dirname(source))
|
||||
zf.write(file_path, arcname)
|
||||
|
||||
processed_files += 1
|
||||
if total_files > 0:
|
||||
self.progress(int(processed_files / total_files * 100))
|
||||
|
||||
self.log("Arsip berhasil dibuat.")
|
||||
|
||||
else:
|
||||
# COPY MODE
|
||||
dest_path = os.path.join(dest, "{}_{}".format(name, timestamp))
|
||||
self.log("Menyalin ke: {}".format(dest_path))
|
||||
|
||||
if os.path.isfile(source):
|
||||
shutil.copy2(source, dest_path)
|
||||
self.progress(100)
|
||||
else:
|
||||
# Initial implementation: simple copytree
|
||||
# For better progress, we'd need to walk and copy manually like Zip
|
||||
# But copytree is robust. Let's do a manual walk for progress.
|
||||
total_files = 0
|
||||
for root, dirs, files in os.walk(source):
|
||||
total_files += len(files)
|
||||
|
||||
processed_files = 0
|
||||
|
||||
# Re-walk to copy
|
||||
# We need to replicate directory structure first
|
||||
# Or just use shutil.copytree but we lose granular progress
|
||||
# Let's implement manual walk for progress
|
||||
|
||||
|
||||
for root, dirs, files in os.walk(source):
|
||||
if self.is_cancelled():
|
||||
self.log("Backup dibatalkan.")
|
||||
return
|
||||
|
||||
# Compute destination directory
|
||||
rel_path = os.path.relpath(root, source)
|
||||
current_dest_dir = os.path.join(dest_path, rel_path)
|
||||
os.makedirs(current_dest_dir, exist_ok=True)
|
||||
|
||||
for file in files:
|
||||
src_file = os.path.join(root, file)
|
||||
dst_file = os.path.join(current_dest_dir, file)
|
||||
shutil.copy2(src_file, dst_file)
|
||||
|
||||
processed_files += 1
|
||||
if total_files > 0:
|
||||
self.progress(int(processed_files / total_files * 100))
|
||||
|
||||
self.log("Penyalinan berhasil diselesaikan.")
|
||||
|
||||
# Cleanup
|
||||
self.cleanup_old_backups(dest, config.get('retention_days', 0))
|
||||
self.progress(100)
|
||||
|
||||
|
||||
class MySQLBackup(BackupStrategy):
|
||||
def run_backup(self, config):
|
||||
host = config.get('host', 'localhost')
|
||||
port = str(config.get('port', 3306))
|
||||
user = config.get('user', 'root')
|
||||
password = config.get('password', '')
|
||||
database = config.get('database')
|
||||
dest = config.get('dest')
|
||||
all_databases = config.get('all_databases', False)
|
||||
skip_ssl = config.get('skip_ssl', True)
|
||||
|
||||
# Detect Server Version
|
||||
self.log("Mendeteksi versi server MySQL di {}:{}...".format(host, port))
|
||||
version, _, error = get_mysql_info(host, port, user, password, skip_ssl)
|
||||
|
||||
server_type = 'mysql' # Default
|
||||
server_version = None
|
||||
|
||||
if error:
|
||||
self.log("Warning: Gagal mendeteksi versi server ({}). Menggunakan tool default.".format(error))
|
||||
else:
|
||||
self.log("Versi server terdeteksi: {}".format(version))
|
||||
server_version = version # Pass full string, find_sql_tool handles parsing
|
||||
# Check if MariaDB
|
||||
if 'MariaDB' in version or 'mariadb' in version.lower():
|
||||
server_type = 'mariadb'
|
||||
|
||||
# Find mysqldump: Priority sql_tools (custom) > sql_tools (bundled)
|
||||
mysqldump_path = find_sql_tool('mysqldump.exe', server_type, server_version)
|
||||
|
||||
if not mysqldump_path:
|
||||
raise FileNotFoundError(
|
||||
"mysqldump.exe tidak ditemukan!\n"
|
||||
"Letakkan di folder 'sql_tools' di samping ProBackup.exe\n"
|
||||
"atau pastikan sql_tools ada di dalam bundle.")
|
||||
|
||||
if not os.path.exists(dest):
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
|
||||
# Log tool location for debugging
|
||||
self.log("Menggunakan mysqldump: {}".format(mysqldump_path))
|
||||
|
||||
# Determine backup mode
|
||||
if all_databases:
|
||||
self.log("Menghubungkan ke MySQL: {}:{} (SEMUA DATABASE)".format(host, port))
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
dump_file_path = os.path.join(dest, "all_databases_{}.sql".format(timestamp))
|
||||
else:
|
||||
self.log("Menghubungkan ke MySQL: {}:{} / {}".format(host, port, database))
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
dump_file_path = os.path.join(dest, "{}_{}.sql".format(database, timestamp))
|
||||
|
||||
# Get skip_ssl setting (default True for backward compatibility)
|
||||
skip_ssl = config.get('skip_ssl', True)
|
||||
|
||||
# Command Construction - build properly from start
|
||||
cmd = [mysqldump_path, '-h', host, '-P', port, '-u', user]
|
||||
|
||||
# Add password if provided
|
||||
if password:
|
||||
cmd.append('--password={}'.format(password))
|
||||
|
||||
# Add skip-ssl if enabled - detect tool type for correct option
|
||||
if skip_ssl:
|
||||
tool_basename = os.path.basename(mysqldump_path).lower()
|
||||
if 'mariadb' in tool_basename or 'mariadump' in tool_basename:
|
||||
# MariaDB uses --ssl=0 or --skip-ssl depending on version
|
||||
cmd.append('--ssl=0')
|
||||
self.log("Menggunakan opsi SSL MariaDB: --ssl=0")
|
||||
else:
|
||||
# MySQL uses --skip-ssl
|
||||
cmd.append('--skip-ssl')
|
||||
self.log("Menggunakan opsi SSL MySQL: --skip-ssl")
|
||||
|
||||
# Add remaining options
|
||||
cmd.extend([
|
||||
'--quick',
|
||||
'--lock-tables=false',
|
||||
'--result-file=' + dump_file_path,
|
||||
])
|
||||
|
||||
# Add database or all-databases flag
|
||||
if all_databases:
|
||||
cmd.append('--all-databases')
|
||||
self.log("Mode: Backup semua database (--all-databases)")
|
||||
else:
|
||||
cmd.append(database)
|
||||
|
||||
self.log("Menjalankan mysqldump ke: {}".format(dump_file_path))
|
||||
self.log("Mohon tunggu...")
|
||||
|
||||
try:
|
||||
# Hide password warning on stderr
|
||||
# Using Popen to handle cancellation
|
||||
# Hide console window on Windows
|
||||
startupinfo = None
|
||||
if sys.platform == 'win32':
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
startupinfo.wShowWindow = subprocess.SW_HIDE
|
||||
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, startupinfo=startupinfo)
|
||||
|
||||
# Monitor file size progress
|
||||
last_log_time = time.time()
|
||||
log_interval = 2 # Log every 2 seconds
|
||||
|
||||
while True:
|
||||
retcode = process.poll()
|
||||
if retcode is not None:
|
||||
break
|
||||
|
||||
if self.is_cancelled():
|
||||
process.terminate()
|
||||
self.log("Backup dibatalkan.")
|
||||
if os.path.exists(dump_file_path):
|
||||
os.remove(dump_file_path)
|
||||
return
|
||||
|
||||
# Log file size progress
|
||||
current_time = time.time()
|
||||
if current_time - last_log_time >= log_interval:
|
||||
if os.path.exists(dump_file_path):
|
||||
file_size = os.path.getsize(dump_file_path)
|
||||
self.log("Progress: {} written...".format(format_size(file_size)))
|
||||
last_log_time = current_time
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
if retcode != 0:
|
||||
# mysqldump writes "Using a password..." to stderr as warning, not error.
|
||||
# We need to distinguish real errors.
|
||||
# Usually exit code 0 is success.
|
||||
raise Exception("mysqldump error (Code {}): {}".format(retcode, stderr))
|
||||
|
||||
# Log final size
|
||||
if os.path.exists(dump_file_path):
|
||||
final_size = os.path.getsize(dump_file_path)
|
||||
self.log("Dump selesai. Total: {}".format(format_size(final_size)))
|
||||
else:
|
||||
self.log("Dump selesai.")
|
||||
self.progress(80)
|
||||
|
||||
# Compress if requested
|
||||
if config.get('compress', False):
|
||||
zip_path = dump_file_path.replace('.sql', '.zip')
|
||||
self.log("Mengompres ke: {}".format(zip_path))
|
||||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.write(dump_file_path, os.path.basename(dump_file_path))
|
||||
os.remove(dump_file_path)
|
||||
self.log("Kompresi selesai.")
|
||||
|
||||
self.log("Backup MySQL berhasil diselesaikan.")
|
||||
self.progress(100)
|
||||
self.cleanup_old_backups(dest, config.get('retention_days', 0))
|
||||
|
||||
except Exception as e:
|
||||
self.log("Gagal melakukan backup: {}".format(e))
|
||||
raise e
|
||||
|
||||
|
||||
class PostgresBackup(BackupStrategy):
|
||||
def run_backup(self, config):
|
||||
host = config.get('host', 'localhost')
|
||||
port = str(config.get('port', 5432))
|
||||
user = config.get('user', 'postgres')
|
||||
password = config.get('password', '')
|
||||
database = config.get('database')
|
||||
dest = config.get('dest')
|
||||
all_databases = config.get('all_databases', False)
|
||||
|
||||
if not os.path.exists(dest):
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
|
||||
# Detect Server Version
|
||||
self.log("Mendeteksi versi server Postgres di {}:{}...".format(host, port))
|
||||
version, _, error = get_postgres_info(host, port, user, password)
|
||||
|
||||
server_version = None
|
||||
if error:
|
||||
self.log("Warning: Gagal mendeteksi versi server ({}). Menggunakan tool default.".format(error))
|
||||
else:
|
||||
self.log("Versi server terdeteksi: {}".format(version))
|
||||
server_version = version
|
||||
|
||||
# Determine which tool to use based on all_databases option
|
||||
if all_databases:
|
||||
# Use pg_dumpall for all databases
|
||||
pg_tool_path = find_sql_tool('pg_dumpall.exe', 'postgres', server_version)
|
||||
if not pg_tool_path:
|
||||
raise FileNotFoundError(
|
||||
"pg_dumpall.exe tidak ditemukan!\n"
|
||||
"Letakkan di folder 'sql_tools' di samping ProBackup.exe\n"
|
||||
"atau pastikan sql_tools ada di dalam bundle.")
|
||||
|
||||
self.log("Menggunakan pg_dumpall: {}".format(pg_tool_path))
|
||||
self.log("Menghubungkan ke Postgres: {}:{} (SEMUA DATABASE)".format(host, port))
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
dump_file_path = os.path.join(dest, "all_databases_{}.sql".format(timestamp))
|
||||
else:
|
||||
# Use pg_dump for single database
|
||||
pg_tool_path = find_sql_tool('pg_dump.exe', 'postgres', server_version)
|
||||
if not pg_tool_path:
|
||||
raise FileNotFoundError(
|
||||
"pg_dump.exe tidak ditemukan!\n"
|
||||
"Letakkan di folder 'sql_tools' di samping ProBackup.exe\n"
|
||||
"atau pastikan sql_tools ada di dalam bundle.")
|
||||
|
||||
self.log("Menggunakan pg_dump: {}".format(pg_tool_path))
|
||||
self.log("Menghubungkan ke Postgres: {}:{} / {}".format(host, port, database))
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
dump_file_path = os.path.join(dest, "{}_{}.sql".format(database, timestamp))
|
||||
|
||||
# Prepare Environment for Password
|
||||
env = os.environ.copy()
|
||||
env['PGPASSWORD'] = password
|
||||
|
||||
# Command Construction
|
||||
if all_databases:
|
||||
# pg_dumpall command
|
||||
cmd = [
|
||||
pg_tool_path,
|
||||
'-h', host,
|
||||
'-p', port,
|
||||
'-U', user,
|
||||
'--no-owner',
|
||||
'--no-acl',
|
||||
'-f', dump_file_path,
|
||||
]
|
||||
self.log("Mode: Backup semua database (pg_dumpall)")
|
||||
else:
|
||||
# pg_dump command for single database
|
||||
cmd = [
|
||||
pg_tool_path,
|
||||
'-h', host,
|
||||
'-p', port,
|
||||
'-U', user,
|
||||
'--no-owner',
|
||||
'--no-acl',
|
||||
'-F', 'p', # Plain text format
|
||||
'-f', dump_file_path,
|
||||
database
|
||||
]
|
||||
|
||||
self.log("Menjalankan backup ke: {}".format(dump_file_path))
|
||||
self.log("Mohon tunggu, proses ini mungkin memakan waktu...")
|
||||
|
||||
try:
|
||||
# We use subprocess.run. It blocks, which is okay in a WorkerThread.
|
||||
# To support cancellation, we ideally should use Popen and poll, but for simplicity/robustness:
|
||||
# If user cancels in UI, the thread might continue until this returns?
|
||||
# Or we can use Popen.
|
||||
|
||||
# Hide console window on Windows
|
||||
startupinfo = None
|
||||
if sys.platform == 'win32':
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
startupinfo.wShowWindow = subprocess.SW_HIDE
|
||||
|
||||
process = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, startupinfo=startupinfo)
|
||||
|
||||
# Monitor file size progress
|
||||
last_log_time = time.time()
|
||||
log_interval = 2 # Log every 2 seconds
|
||||
|
||||
while True:
|
||||
retcode = process.poll()
|
||||
if retcode is not None:
|
||||
break
|
||||
|
||||
if self.is_cancelled():
|
||||
process.terminate()
|
||||
self.log("Backup dibatalkan (Process Terminated).")
|
||||
if os.path.exists(dump_file_path):
|
||||
os.remove(dump_file_path) # Cleanup partial
|
||||
return
|
||||
|
||||
# Log file size progress
|
||||
current_time = time.time()
|
||||
if current_time - last_log_time >= log_interval:
|
||||
if os.path.exists(dump_file_path):
|
||||
file_size = os.path.getsize(dump_file_path)
|
||||
self.log("Progress: {} written...".format(format_size(file_size)))
|
||||
last_log_time = current_time
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
stdout, stderr = process.communicate()
|
||||
|
||||
if retcode != 0:
|
||||
raise Exception("pg_dump error (Code {}): {}".format(retcode, stderr))
|
||||
|
||||
# Log final size
|
||||
if os.path.exists(dump_file_path):
|
||||
final_size = os.path.getsize(dump_file_path)
|
||||
self.log("Dump selesai. Total: {}".format(format_size(final_size)))
|
||||
else:
|
||||
self.log("Dump selesai.")
|
||||
self.progress(80)
|
||||
|
||||
# Compress if requested
|
||||
if config.get('compress', False):
|
||||
zip_path = dump_file_path.replace('.sql', '.zip')
|
||||
self.log("Mengompres ke: {}".format(zip_path))
|
||||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.write(dump_file_path, os.path.basename(dump_file_path))
|
||||
os.remove(dump_file_path)
|
||||
self.log("Kompresi selesai.")
|
||||
|
||||
self.log("Backup PostgreSQL berhasil diselesaikan.")
|
||||
self.progress(100)
|
||||
self.cleanup_old_backups(dest, config.get('retention_days', 0))
|
||||
|
||||
except Exception as e:
|
||||
self.log("Gagal melakukan backup: {}".format(e))
|
||||
raise e
|
||||
4
build_xp.bat
Normal file
4
build_xp.bat
Normal file
@@ -0,0 +1,4 @@
|
||||
pyinstaller --noconfirm --distpath dist-winXP --workpath build-winXP --clean ProBackup.spec
|
||||
echo.
|
||||
echo Building Keygen...
|
||||
pyinstaller --noconfirm --distpath dist-winXP --workpath build-winXP --clean Keygen.spec
|
||||
119
config_manager.py
Normal file
119
config_manager.py
Normal file
@@ -0,0 +1,119 @@
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
import base64
|
||||
|
||||
CONFIG_FILE = "config.dat" # Obfuscated file
|
||||
|
||||
class ConfigManager:
|
||||
DEFAULT_JOB = {
|
||||
"id": "",
|
||||
"name": "New Backup Job",
|
||||
"type": 0, # 0: File, 1: MySQL, 2: Postgres
|
||||
"dest": "",
|
||||
# File
|
||||
"source": "",
|
||||
"zip": True,
|
||||
# DB
|
||||
"host": "localhost",
|
||||
"port": "",
|
||||
"user": "",
|
||||
"password": "",
|
||||
"database": "",
|
||||
"retention_days": 0 # 0 means forever
|
||||
}
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"jobs": [],
|
||||
"schedule_enabled": False,
|
||||
"schedule_interval": 60,
|
||||
"schedule_unit": "Menit"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_new_job():
|
||||
job = ConfigManager.DEFAULT_JOB.copy()
|
||||
job["id"] = str(uuid.uuid4())
|
||||
return job
|
||||
|
||||
@staticmethod
|
||||
def load_config():
|
||||
# Check for old unencrypted config.json and migrate
|
||||
if os.path.exists("config.json"):
|
||||
try:
|
||||
with open("config.json", 'r') as f:
|
||||
data = json.load(f)
|
||||
print("Migrasi config.json...")
|
||||
config = ConfigManager._migrate_old_config(data)
|
||||
ConfigManager.save_config(config)
|
||||
os.rename("config.json", "config.json.bak")
|
||||
return config
|
||||
except Exception as e:
|
||||
print("Error migrasi config: {}".format(e))
|
||||
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
return ConfigManager.DEFAULT_CONFIG.copy()
|
||||
|
||||
try:
|
||||
with open(CONFIG_FILE, 'rb') as f:
|
||||
encoded_data = f.read()
|
||||
|
||||
# Simple Base64 Decode
|
||||
decoded_data = base64.b64decode(encoded_data)
|
||||
data = json.loads(decoded_data.decode('utf-8'))
|
||||
|
||||
config = ConfigManager.DEFAULT_CONFIG.copy()
|
||||
config.update(data)
|
||||
return config
|
||||
except Exception as e:
|
||||
print("Error loading config: {}".format(e))
|
||||
return ConfigManager.DEFAULT_CONFIG.copy()
|
||||
|
||||
@staticmethod
|
||||
def _migrate_old_config(data):
|
||||
"""Migrate old config format to new format"""
|
||||
config = ConfigManager.DEFAULT_CONFIG.copy()
|
||||
|
||||
# Check if it's the very old single-job format
|
||||
if "backup_type" in data:
|
||||
job = ConfigManager.create_new_job()
|
||||
job["name"] = "Default Backup"
|
||||
job["type"] = data.get("backup_type", 0)
|
||||
job["dest"] = data.get("dest_path", "")
|
||||
job["source"] = data.get("file_source", "")
|
||||
job["zip"] = data.get("zip_enabled", True)
|
||||
|
||||
if job["type"] == 1: # MySQL
|
||||
job["host"] = data.get("mysql_host", "localhost")
|
||||
job["port"] = data.get("mysql_port", "3306")
|
||||
job["user"] = data.get("mysql_user", "root")
|
||||
job["password"] = data.get("mysql_pass", "")
|
||||
job["database"] = data.get("mysql_db", "")
|
||||
elif job["type"] == 2: # Postgres
|
||||
job["host"] = data.get("pg_host", "localhost")
|
||||
job["port"] = data.get("pg_port", "5432")
|
||||
job["user"] = data.get("pg_user", "postgres")
|
||||
job["password"] = data.get("pg_pass", "")
|
||||
job["database"] = data.get("pg_db", "")
|
||||
|
||||
config["jobs"] = [job]
|
||||
config["schedule_enabled"] = data.get("schedule_enabled", False)
|
||||
config["schedule_interval"] = data.get("schedule_interval", 60)
|
||||
config["schedule_unit"] = data.get("schedule_unit", "Menit")
|
||||
else:
|
||||
# Already in new format
|
||||
config.update(data)
|
||||
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def save_config(config):
|
||||
try:
|
||||
json_data = json.dumps(config, indent=4)
|
||||
# Simple Base64 Encode
|
||||
encoded_data = base64.b64encode(json_data.encode('utf-8'))
|
||||
|
||||
with open(CONFIG_FILE, 'wb') as f:
|
||||
f.write(encoded_data)
|
||||
except Exception as e:
|
||||
print("Error saving config: {}".format(e))
|
||||
BIN
dist-winXP/ProBackup.exe
Normal file
BIN
dist-winXP/ProBackup.exe
Normal file
Binary file not shown.
118
extract_tools.py
Normal file
118
extract_tools.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import os
|
||||
import zipfile
|
||||
import shutil
|
||||
import re
|
||||
|
||||
# Source and Destination
|
||||
SOURCE_DIR = 'sql_tools_zip'
|
||||
DEST_DIR = 'sql_tools'
|
||||
|
||||
# Mapping structure: (Zip Pattern, Target Folder Structure, Executables to Extract)
|
||||
# Target Folder Structure: e.g. "mysql/{version}" where {version} is extracted from filename
|
||||
RULES = [
|
||||
{
|
||||
'pattern': r'mysql-(\d+\.\d+)\.\d+-winx64\.zip',
|
||||
'type': 'mysql',
|
||||
'bin_path': 'bin/', # usually inside root folder of zip
|
||||
'executables': ['mysqldump.exe']
|
||||
},
|
||||
{
|
||||
'pattern': r'mariadb-(\d+\.\d+)\.\d+-winx64.*\.zip',
|
||||
'type': 'mariadb',
|
||||
'bin_path': 'bin/',
|
||||
'executables': ['mariadb-dump.exe', 'mysqldump.exe'] # Try both
|
||||
},
|
||||
{
|
||||
'pattern': r'postgresql-(\d+\.\d+).*windows-x64-binaries\.zip',
|
||||
'type': 'postgres',
|
||||
'bin_path': 'pgsql/bin/', # pg binaries zip usually has pgsql/bin
|
||||
'executables': ['pg_dump.exe', 'pg_dumpall.exe', 'libpq.dll', 'libiconv-2.dll', 'libintl-8.dll', 'libssl-1_1-x64.dll', 'libcrypto-1_1-x64.dll'] # DLLs might be needed!
|
||||
# Modern PG zip structure might differ, checking pgsql/bin is standard for binaries zip
|
||||
},
|
||||
{
|
||||
# Special handler for PG 18 if pattern differs or standard regex fails
|
||||
'pattern': r'postgresql-(\d+)\.\d+.*windows-x64-binaries\.zip',
|
||||
'type': 'postgres',
|
||||
'bin_path': 'pgsql/bin/',
|
||||
'executables': ['pg_dump.exe', 'pg_dumpall.exe']
|
||||
}
|
||||
]
|
||||
|
||||
def ensure_dir(path):
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
def extract_tools():
|
||||
if not os.path.exists(SOURCE_DIR):
|
||||
print("Source directory '{}' not found.".format(SOURCE_DIR))
|
||||
return
|
||||
|
||||
ensure_dir(DEST_DIR)
|
||||
|
||||
files = [f for f in os.listdir(SOURCE_DIR) if f.lower().endswith('.zip')]
|
||||
|
||||
print("Found {} zip files.".format(len(files)))
|
||||
|
||||
for filename in files:
|
||||
# Match rule
|
||||
matched_rule = None
|
||||
version = None
|
||||
|
||||
for rule in RULES:
|
||||
match = re.search(rule['pattern'], filename)
|
||||
if match:
|
||||
matched_rule = rule
|
||||
version = match.group(1)
|
||||
break
|
||||
|
||||
if not matched_rule:
|
||||
print("Skipping unknown file: {}".format(filename))
|
||||
continue
|
||||
|
||||
db_type = matched_rule['type']
|
||||
target_dir = os.path.join(DEST_DIR, db_type, version)
|
||||
|
||||
print("Processing {} -> {}...".format(filename, target_dir))
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(os.path.join(SOURCE_DIR, filename), 'r') as zf:
|
||||
# Find the internal path prefix (usually top folder)
|
||||
# We assume binaries are in {top_folder}/{bin_path} or just {bin_path}
|
||||
# Let's search for the executable to determine path
|
||||
|
||||
for exe in matched_rule['executables']:
|
||||
# find file in zip
|
||||
target_member = None
|
||||
for name in zf.namelist():
|
||||
if name.endswith("/{}".format(exe)) or name == exe:
|
||||
# Verify if it looks like a bin folder
|
||||
if matched_rule['bin_path'].strip('/') in name:
|
||||
target_member = name
|
||||
break
|
||||
|
||||
if target_member:
|
||||
ensure_dir(target_dir)
|
||||
source = zf.open(target_member)
|
||||
target_file_path = os.path.join(target_dir, exe)
|
||||
with open(target_file_path, "wb") as target:
|
||||
shutil.copyfileobj(source, target)
|
||||
print(" Extracted: {}".format(exe))
|
||||
|
||||
# For Postgres, we might need DLLs.
|
||||
# Simple logic: if 'postgres' in db_type, try to grab standard DLLs in same dir as exe
|
||||
if db_type == 'postgres':
|
||||
parent = os.path.dirname(target_member)
|
||||
for item in zf.namelist():
|
||||
if os.path.dirname(item) == parent and item.lower().endswith('.dll'):
|
||||
dll_name = os.path.basename(item)
|
||||
with open(os.path.join(target_dir, dll_name), "wb") as dll_out:
|
||||
shutil.copyfileobj(zf.open(item), dll_out)
|
||||
|
||||
else:
|
||||
print(" Warning: {} not found in {}".format(exe, filename))
|
||||
|
||||
except Exception as e:
|
||||
print(" Error extracting {}: {}".format(filename, e))
|
||||
|
||||
if __name__ == "__main__":
|
||||
extract_tools()
|
||||
24
fix_qt_paths.bat
Normal file
24
fix_qt_paths.bat
Normal file
@@ -0,0 +1,24 @@
|
||||
@echo off
|
||||
echo ========================================================
|
||||
echo MEMPERBAIKI PATH PYQT5 (QT.CONF)
|
||||
echo ========================================================
|
||||
echo.
|
||||
echo Masalah: PyQt5 manual Anda mengira dia ada di C:/Qt/5.5.0
|
||||
echo Solusi: Kita buat file C:\Python34\qt.conf untuk mengarahkannya ke yang benar.
|
||||
echo.
|
||||
|
||||
set TARGET_CONF=C:\Python34\qt.conf
|
||||
|
||||
echo [Paths] > "%TARGET_CONF%"
|
||||
echo Prefix = C:/Python34/Lib/site-packages/PyQt5 >> "%TARGET_CONF%"
|
||||
echo Plugins = plugins >> "%TARGET_CONF%"
|
||||
|
||||
echo File terbuat: %TARGET_CONF%
|
||||
echo Isi file:
|
||||
type "%TARGET_CONF%"
|
||||
echo.
|
||||
echo ========================================================
|
||||
echo SELESAI!
|
||||
echo ========================================================
|
||||
echo Silakan jalankan 'build_xp.bat' lagi.
|
||||
pause
|
||||
118
install_deps_xp.bat
Normal file
118
install_deps_xp.bat
Normal file
@@ -0,0 +1,118 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
echo ========================================================
|
||||
echo OTOMATISASI INSTALASI DEPENDENSI PROBACKUP (XP TARGET)
|
||||
echo ========================================================
|
||||
echo.
|
||||
echo Pastikan Python 3.4 terinstall di C:\Python34
|
||||
echo.
|
||||
pause
|
||||
|
||||
set PY_PATH=C:\Python34
|
||||
set SITE_PACKAGES=%PY_PATH%\Lib\site-packages
|
||||
set PYTHON_EXE="%PY_PATH%\python.exe"
|
||||
|
||||
:: ----------------------------------------------------------
|
||||
:: 1. CLEANUP
|
||||
:: ----------------------------------------------------------
|
||||
echo.
|
||||
echo [1/8] Membersihkan instalasi PIP lama...
|
||||
|
||||
if exist "%SITE_PACKAGES%\pip" rd /s /q "%SITE_PACKAGES%\pip"
|
||||
if exist "%SITE_PACKAGES%\setuptools" rd /s /q "%SITE_PACKAGES%\setuptools"
|
||||
for /d %%G in ("%SITE_PACKAGES%\pip-*.dist-info") do rd /s /q "%%G"
|
||||
|
||||
:: ----------------------------------------------------------
|
||||
:: 2. ENSURE PIP
|
||||
:: ----------------------------------------------------------
|
||||
echo.
|
||||
echo [2/8] Menginstall Pip Bawaan...
|
||||
%PYTHON_EXE% -m ensurepip --default-pip
|
||||
if %errorlevel% neq 0 goto :Error
|
||||
|
||||
:: ----------------------------------------------------------
|
||||
:: 3. UPDATE PIP & BUILD TOOLS
|
||||
:: ----------------------------------------------------------
|
||||
echo.
|
||||
echo [3/8] Mencoba update Pip, Setuptools, Wheel...
|
||||
%PYTHON_EXE% -m pip install pip==19.1.1 --no-cache-dir
|
||||
:: Downgrade setuptools to 39.2.0 (Stable for Py3.4 era)
|
||||
%PYTHON_EXE% -m pip install setuptools==39.2.0 --no-cache-dir
|
||||
%PYTHON_EXE% -m pip install wheel --no-cache-dir
|
||||
|
||||
:: ----------------------------------------------------------
|
||||
:: 4. INSTALL CYCLES (MANUAL PEFILE)
|
||||
:: ----------------------------------------------------------
|
||||
echo.
|
||||
echo [4/8] Menginstall Pefile dan Altgraph...
|
||||
:: Gunakan versi jadul yang stabil untuk Py3.4
|
||||
%PYTHON_EXE% -m pip install altgraph==0.16.1 --no-cache-dir
|
||||
%PYTHON_EXE% -m pip install pefile==2017.11.5 --no-cache-dir
|
||||
%PYTHON_EXE% -m pip install future --no-cache-dir
|
||||
|
||||
:: ----------------------------------------------------------
|
||||
:: 5. INSTALL MAIN LIBS
|
||||
:: ----------------------------------------------------------
|
||||
echo.
|
||||
echo [5/8] Menginstall Library Lain...
|
||||
%PYTHON_EXE% -m pip install pymysql==0.9.3 --no-cache-dir
|
||||
%PYTHON_EXE% -m pip install psycopg2==2.7.7 --no-cache-dir
|
||||
%PYTHON_EXE% -m pip install apscheduler==3.6.3 --no-cache-dir
|
||||
|
||||
:: ----------------------------------------------------------
|
||||
:: 6. INSTALL PYINSTALLER
|
||||
:: ----------------------------------------------------------
|
||||
echo.
|
||||
echo [6/8] Menginstall PyInstaller...
|
||||
:: PyInstaller 3.4 is the robust choice for Py 3.4
|
||||
%PYTHON_EXE% -m pip install pyinstaller==3.4 --no-cache-dir
|
||||
|
||||
:: ----------------------------------------------------------
|
||||
:: 7. PYQT5 CHECK
|
||||
:: ----------------------------------------------------------
|
||||
echo.
|
||||
echo [7/8] Cek Instalasi PyQt5...
|
||||
%PYTHON_EXE% -c "import PyQt5" 2>nul
|
||||
if %errorlevel% equ 0 goto :PyQtFound
|
||||
|
||||
echo - Mencoba install PyQt5 via pip (backup plan)...
|
||||
%PYTHON_EXE% -m pip install PyQt5==5.6 --no-cache-dir
|
||||
if %errorlevel% equ 0 goto :PyQtFound
|
||||
|
||||
goto :PyQtMissing
|
||||
|
||||
:PyQtFound
|
||||
echo - PyQt5 terdeteksi. Oke.
|
||||
goto :Finalize
|
||||
|
||||
:PyQtMissing
|
||||
echo.
|
||||
echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
echo !!! ERROR: PYQT5 TIDAK DITEMUKAN / GAGAL INSTALL !!
|
||||
echo !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
echo.
|
||||
echo Anda harus menginstall MANUAL file ini:
|
||||
echo 1. PyQt5-5.5.1-gpl-Py3.4-Qt5.5.1-x32.exe
|
||||
echo 2. Download dari SourceForge.
|
||||
echo.
|
||||
echo Setelah install manual, jalankan script ini lagi dipersilakan (opsional).
|
||||
echo Atau langsung coba build_xp.bat jika yakin sudah install.
|
||||
echo.
|
||||
pause
|
||||
exit /b
|
||||
|
||||
:Error
|
||||
echo.
|
||||
echo !!! TERJADI ERROR FATAL !!!
|
||||
echo Cek pesan error di atas.
|
||||
pause
|
||||
exit /b
|
||||
|
||||
:Finalize
|
||||
echo.
|
||||
echo ========================================================
|
||||
echo SELESAI! SIAP BUILD!
|
||||
echo ========================================================
|
||||
echo Sekarang jalankan 'build_xp.bat'.
|
||||
pause
|
||||
104
keygen.py
Normal file
104
keygen.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
from license_manager import LicenseManager
|
||||
|
||||
class KeygenApp:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("ProBackup Key Generator (XP)")
|
||||
self.root.geometry("450x250")
|
||||
self.root.resizable(False, False)
|
||||
|
||||
# Center window
|
||||
self.center_window()
|
||||
|
||||
# Styles
|
||||
bg_color = "#f0f0f0"
|
||||
self.root.configure(bg=bg_color)
|
||||
|
||||
# Title
|
||||
tk.Label(root, text="ProBackup Activation Generator", font=("Arial", 14, "bold"), bg=bg_color).pack(pady=10)
|
||||
|
||||
# HWID Input
|
||||
input_frame = tk.Frame(root, bg=bg_color)
|
||||
input_frame.pack(pady=5, padx=20, fill="x")
|
||||
|
||||
tk.Label(input_frame, text="Machine ID (Paste here):", bg=bg_color).pack(anchor="w")
|
||||
self.hwid_entry = tk.Entry(input_frame, font=("Consolas", 10))
|
||||
self.hwid_entry.pack(fill="x", pady=2)
|
||||
# Add right click menu for paste (optional but helpful on older systems)
|
||||
self.create_context_menu(self.hwid_entry)
|
||||
|
||||
# Generate Button
|
||||
self.btn_generate = tk.Button(root, text="GENERATE KEY", command=self.generate_key,
|
||||
bg="#4CAF50", fg="white", font=("Arial", 10, "bold"), height=1)
|
||||
self.btn_generate.pack(pady=10)
|
||||
|
||||
# Output
|
||||
output_frame = tk.Frame(root, bg=bg_color)
|
||||
output_frame.pack(pady=5, padx=20, fill="x")
|
||||
|
||||
tk.Label(output_frame, text="Activation Key:", bg=bg_color).pack(anchor="w")
|
||||
self.key_entry = tk.Entry(output_frame, font=("Consolas", 10), state="readonly", fg="blue")
|
||||
self.key_entry.pack(fill="x", pady=2)
|
||||
self.create_context_menu(self.key_entry)
|
||||
|
||||
# Instructions
|
||||
tk.Label(root, text="Copy the Activation Key and send it to the user.",
|
||||
bg=bg_color, fg="#666666", font=("Arial", 8)).pack(side="bottom", pady=10)
|
||||
|
||||
def center_window(self):
|
||||
self.root.update_idletasks()
|
||||
width = self.root.winfo_width()
|
||||
height = self.root.winfo_height()
|
||||
x = (self.root.winfo_screenwidth() // 2) - (width // 2)
|
||||
y = (self.root.winfo_screenheight() // 2) - (height // 2)
|
||||
self.root.geometry('{}x{}+{}+{}'.format(width, height, x, y))
|
||||
|
||||
def create_context_menu(self, widget):
|
||||
menu = tk.Menu(self.root, tearoff=0)
|
||||
menu.add_command(label="Cut", command=lambda: widget.event_generate("<<Cut>>"))
|
||||
menu.add_command(label="Copy", command=lambda: widget.event_generate("<<Copy>>"))
|
||||
menu.add_command(label="Paste", command=lambda: widget.event_generate("<<Paste>>"))
|
||||
|
||||
def show_menu(event):
|
||||
menu.post(event.x_root, event.y_root)
|
||||
|
||||
widget.bind("<Button-3>", show_menu)
|
||||
|
||||
def generate_key(self):
|
||||
hwid = self.hwid_entry.get().strip()
|
||||
if not hwid:
|
||||
messagebox.showerror("Error", "Please enter a Machine ID first.")
|
||||
return
|
||||
|
||||
try:
|
||||
full_sig = LicenseManager.generate_signature(hwid)
|
||||
activation_key = full_sig[:32]
|
||||
|
||||
self.key_entry.config(state="normal")
|
||||
self.key_entry.delete(0, tk.END)
|
||||
self.key_entry.insert(0, activation_key)
|
||||
self.key_entry.config(state="readonly")
|
||||
|
||||
# Auto copy to clipboard
|
||||
self.root.clipboard_clear()
|
||||
self.root.clipboard_append(activation_key)
|
||||
messagebox.showinfo("Success", "Key generated and copied to clipboard!")
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", "Generasi gagal: {}".format(e))
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
# Try setting icon if available
|
||||
try:
|
||||
root.iconbitmap("icon.ico")
|
||||
except: pass
|
||||
|
||||
app = KeygenApp(root)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
144
license_manager.py
Normal file
144
license_manager.py
Normal file
@@ -0,0 +1,144 @@
|
||||
import hashlib
|
||||
import uuid
|
||||
import subprocess
|
||||
import platform
|
||||
import os
|
||||
import base64
|
||||
|
||||
# Simple Secret Salt - In a real app, obfuscate this or use asymmetric keys (RSA)
|
||||
# For this level, a symmetric secret is "good enough" for basic protection.
|
||||
SECRET_SALT = "ProBackup_Secret_Salt_2026_!@#"
|
||||
|
||||
# Flag to hide console window on Windows
|
||||
CREATE_NO_WINDOW = 0x08000000
|
||||
|
||||
class LicenseManager:
|
||||
@staticmethod
|
||||
def get_motherboard_id():
|
||||
"""Gets the motherboard UUID using WMIC (Windows only)."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['wmic', 'csproduct', 'get', 'uuid'],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
creationflags=CREATE_NO_WINDOW
|
||||
)
|
||||
lines = result.stdout.strip().split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line and line != 'UUID':
|
||||
return line
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_processor_id():
|
||||
"""Gets the processor ID using WMIC (Windows only)."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['wmic', 'cpu', 'get', 'processorid'],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
creationflags=CREATE_NO_WINDOW
|
||||
)
|
||||
lines = result.stdout.strip().split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line and line != 'ProcessorId':
|
||||
return line
|
||||
except:
|
||||
pass
|
||||
# Fallback to platform.processor()
|
||||
return platform.processor()
|
||||
|
||||
@staticmethod
|
||||
def _hash_to_hwid(raw_id):
|
||||
"""Converts raw ID string to formatted HWID."""
|
||||
hwid_hash = hashlib.sha256(raw_id.encode()).hexdigest().upper()
|
||||
return "{}-{}-{}-{}".format(hwid_hash[:4], hwid_hash[4:8], hwid_hash[8:12], hwid_hash[12:16])
|
||||
|
||||
@staticmethod
|
||||
def get_hardware_id():
|
||||
"""
|
||||
Returns hardware ID based on combination of Motherboard UUID + Processor ID.
|
||||
This is for DISPLAY purposes to customer.
|
||||
"""
|
||||
mb_id = LicenseManager.get_motherboard_id() or "UNKNOWN_MB"
|
||||
proc_id = LicenseManager.get_processor_id() or "UNKNOWN_CPU"
|
||||
|
||||
# Combine both IDs for display
|
||||
combined = "COMBO-{}-{}".format(mb_id, proc_id)
|
||||
return LicenseManager._hash_to_hwid(combined)
|
||||
|
||||
@staticmethod
|
||||
def get_mb_hwid():
|
||||
"""Returns HWID based on motherboard only."""
|
||||
mb_id = LicenseManager.get_motherboard_id() or "UNKNOWN_MB"
|
||||
return LicenseManager._hash_to_hwid("MB-{}".format(mb_id))
|
||||
|
||||
@staticmethod
|
||||
def get_cpu_hwid():
|
||||
"""Returns HWID based on processor only."""
|
||||
proc_id = LicenseManager.get_processor_id() or "UNKNOWN_CPU"
|
||||
return LicenseManager._hash_to_hwid("CPU-{}".format(proc_id))
|
||||
|
||||
@staticmethod
|
||||
def generate_signature(hwid):
|
||||
"""Generates the expected signature for a given HWID."""
|
||||
data = "{}|{}".format(hwid, SECRET_SALT)
|
||||
return hashlib.sha256(data.encode()).hexdigest().upper()
|
||||
|
||||
@staticmethod
|
||||
def validate_license(key, hwid=None):
|
||||
"""
|
||||
Validates license with flexible hardware check.
|
||||
Returns tuple: (is_valid, status)
|
||||
- status: 'valid', 'mb_changed', 'cpu_changed', 'both_changed', 'invalid'
|
||||
"""
|
||||
if not key:
|
||||
return False, 'invalid'
|
||||
|
||||
key = key.strip().upper()
|
||||
|
||||
# Check against combined HWID (exact match - preferred)
|
||||
combined_hwid = LicenseManager.get_hardware_id()
|
||||
expected_combined = LicenseManager.generate_signature(combined_hwid)[:32]
|
||||
if key == expected_combined:
|
||||
return True, 'valid'
|
||||
|
||||
# Check against motherboard only
|
||||
mb_hwid = LicenseManager.get_mb_hwid()
|
||||
expected_mb = LicenseManager.generate_signature(mb_hwid)[:32]
|
||||
mb_match = (key == expected_mb)
|
||||
|
||||
# Check against processor only
|
||||
cpu_hwid = LicenseManager.get_cpu_hwid()
|
||||
expected_cpu = LicenseManager.generate_signature(cpu_hwid)[:32]
|
||||
cpu_match = (key == expected_cpu)
|
||||
|
||||
# If either matches, license is valid (single hardware change allowed)
|
||||
if mb_match:
|
||||
return True, 'cpu_changed' # MB matches, CPU must have changed
|
||||
if cpu_match:
|
||||
return True, 'mb_changed' # CPU matches, MB must have changed
|
||||
|
||||
# Neither matches - could be both changed or completely invalid key
|
||||
return False, 'both_changed'
|
||||
|
||||
@staticmethod
|
||||
def save_license(key):
|
||||
try:
|
||||
with open("license.dat", "w") as f:
|
||||
f.write(key.strip())
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def load_license():
|
||||
if not os.path.exists("license.dat"):
|
||||
return None
|
||||
try:
|
||||
with open("license.dat", "r") as f:
|
||||
return f.read().strip()
|
||||
except:
|
||||
return None
|
||||
BIN
python-3.4.4.msi
Normal file
BIN
python-3.4.4.msi
Normal file
Binary file not shown.
105
registration_dialog.py
Normal file
105
registration_dialog.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QMessageBox, QApplication)
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QClipboard
|
||||
|
||||
from license_manager import LicenseManager
|
||||
|
||||
class RegistrationDialog(QDialog):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Aktivasi ProBackup")
|
||||
self.resize(400, 300)
|
||||
self.setStyleSheet("""
|
||||
QDialog { background-color: #282a36; color: #f8f8f2; font-family: "Segoe UI"; }
|
||||
QLabel { color: #f8f8f2; font-size: 14px; }
|
||||
QLineEdit { background-color: #44475a; border: 1px solid #6272a4; padding: 8px; color: #f8f8f2; border-radius: 4px; }
|
||||
QPushButton { background-color: #6272a4; color: white; border: none; padding: 10px; border-radius: 4px; font-weight: bold; }
|
||||
QPushButton:hover { background-color: #bd93f9; }
|
||||
""")
|
||||
|
||||
self.hwid = LicenseManager.get_hardware_id()
|
||||
self.verified = False
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(15)
|
||||
|
||||
# Title
|
||||
title = QLabel("Aktivasi Produk Diperlukan")
|
||||
title.setStyleSheet("font-size: 18px; font-weight: bold; color: #ff5555;")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
|
||||
# Info
|
||||
info = QLabel("Untuk menggunakan aplikasi ini, silakan kirimkan ID Mesin di bawah ini ke administrator untuk mendapatkan Kunci Aktivasi.")
|
||||
info.setWordWrap(True)
|
||||
layout.addWidget(info)
|
||||
|
||||
# HWID Display
|
||||
layout.addWidget(QLabel("ID Mesin Anda:"))
|
||||
|
||||
self.hwid_input = QLineEdit(self.hwid)
|
||||
self.hwid_input.setReadOnly(True)
|
||||
self.hwid_input.setStyleSheet("font-family: Consolas; font-size: 14px; color: #50fa7b;")
|
||||
layout.addWidget(self.hwid_input)
|
||||
|
||||
copy_btn = QPushButton("Salin ID Mesin")
|
||||
copy_btn.clicked.connect(self.copy_hwid)
|
||||
layout.addWidget(copy_btn)
|
||||
|
||||
# Key Input
|
||||
layout.addWidget(QLabel("Masukkan Kunci Aktivasi:"))
|
||||
self.key_input = QLineEdit()
|
||||
self.key_input.setPlaceholderText("Tempel kunci aktivasi disini...")
|
||||
layout.addWidget(self.key_input)
|
||||
|
||||
# Actions
|
||||
activate_btn = QPushButton("AKTIFKAN")
|
||||
activate_btn.setStyleSheet("background-color: #50fa7b; color: #282a36;")
|
||||
activate_btn.clicked.connect(self.verify_key)
|
||||
layout.addWidget(activate_btn)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
def copy_hwid(self):
|
||||
cb = QApplication.clipboard()
|
||||
cb.setText(self.hwid)
|
||||
QMessageBox.information(self, "Disalin", "ID Mesin telah disalin ke clipboard.")
|
||||
|
||||
def verify_key(self):
|
||||
key = self.key_input.text().strip()
|
||||
is_valid, status = LicenseManager.validate_license(key, self.hwid)
|
||||
|
||||
if is_valid:
|
||||
LicenseManager.save_license(key)
|
||||
if status == 'mb_changed':
|
||||
QMessageBox.warning(self, "Peringatan Hardware",
|
||||
"Aktivasi berhasil, namun terdeteksi MOTHERBOARD telah berubah.\n"
|
||||
"Jika Anda mengganti hardware lagi, lisensi bisa tidak valid.")
|
||||
elif status == 'cpu_changed':
|
||||
QMessageBox.warning(self, "Peringatan Hardware",
|
||||
"Aktivasi berhasil, namun terdeteksi PROCESSOR telah berubah.\n"
|
||||
"Jika Anda mengganti hardware lagi, lisensi bisa tidak valid.")
|
||||
else:
|
||||
QMessageBox.information(self, "Sukses", "Aktivasi Berhasil! Terima kasih.")
|
||||
self.verified = True
|
||||
self.accept()
|
||||
else:
|
||||
if status == 'both_changed':
|
||||
QMessageBox.critical(self, "Hardware Berubah",
|
||||
"Motherboard DAN Processor terdeteksi berubah.\n\n"
|
||||
"Lisensi tidak valid untuk hardware baru ini.\n"
|
||||
"Silakan hubungi admin untuk aktivasi ulang.\n\n"
|
||||
"WA: +62 817-0380-6655")
|
||||
else:
|
||||
QMessageBox.warning(self, "Gagal", "Kunci Aktivasi Salah. Silakan coba lagi.")
|
||||
|
||||
def closeEvent(self, event):
|
||||
if not self.verified:
|
||||
# If closed without verification, reject dialog
|
||||
self.reject()
|
||||
else:
|
||||
event.accept()
|
||||
52
registry_manager.py
Normal file
52
registry_manager.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import winreg
|
||||
import hashlib
|
||||
from license_manager import LicenseManager
|
||||
|
||||
class RegistryManager:
|
||||
# We will generate a path based on HWID hash to obscure it
|
||||
# Format: Software\{SHA256_of_HWID_First_16_Chars}
|
||||
|
||||
@staticmethod
|
||||
def _get_reg_path():
|
||||
hwid = LicenseManager.get_hardware_id()
|
||||
# Create a hash of the HWID to make it look like a random system key
|
||||
path_hash = hashlib.md5("WartanaProBackup_{}".format(hwid).encode()).hexdigest().upper()
|
||||
return "SOFTWARE\\{}".format(path_hash)
|
||||
|
||||
@staticmethod
|
||||
def get_trial_count():
|
||||
try:
|
||||
path = RegistryManager._get_reg_path()
|
||||
# Open Key
|
||||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_READ)
|
||||
value, regtype = winreg.QueryValueEx(key, "SystemState") # Obscured value name too
|
||||
winreg.CloseKey(key)
|
||||
return int(value)
|
||||
except WindowsError:
|
||||
# Key not found, return 0
|
||||
return 0
|
||||
except Exception:
|
||||
return 3 # Fail safe -> block if error
|
||||
|
||||
@staticmethod
|
||||
def increment_trial_count():
|
||||
try:
|
||||
path = RegistryManager._get_reg_path()
|
||||
# Create key if not exists
|
||||
key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, path)
|
||||
|
||||
# Read current
|
||||
current = 0
|
||||
try:
|
||||
val, _ = winreg.QueryValueEx(key, "SystemState")
|
||||
current = int(val)
|
||||
except: pass
|
||||
|
||||
# Increment
|
||||
new_val = current + 1
|
||||
winreg.SetValueEx(key, "SystemState", 0, winreg.REG_DWORD, new_val)
|
||||
winreg.CloseKey(key)
|
||||
return new_val
|
||||
except Exception as e:
|
||||
print("Reg Error: {}".format(e))
|
||||
return 3 # Fail safe
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
PyQt5==5.6
|
||||
pymysql==0.9.3
|
||||
psycopg2==2.7.7
|
||||
apscheduler==3.6.3
|
||||
pyinstaller==3.5
|
||||
# cryptography removed due to compilation issues on win7/py3.4
|
||||
26
rthook_qt_fix.py
Normal file
26
rthook_qt_fix.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Fixing Qt Platform Plugin "windows" error for PyInstaller OneFile
|
||||
# This runs BEFORE main.py
|
||||
|
||||
def install_qt_plugin_path():
|
||||
# If running in a PyInstaller bundle
|
||||
if getattr(sys, 'frozen', False):
|
||||
# sys._MEIPASS is where PyInstaller unpacks the bundle
|
||||
base_path = getattr(sys, '_MEIPASS', os.path.dirname(sys.executable))
|
||||
|
||||
# We bundled paths via "a.datas += Tree(..., prefix='platforms')"
|
||||
# So it should be at base_path/platforms
|
||||
platform_plugin_path = os.path.join(base_path, 'platforms')
|
||||
|
||||
print("runtime_hook: Detected base path: {}".format(base_path))
|
||||
print("runtime_hook: Setting QT_QPA_PLATFORM_PLUGIN_PATH to: {}".format(platform_plugin_path))
|
||||
|
||||
# Force Qt to look here
|
||||
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = platform_plugin_path
|
||||
|
||||
# Also ensure general plugin path is set, just in case
|
||||
os.environ['QT_PLUGIN_PATH'] = base_path
|
||||
|
||||
install_qt_plugin_path()
|
||||
BIN
sql_tools/libcrypto-1_1-x64.dll
Normal file
BIN
sql_tools/libcrypto-1_1-x64.dll
Normal file
Binary file not shown.
BIN
sql_tools/libiconv-2.dll
Normal file
BIN
sql_tools/libiconv-2.dll
Normal file
Binary file not shown.
BIN
sql_tools/libintl-9.dll
Normal file
BIN
sql_tools/libintl-9.dll
Normal file
Binary file not shown.
BIN
sql_tools/libpq.dll
Normal file
BIN
sql_tools/libpq.dll
Normal file
Binary file not shown.
BIN
sql_tools/libssl-1_1-x64.dll
Normal file
BIN
sql_tools/libssl-1_1-x64.dll
Normal file
Binary file not shown.
BIN
sql_tools/libwinpthread-1.dll
Normal file
BIN
sql_tools/libwinpthread-1.dll
Normal file
Binary file not shown.
BIN
sql_tools/mariadb/10.11/mariadb-dump.exe
Normal file
BIN
sql_tools/mariadb/10.11/mariadb-dump.exe
Normal file
Binary file not shown.
BIN
sql_tools/mariadb/10.11/mysqldump.exe
Normal file
BIN
sql_tools/mariadb/10.11/mysqldump.exe
Normal file
Binary file not shown.
BIN
sql_tools/mariadb/10.6/mariadb-dump.exe
Normal file
BIN
sql_tools/mariadb/10.6/mariadb-dump.exe
Normal file
Binary file not shown.
BIN
sql_tools/mariadb/10.6/mysqldump.exe
Normal file
BIN
sql_tools/mariadb/10.6/mysqldump.exe
Normal file
Binary file not shown.
BIN
sql_tools/mariadb/11.8/mariadb-dump.exe
Normal file
BIN
sql_tools/mariadb/11.8/mariadb-dump.exe
Normal file
Binary file not shown.
BIN
sql_tools/mariadb/11.8/mysqldump.exe
Normal file
BIN
sql_tools/mariadb/11.8/mysqldump.exe
Normal file
Binary file not shown.
BIN
sql_tools/mysql/5.7/mysqldump.exe
Normal file
BIN
sql_tools/mysql/5.7/mysqldump.exe
Normal file
Binary file not shown.
BIN
sql_tools/mysql/8.0/mysqldump.exe
Normal file
BIN
sql_tools/mysql/8.0/mysqldump.exe
Normal file
Binary file not shown.
BIN
sql_tools/mysql/8.4/mysqldump.exe
Normal file
BIN
sql_tools/mysql/8.4/mysqldump.exe
Normal file
Binary file not shown.
BIN
sql_tools/mysql/9.4/mysqldump.exe
Normal file
BIN
sql_tools/mysql/9.4/mysqldump.exe
Normal file
Binary file not shown.
BIN
sql_tools/mysql/9.5/mysqldump.exe
Normal file
BIN
sql_tools/mysql/9.5/mysqldump.exe
Normal file
Binary file not shown.
BIN
sql_tools/mysqldump.exe
Normal file
BIN
sql_tools/mysqldump.exe
Normal file
Binary file not shown.
BIN
sql_tools/pg_dump.exe
Normal file
BIN
sql_tools/pg_dump.exe
Normal file
Binary file not shown.
BIN
sql_tools/pg_dumpall.exe
Normal file
BIN
sql_tools/pg_dumpall.exe
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/icudt77.dll
Normal file
BIN
sql_tools/postgres/18.1/icudt77.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/icuin77.dll
Normal file
BIN
sql_tools/postgres/18.1/icuin77.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/icuio77.dll
Normal file
BIN
sql_tools/postgres/18.1/icuio77.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/icutu77.dll
Normal file
BIN
sql_tools/postgres/18.1/icutu77.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/icuuc77.dll
Normal file
BIN
sql_tools/postgres/18.1/icuuc77.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/libcrypto-3-x64.dll
Normal file
BIN
sql_tools/postgres/18.1/libcrypto-3-x64.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/libcurl.dll
Normal file
BIN
sql_tools/postgres/18.1/libcurl.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/libecpg.dll
Normal file
BIN
sql_tools/postgres/18.1/libecpg.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/libecpg_compat.dll
Normal file
BIN
sql_tools/postgres/18.1/libecpg_compat.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/libiconv-2.dll
Normal file
BIN
sql_tools/postgres/18.1/libiconv-2.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/libintl-9.dll
Normal file
BIN
sql_tools/postgres/18.1/libintl-9.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/liblz4.dll
Normal file
BIN
sql_tools/postgres/18.1/liblz4.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/libpgtypes.dll
Normal file
BIN
sql_tools/postgres/18.1/libpgtypes.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/libpq.dll
Normal file
BIN
sql_tools/postgres/18.1/libpq.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/libssl-3-x64.dll
Normal file
BIN
sql_tools/postgres/18.1/libssl-3-x64.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/libwinpthread-1.dll
Normal file
BIN
sql_tools/postgres/18.1/libwinpthread-1.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/libxml2.dll
Normal file
BIN
sql_tools/postgres/18.1/libxml2.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/libxslt.dll
Normal file
BIN
sql_tools/postgres/18.1/libxslt.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/libzstd.dll
Normal file
BIN
sql_tools/postgres/18.1/libzstd.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/pg_dump.exe
Normal file
BIN
sql_tools/postgres/18.1/pg_dump.exe
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/pg_dumpall.exe
Normal file
BIN
sql_tools/postgres/18.1/pg_dumpall.exe
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/testplug.dll
Normal file
BIN
sql_tools/postgres/18.1/testplug.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/wxbase328u_net_vc_x64_custom.dll
Normal file
BIN
sql_tools/postgres/18.1/wxbase328u_net_vc_x64_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/wxbase328u_vc_x64_custom.dll
Normal file
BIN
sql_tools/postgres/18.1/wxbase328u_vc_x64_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/wxbase328u_xml_vc_x64_custom.dll
Normal file
BIN
sql_tools/postgres/18.1/wxbase328u_xml_vc_x64_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/wxmsw328u_adv_vc_x64_custom.dll
Normal file
BIN
sql_tools/postgres/18.1/wxmsw328u_adv_vc_x64_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/wxmsw328u_aui_vc_x64_custom.dll
Normal file
BIN
sql_tools/postgres/18.1/wxmsw328u_aui_vc_x64_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/wxmsw328u_core_vc_x64_custom.dll
Normal file
BIN
sql_tools/postgres/18.1/wxmsw328u_core_vc_x64_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/wxmsw328u_html_vc_x64_custom.dll
Normal file
BIN
sql_tools/postgres/18.1/wxmsw328u_html_vc_x64_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/wxmsw328u_xrc_vc_x64_custom.dll
Normal file
BIN
sql_tools/postgres/18.1/wxmsw328u_xrc_vc_x64_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/18.1/zlib1.dll
Normal file
BIN
sql_tools/postgres/18.1/zlib1.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/libcrypto-1_1-x64.dll
Normal file
BIN
sql_tools/postgres/9.6/libcrypto-1_1-x64.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/libcurl.dll
Normal file
BIN
sql_tools/postgres/9.6/libcurl.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/libecpg.dll
Normal file
BIN
sql_tools/postgres/9.6/libecpg.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/libecpg_compat.dll
Normal file
BIN
sql_tools/postgres/9.6/libecpg_compat.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/libiconv-2.dll
Normal file
BIN
sql_tools/postgres/9.6/libiconv-2.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/libintl-8.dll
Normal file
BIN
sql_tools/postgres/9.6/libintl-8.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/libpgtypes.dll
Normal file
BIN
sql_tools/postgres/9.6/libpgtypes.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/libpq.dll
Normal file
BIN
sql_tools/postgres/9.6/libpq.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/libssl-1_1-x64.dll
Normal file
BIN
sql_tools/postgres/9.6/libssl-1_1-x64.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/libxml2.dll
Normal file
BIN
sql_tools/postgres/9.6/libxml2.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/libxslt.dll
Normal file
BIN
sql_tools/postgres/9.6/libxslt.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/pg_dump.exe
Normal file
BIN
sql_tools/postgres/9.6/pg_dump.exe
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/pg_dumpall.exe
Normal file
BIN
sql_tools/postgres/9.6/pg_dumpall.exe
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/wxbase28u_net_vc_custom.dll
Normal file
BIN
sql_tools/postgres/9.6/wxbase28u_net_vc_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/wxbase28u_vc_custom.dll
Normal file
BIN
sql_tools/postgres/9.6/wxbase28u_vc_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/wxbase28u_xml_vc_custom.dll
Normal file
BIN
sql_tools/postgres/9.6/wxbase28u_xml_vc_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/wxmsw28u_adv_vc_custom.dll
Normal file
BIN
sql_tools/postgres/9.6/wxmsw28u_adv_vc_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/wxmsw28u_aui_vc_custom.dll
Normal file
BIN
sql_tools/postgres/9.6/wxmsw28u_aui_vc_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/wxmsw28u_core_vc_custom.dll
Normal file
BIN
sql_tools/postgres/9.6/wxmsw28u_core_vc_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/wxmsw28u_html_vc_custom.dll
Normal file
BIN
sql_tools/postgres/9.6/wxmsw28u_html_vc_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/wxmsw28u_stc_vc_custom.dll
Normal file
BIN
sql_tools/postgres/9.6/wxmsw28u_stc_vc_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/wxmsw28u_xrc_vc_custom.dll
Normal file
BIN
sql_tools/postgres/9.6/wxmsw28u_xrc_vc_custom.dll
Normal file
Binary file not shown.
BIN
sql_tools/postgres/9.6/zlib1.dll
Normal file
BIN
sql_tools/postgres/9.6/zlib1.dll
Normal file
Binary file not shown.
BIN
sql_tools/zlib1.dll
Normal file
BIN
sql_tools/zlib1.dll
Normal file
Binary file not shown.
BIN
vcredist_x86.exe
Normal file
BIN
vcredist_x86.exe
Normal file
Binary file not shown.
Reference in New Issue
Block a user