163 lines
4.7 KiB
GDScript
163 lines
4.7 KiB
GDScript
extends Node
|
|
class_name UpdateApplier
|
|
## Handles applying downloaded updates: backup, replace, rollback
|
|
|
|
signal update_started
|
|
signal update_progress(step: String, percentage: float)
|
|
signal update_completed
|
|
signal update_failed(error: String)
|
|
signal rollback_completed
|
|
|
|
const BACKUP_COUNT := 2 # Keep this many backup versions
|
|
|
|
func apply_update(downloaded_pck_path: String, version: String) -> void:
|
|
emit_signal("update_started")
|
|
|
|
var game_dir := Config.GAME_DIRECTORY
|
|
var pck_name := Config.get_game_pck()
|
|
var target_pck := game_dir + pck_name
|
|
var backup_dir := Config.BACKUP_DIRECTORY
|
|
|
|
# Step 1: Ensure game directory exists
|
|
emit_signal("update_progress", "Preparing directories...", 10.0)
|
|
if not _ensure_directory(game_dir):
|
|
emit_signal("update_failed", "Failed to create game directory")
|
|
return
|
|
|
|
if not _ensure_directory(backup_dir):
|
|
emit_signal("update_failed", "Failed to create backup directory")
|
|
return
|
|
|
|
# Step 2: Backup existing PCK if it exists
|
|
emit_signal("update_progress", "Backing up current version...", 30.0)
|
|
if FileAccess.file_exists(target_pck):
|
|
var backup_name := pck_name.get_basename() + "_backup_" + Time.get_datetime_string_from_system().replace(":", "-") + ".pck"
|
|
var backup_path := backup_dir + backup_name
|
|
|
|
if not _copy_file(target_pck, backup_path):
|
|
emit_signal("update_failed", "Failed to backup current version")
|
|
return
|
|
|
|
# Clean up old backups
|
|
_cleanup_old_backups(backup_dir, BACKUP_COUNT)
|
|
|
|
# Step 3: Move downloaded file to game directory
|
|
emit_signal("update_progress", "Installing update...", 60.0)
|
|
|
|
# Delete old PCK first
|
|
if FileAccess.file_exists(target_pck):
|
|
var dir := DirAccess.open(game_dir)
|
|
if dir:
|
|
var err := dir.remove(pck_name)
|
|
if err != OK:
|
|
emit_signal("update_failed", "Failed to remove old game file")
|
|
return
|
|
|
|
# Copy new PCK
|
|
if not _copy_file(downloaded_pck_path, target_pck):
|
|
emit_signal("update_failed", "Failed to install update")
|
|
return
|
|
|
|
# Step 4: Copy executable if needed (first install)
|
|
emit_signal("update_progress", "Finalizing...", 80.0)
|
|
|
|
# Clean up temp file
|
|
var temp_dir := DirAccess.open("user://temp/")
|
|
if temp_dir:
|
|
temp_dir.remove(downloaded_pck_path.get_file())
|
|
|
|
emit_signal("update_progress", "Update complete!", 100.0)
|
|
emit_signal("update_completed")
|
|
|
|
func _ensure_directory(path: String) -> bool:
|
|
var dir := DirAccess.open("user://")
|
|
if dir:
|
|
var relative_path := path.replace("user://", "")
|
|
if not dir.dir_exists(relative_path):
|
|
return dir.make_dir_recursive(relative_path) == OK
|
|
return true
|
|
return false
|
|
|
|
func _copy_file(from: String, to: String) -> bool:
|
|
var source := FileAccess.open(from, FileAccess.READ)
|
|
if not source:
|
|
push_error("[UpdateApplier] Cannot open source file: " + from)
|
|
return false
|
|
|
|
var dest := FileAccess.open(to, FileAccess.WRITE)
|
|
if not dest:
|
|
source.close()
|
|
push_error("[UpdateApplier] Cannot open destination file: " + to)
|
|
return false
|
|
|
|
# Copy in chunks for large files
|
|
const CHUNK_SIZE := 1024 * 1024 # 1MB chunks
|
|
while source.get_position() < source.get_length():
|
|
var chunk := source.get_buffer(CHUNK_SIZE)
|
|
dest.store_buffer(chunk)
|
|
|
|
source.close()
|
|
dest.close()
|
|
|
|
print("[UpdateApplier] Copied: ", from, " -> ", to)
|
|
return true
|
|
|
|
func _cleanup_old_backups(backup_dir: String, keep_count: int) -> void:
|
|
var dir := DirAccess.open(backup_dir)
|
|
if not dir:
|
|
return
|
|
|
|
var backups: Array[String] = []
|
|
dir.list_dir_begin()
|
|
var file := dir.get_next()
|
|
while file != "":
|
|
if file.ends_with(".pck") and "backup" in file:
|
|
backups.append(file)
|
|
file = dir.get_next()
|
|
dir.list_dir_end()
|
|
|
|
# Sort by name (which includes timestamp)
|
|
backups.sort()
|
|
|
|
# Remove oldest backups if we have too many
|
|
while backups.size() > keep_count:
|
|
var old_backup = backups.pop_front()
|
|
dir.remove(old_backup)
|
|
print("[UpdateApplier] Removed old backup: ", old_backup)
|
|
|
|
func rollback_to_backup() -> bool:
|
|
## Rollback to the most recent backup
|
|
var backup_dir := Config.BACKUP_DIRECTORY
|
|
var game_dir := Config.GAME_DIRECTORY
|
|
var pck_name := Config.get_game_pck()
|
|
|
|
var dir := DirAccess.open(backup_dir)
|
|
if not dir:
|
|
push_error("[UpdateApplier] Cannot access backup directory")
|
|
return false
|
|
|
|
# Find the most recent backup
|
|
var backups: Array[String] = []
|
|
dir.list_dir_begin()
|
|
var file := dir.get_next()
|
|
while file != "":
|
|
if file.ends_with(".pck") and "backup" in file:
|
|
backups.append(file)
|
|
file = dir.get_next()
|
|
dir.list_dir_end()
|
|
|
|
if backups.is_empty():
|
|
push_error("[UpdateApplier] No backups found")
|
|
return false
|
|
|
|
backups.sort()
|
|
var latest_backup = backups.back()
|
|
|
|
# Restore the backup
|
|
if _copy_file(backup_dir + latest_backup, game_dir + pck_name):
|
|
emit_signal("rollback_completed")
|
|
print("[UpdateApplier] Rolled back to: ", latest_backup)
|
|
return true
|
|
|
|
return false
|