initial commit

This commit is contained in:
Umiko 2025-05-02 07:05:17 +07:00
commit 82411a47ea
6 changed files with 539 additions and 0 deletions

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
build/
dist/
*.egg-info/
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.cache
.pytest_cache/
# VS Code
.vscode/
# Compiled executable files
*.exe
# System files
.DS_Store
Thumbs.db
# Environment files
.env
.venv/
ENV/
env/
# Manifest files
manifest.json
#Add more if you need.

62
Generator.py Normal file
View File

@ -0,0 +1,62 @@
import os
import hashlib
import json
import argparse
excluded_paths = [
'manifest.json',
'Updater.exe'
]
def is_excluded(path):
for excluded in excluded_paths:
if path.startswith(excluded):
return True
return False
def file_hash(path):
h = hashlib.sha1()
with open(path, 'rb') as f:
while chunk := f.read(8192):
h.update(chunk)
return h.hexdigest()
def generate_manifest(directory):
manifest = {}
for root, dirs, files in os.walk(directory):
for file in files:
file_path = os.path.join(root, file)
relative_path = os.path.relpath(file_path, directory).replace("\\", "/")
if is_excluded(relative_path):
continue
hash_value = file_hash(file_path)
manifest[relative_path] = hash_value
return manifest
def save_manifest(manifest, output_path):
with open(output_path, 'w') as f:
json.dump(manifest, f, indent=4)
print(f"Manifest saved to {output_path}")
def main():
parser = argparse.ArgumentParser(description="Generate manifest.json with SHA1 hashes.")
parser.add_argument('directory', type=str, help="Folder to scan")
parser.add_argument('--output', type=str, help="Optional output path for manifest.json")
args = parser.parse_args()
directory = args.directory
if not os.path.isdir(directory):
print(f"Directory '{directory}' does not exist.")
return
output_path = args.output if args.output else os.path.join(directory, 'manifest.json')
print(f"Scanning directory: {directory}...")
manifest = generate_manifest(directory)
save_manifest(manifest, output_path)
if __name__ == "__main__":
main()

12
MIT-LICENSE.txt Normal file
View File

@ -0,0 +1,12 @@
The MIT License
Version N/A
SPDX short identifier: MIT
Open Source Initiative Approved License
Copyright 2025 Radiant Code
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

166
README.md Normal file
View File

@ -0,0 +1,166 @@
# Updater System
**A simple and customizable update system** for Windows applications.
Supports update checking via a remote `manifest.json` hosted on an HTTP or HTTPS server.
---
## Features
- Automatic checking and downloading of updated files
- GUI built with wxPython
- File integrity verification using SHA-1
- Easy integration into any existing application
---
## Requirements
- Python 3.8 or higher
- `wxPython` library (`pip install wxPython`)
- Server access to host `manifest.json` and application files over HTTP or HTTPS
---
## How It Works
1. The application provides the updater with the base server URL.
2. The updater Reads `manifest.json` and checks which files need updating.
3. If updates are available, users can choose which files to update.
4. Updated files are downloaded and the main application is relaunched.
---
## Setup
### 1. Set `MAIN_APP`
At the top of your `updater.py`, configure:
```python
MAIN_APP = "YourApp.exe"
```
> `MAIN_APP` defines the executable file that will be relaunched after a successful update.
---
### 2. Prepare Your Server
Host the following:
- `manifest.json` (contains file listings and their SHA-1 hashes)
- All application files that may require updating
Ensure files are accessible via URLs, e.g., `https://yourdomain.com/yourapp/manifest.json`.
---
### 3. Generate `manifest.json`
Use the provided `generator.py` tool.
#### Usage:
```bash
python3 generator.py <folder_name>
```
Example:
```bash
python3 generator.py my_app_folder
```
This command scans all files inside the specified folder and generates a `manifest.json`.
**Important Notes:**
- Some files are automatically excluded, such as:
- `manifest.json`
- `Updater.exe`
- You can modify the `excluded_paths` list in `generator.py` to customize exclusions.
---
### 4. Compile the Updater
It is **strongly recommended** to compile `updater.py` into an executable (.exe) for better usability and portability.
You can use any Python-to-EXE compiler, such as:
- [Nuitka](https://nuitka.net/)
- [PyInstaller](https://pyinstaller.org/)
- [cx_Freeze](https://anthony-tuininga.github.io/cx_Freeze/)
Example compilation using PyInstaller:
```bash
pyinstaller --onefile updater.py
```
---
## Integrating the Updater into Your Application
You can easily trigger the updater using the following pattern:
```python
import wx
import os
import threading
import subprocess
class MainFrame(wx.Frame):
def __init__(self, parent, title):
super().__init__(parent, title=title, size=(300, 200))
panel = wx.Panel(self)
menubar = wx.MenuBar()
file_menu = wx.Menu()
run_updater_item = file_menu.Append(wx.ID_ANY, "Run Updater\tCtrl+U")
exit_item = file_menu.Append(wx.ID_EXIT, "Exit\tCtrl+Q")
menubar.Append(file_menu, "File")
self.SetMenuBar(menubar)
# Event bindings
self.Bind(wx.EVT_MENU, self.onRunUpdater, run_updater_item)
self.Bind(wx.EVT_MENU, self.onExit, exit_item)
def run_updater_thread(self, updater_path, updater_key):
"""Launch the updater in a separate thread."""
if os.path.exists(updater_path):
try:
subprocess.Popen([updater_path, updater_key], shell=False)
except Exception as e:
print(f"Error running updater: {e}")
else:
print("Updater executable not found.")
def onRunUpdater(self, event):
"""Handler for running the updater."""
updater_path = os.path.join(os.getcwd(), "Updater.exe")
updater_key = "https://yourdomain.com/yourapp/"
threading.Thread(target=self.run_updater_thread, args=(updater_path, updater_key)).start()
def onExit(self, event):
"""Exit the application."""
self.Close()
if __name__ == "__main__":
app = wx.App(False)
frame = MainFrame(None, "My App")
frame.Show()
app.MainLoop()
```
---
## Example Directory Structure on your server-site
```
/your-app/
Updater.exe
YourApp.exe
manifest.json
(other file/folderss...)
```
---
## Best Practices
- Always compile the updater script to `.exe` format to ensure a smooth user experience.
- Sign your executables if possible to avoid Windows SmartScreen warnings.
- Regularly update your `manifest.json` whenever files are changed.
---
## License
This project is licensed under the MIT License.
You are free to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the software.
See the MIT-LICENSE.txt file for more details.

255
Updater.py Normal file
View File

@ -0,0 +1,255 @@
import os
import sys
import urllib.request
import urllib.error
import hashlib
import json
import wx
import subprocess
import time
import psutil
MAIN_APP = "YourApp.exe"
def file_hash(path):
"""
Calculate and return the SHA-1 hash of the specified file.
"""
h = hashlib.sha1()
with open(path, 'rb') as f:
while chunk := f.read(8192):
h.update(chunk)
return h.hexdigest()
def load_remote_manifest():
"""
Load and return the remote manifest file as a JSON object.
Exit the program if any network or HTTP error occurs.
"""
try:
with urllib.request.urlopen(MANIFEST_URL) as response:
if response.status == 404:
raise Exception(f"Manifest file not found: {MANIFEST_URL}")
return json.load(response)
except urllib.error.HTTPError as e:
if e.code == 404:
wx.MessageBox(f"Error 404: Manifest not found at {MANIFEST_URL}", "File Not Found", wx.OK | wx.ICON_ERROR)
else:
wx.MessageBox(f"HTTP error: {e.code} - {e.reason}", "HTTP Error", wx.OK | wx.ICON_ERROR)
sys.exit(0)
except urllib.error.URLError as e:
wx.MessageBox(f"Network error: {str(e)}\nPlease check your internet connection and try again.", "Network Error", wx.OK | wx.ICON_ERROR)
sys.exit(0)
except Exception as e:
wx.MessageBox(f"An error occurred: {str(e)}", "Error", wx.OK | wx.ICON_ERROR)
sys.exit(0)
def download_file(filename, on_progress=None):
"""
Download a file from the remote URL.
Optionally updates a progress callback.
"""
url = BASE_DOWNLOAD_URL + filename
try:
with urllib.request.urlopen(url) as response:
total_size = int(response.headers.get("Content-Length", 0))
downloaded = 0
with open(filename, 'wb') as out_file:
while True:
chunk = response.read(8192)
if not chunk:
break
out_file.write(chunk)
downloaded += len(chunk)
if on_progress and total_size > 0:
percent = int(downloaded / total_size * 100)
on_progress(percent)
except urllib.error.URLError as e:
wx.MessageBox(f"Network error: {str(e)}\nPlease check your internet connection and try again.", "Network Error", wx.OK | wx.ICON_ERROR)
sys.exit(0)
except Exception as e:
wx.MessageBox(f"Download error: {str(e)}", "Download Error", wx.OK | wx.ICON_ERROR)
sys.exit(0)
def terminate_main_app():
"""
Terminates the main application process if it is currently running.
"""
try:
for proc in psutil.process_iter(['pid', 'name']):
if proc.info['name'] and proc.info['name'].lower() == MAIN_APP.lower():
try:
proc.terminate()
proc.wait(timeout=5)
except psutil.NoSuchProcess:
pass
except Exception as e:
wx.MessageBox(f"Failed to terminate {MAIN_APP}: {str(e)}", "Error", wx.OK | wx.ICON_ERROR)
sys.exit(0)
except Exception as e:
wx.MessageBox(f"Error while checking for {MAIN_APP}: {str(e)}", "Error", wx.OK | wx.ICON_ERROR)
sys.exit(0)
class NoUpdateDialog(wx.Dialog):
"""
Dialog shown when no update is available.
"""
def __init__(self, parent):
super().__init__(parent, title="Updater", size=(300, 120))
panel = wx.Panel(self)
vbox = wx.BoxSizer(wx.VERTICAL)
label = wx.TextCtrl(panel,
value="No updates available. Come back again later!",
style=wx.TE_READONLY | wx.BORDER_NONE
)
label.SetBackgroundColour(panel.GetBackgroundColour())
label.SetFont(wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT))
label.SetName("Update info")
vbox.Add(label, 0, wx.ALL | wx.EXPAND, 15)
close_btn = wx.Button(panel, label="Close")
close_btn.Bind(wx.EVT_BUTTON, self.on_close)
vbox.Add(close_btn, 0, wx.ALL | wx.CENTER, 10)
panel.SetSizer(vbox)
def on_close(self, event):
"""
Closes the dialog when the button is clicked.
"""
self.Close()
def get_remote_file_size(filename):
"""
Returns the size of a remote file in bytes.
"""
url = BASE_DOWNLOAD_URL + filename
with urllib.request.urlopen(url) as response:
return int(response.headers.get("Content-Length", 0))
class UpdateDialog(wx.Dialog):
"""
Dialog that displays changelog and allows user to choose files to update.
"""
def __init__(self, parent, changelog, files):
super().__init__(parent, title="Update Available", size=(550, 600))
panel = wx.Panel(self)
vbox = wx.BoxSizer(wx.VERTICAL)
changelog_area = wx.TextCtrl(panel, value=changelog, style=wx.TE_READONLY | wx.TE_MULTILINE, size=(500, 200))
vbox.Add(wx.StaticText(panel, label="Changelog:"), 0, wx.ALL, 10)
vbox.Add(changelog_area, 0, wx.ALL, 10)
vbox.Add(wx.StaticText(panel, label="Select files to update:"), 0, wx.ALL, 10)
filelist_box = wx.BoxSizer(wx.VERTICAL)
self.update_checkboxes = []
for file in files:
size = self.format_size(get_remote_file_size(file))
checkbox = wx.CheckBox(panel, label=file)
checkbox.SetValue(True)
item_hbox = wx.BoxSizer(wx.HORIZONTAL)
item_hbox.Add(checkbox, 0, wx.ALIGN_CENTER_VERTICAL)
item_hbox.Add(wx.StaticText(panel, label=size), 1, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10)
filelist_box.Add(item_hbox, 0, wx.ALL, 5)
self.update_checkboxes.append((checkbox, file))
vbox.Add(filelist_box, 1, wx.EXPAND | wx.ALL, 10)
hbox = wx.BoxSizer(wx.HORIZONTAL)
update_button = wx.Button(panel, label="&Update Now")
update_button.Bind(wx.EVT_BUTTON, self.on_update_files)
hbox.Add(update_button, 0, wx.ALL, 5)
cancel_button = wx.Button(panel, label="&Cancel")
cancel_button.Bind(wx.EVT_BUTTON, self.on_cancel)
hbox.Add(cancel_button, 0, wx.ALL, 5)
vbox.Add(hbox, 0, wx.ALIGN_CENTER)
self.progress = wx.Gauge(panel, range=100, size=(500, 20))
vbox.Add(wx.StaticText(panel, label="Progress:"), 0, wx.LEFT | wx.TOP, 10)
vbox.Add(self.progress, 0, wx.ALL | wx.EXPAND, 10)
panel.SetSizer(vbox)
def format_size(self, size):
"""
Convert a size in bytes to a human-readable string.
"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024:
return f"{size:.1f} {unit}"
size /= 1024
return f"{size:.1f} TB"
def on_update_files(self, event):
"""
Download selected update files and relaunch the main application.
"""
terminate_main_app()
try:
for checkbox, filename in self.update_checkboxes:
if checkbox.GetValue():
download_file(filename, on_progress=self.progress.SetValue)
self.progress.SetValue(0)
self.Close()
os.startfile(MAIN_APP)
sys.exit(0)
except Exception as e:
wx.MessageBox(f"Update failed:\n{str(e)}", "Update Error", wx.OK | wx.ICON_ERROR)
def on_cancel(self, event):
"""
Cancel the update and exit the application.
"""
self.Close()
sys.exit(0)
def set_base_urls(base_url):
"""
Set global URLs used for manifest, changelog, and downloads.
"""
global BASE_URL, MANIFEST_URL, CHANGELOG_URL, BASE_DOWNLOAD_URL
BASE_URL = base_url.rstrip('/') + '/'
MANIFEST_URL = BASE_URL + "manifest.json"
CHANGELOG_URL = BASE_URL + "changelog.txt"
BASE_DOWNLOAD_URL = BASE_URL
def main():
"""
Entry point for the updater.
Checks for updates and launches the appropriate dialog.
"""
if len(sys.argv) < 2:
print("Usage: python3 updater.py <base_url>")
sys.exit(0)
set_base_urls(sys.argv[1])
try:
app = wx.App(False)
manifest = load_remote_manifest()
manifest.pop("manifest.json", None)
to_update = [
fname for fname, remote_hash in manifest.items()
if not os.path.exists(fname) or file_hash(fname) != remote_hash
]
if not to_update:
dialog = NoUpdateDialog(None)
dialog.ShowModal()
dialog.Destroy()
sys.exit(0)
else:
with urllib.request.urlopen(CHANGELOG_URL) as res:
changelog = res.read().decode()
dialog = UpdateDialog(None, changelog, to_update)
dialog.ShowModal()
app.MainLoop()
except Exception as e:
print(f"Updater Error: {e}")
if __name__ == "__main__":
main()

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
wxPython
psutil