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