Extract language packs to a temp directory and verify before swapping
Follow-up to the previous download fix: the old pack was no longer deleted before the download, but it was still deleted at the start of the extraction step. If the app crashed mid-extraction or the archive was truncated, the device was left without a valid pack; worse, a partially extracted directory containing language.db would pass is_language_directory_valid() on the next offline launch and be accepted as a working pack. The extraction thread now follows an extract / verify / swap sequence: 1. Extract the archive into <lang>_tmp instead of the final location, leaving the current pack untouched and avoiding any risk of merging old and new files. 2. Validate the extracted pack (non-empty, language.db present) before touching the current one; on failure, discard the temp directory and the archive, show the invalid-directory error, and keep playing with the current pack. 3. Only then delete the previous pack and rename the new one into place. The window without a valid pack shrinks from the whole extraction to two filesystem operations. If the final rename fails, the temp directory is kept (the data is intact on disk) and the error popup is shown; the next launch detects the missing pack and downloads it again. A leftover temp directory from a crash is removed at the start of the next extraction. Verified with the GUT test suite (62/62 passing).
This commit is contained in:
@@ -176,29 +176,46 @@ func _copy_data(this: PackageDownloader) -> void:
|
||||
mutex.unlock()
|
||||
)
|
||||
|
||||
# Cleanup previous files
|
||||
if DirAccess.dir_exists_absolute(current_language_path):
|
||||
Log.trace("PackageDownloader: Removing existing language directory before extraction")
|
||||
Utils.delete_directory_recursive(ProjectSettings.globalize_path(current_language_path))
|
||||
|
||||
# Extract the archive
|
||||
var subfolder: String = unzipper.extract(language_zip_path, USER_LANGUAGE_RESOURCES_PATH, false)
|
||||
# Extract to a temporary directory so the current pack stays usable if
|
||||
# the extraction fails or is interrupted
|
||||
var temp_extract_path: String = USER_LANGUAGE_RESOURCES_PATH.path_join(language + "_tmp")
|
||||
if DirAccess.dir_exists_absolute(temp_extract_path):
|
||||
Utils.delete_directory_recursive(ProjectSettings.globalize_path(temp_extract_path))
|
||||
|
||||
var subfolder: String = unzipper.extract(language_zip_path, temp_extract_path, false)
|
||||
if subfolder == "":
|
||||
Log.error("PackageDownloader: Extraction failed for %s" % language_zip_path)
|
||||
this.call_thread_safe("_show_error", 2) # Error downloading
|
||||
return
|
||||
|
||||
# Move the data to the locale folder of the user
|
||||
var error: Error = DirAccess.rename_absolute(USER_LANGUAGE_RESOURCES_PATH.path_join(subfolder), current_language_path)
|
||||
|
||||
# Check the new pack before replacing the current one
|
||||
var new_pack_path: String = temp_extract_path.path_join(subfolder)
|
||||
if not is_language_directory_valid(new_pack_path):
|
||||
Log.error("PackageDownloader: Extracted package at %s is invalid, keeping the current language pack" % new_pack_path)
|
||||
Utils.delete_directory_recursive(ProjectSettings.globalize_path(temp_extract_path))
|
||||
DirAccess.remove_absolute(language_zip_path)
|
||||
this.call_thread_safe("_show_error", 3) # Invalid language directory
|
||||
return
|
||||
|
||||
# Replace the previous pack, now that the new one is fully extracted
|
||||
if DirAccess.dir_exists_absolute(current_language_path):
|
||||
Log.trace("PackageDownloader: Removing previous language directory")
|
||||
Utils.delete_directory_recursive(ProjectSettings.globalize_path(current_language_path))
|
||||
|
||||
var error: Error = DirAccess.rename_absolute(new_pack_path, current_language_path)
|
||||
if error != OK:
|
||||
Log.error("PackageDownloader: Error " + error_string(error) + " while renaming folder from %s to %s" % [USER_LANGUAGE_RESOURCES_PATH.path_join(subfolder), current_language_path])
|
||||
else:
|
||||
Log.trace("PackageDownloader: Package extracted to %s" % current_language_path)
|
||||
|
||||
# Keep the temporary directory so the data is not lost; the next
|
||||
# launch will detect the missing pack and download it again
|
||||
Log.error("PackageDownloader: Error " + error_string(error) + " while renaming folder from %s to %s" % [new_pack_path, current_language_path])
|
||||
this.call_thread_safe("_show_error", 2) # Error downloading
|
||||
return
|
||||
Log.trace("PackageDownloader: Package extracted to %s" % current_language_path)
|
||||
|
||||
# Cleanup unnecessary files
|
||||
Utils.delete_directory_recursive(ProjectSettings.globalize_path(temp_extract_path))
|
||||
DirAccess.remove_absolute(language_zip_path)
|
||||
Log.trace("PackageDownloader: Removed temporary archive %s" % language_zip_path)
|
||||
|
||||
|
||||
# Go to main menu
|
||||
this.call_thread_safe("_go_to_next_scene")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user