generated from SGDA/GodotExampleProject
feat: base
This commit is contained in:
@ -0,0 +1,155 @@
|
||||
@tool
|
||||
class_name APIClient
|
||||
extends Node
|
||||
|
||||
|
||||
signal response_received(response_body)
|
||||
signal request_failed(error)
|
||||
|
||||
const RESULT_CANT_CONNECT = "Failed to connect"
|
||||
const RESULT_CANT_RESOLVE = "Failed to resolve"
|
||||
const RESULT_CONNECTION_ERROR = "Connection error"
|
||||
const RESULT_TIMEOUT = "Connection timeout"
|
||||
const RESULT_SERVER_ERROR = "Server error"
|
||||
const REQUEST_FAILED = "Error in the request"
|
||||
const REQUEST_TIMEOUT = "Request timed out on the client side"
|
||||
const URL_NOT_SET = "URL parameter is not set"
|
||||
const PARSE_FAILED = "Parsing failed"
|
||||
|
||||
## Location of the API endpoint.
|
||||
@export var api_url : String
|
||||
## HTTP request method to use. Typically GET or POST.
|
||||
@export var request_method : HTTPClient.Method = HTTPClient.METHOD_POST
|
||||
@export_group("Advanced")
|
||||
## Location of an API key file, if authorization is required by the endpoint.
|
||||
@export_file("*.txt") var api_key_file : String
|
||||
## Time in seconds before the request fails due to timeout.
|
||||
@export var request_timeout : float = 0.0
|
||||
@export var _send_request_action : bool = false :
|
||||
set(value):
|
||||
if value and Engine.is_editor_hint():
|
||||
request()
|
||||
# For Godot 4.4
|
||||
# @export_tool_button("Send Request") var _send_request_action = request
|
||||
|
||||
|
||||
@onready var _http_request : HTTPRequest = $HTTPRequest
|
||||
@onready var _timeout_timer : Timer= $TimeoutTimer
|
||||
|
||||
## State flag for whether the connection has timed out on the client-side.
|
||||
var timed_out : bool = false
|
||||
|
||||
func get_http_request() -> HTTPRequest:
|
||||
return _http_request
|
||||
|
||||
func get_api_key() -> String:
|
||||
if api_key_file.is_empty():
|
||||
return ""
|
||||
var file := FileAccess.open(api_key_file, FileAccess.READ)
|
||||
var error := FileAccess.get_open_error()
|
||||
if error != OK:
|
||||
push_error("API Key reading error: %d" % error)
|
||||
return ""
|
||||
var content = file.get_as_text()
|
||||
file.close()
|
||||
return content
|
||||
|
||||
func get_api_url() -> String:
|
||||
return api_url
|
||||
|
||||
func get_api_method() -> int:
|
||||
return request_method
|
||||
|
||||
func mock_empty_body() -> String:
|
||||
var form : Dictionary = {}
|
||||
return JSON.stringify(form)
|
||||
|
||||
func mock_request(body : String):
|
||||
await(get_tree().create_timer(10.0).timeout)
|
||||
_on_request_completed(HTTPRequest.RESULT_SUCCESS, "200", [], body)
|
||||
|
||||
func request(body : String = "", request_headers : Array = []) -> void:
|
||||
var local_http_request : HTTPRequest = get_http_request()
|
||||
var key : String = get_api_key()
|
||||
var url : String = get_api_url()
|
||||
var method : int = get_api_method()
|
||||
if url.is_empty():
|
||||
request_failed.emit(URL_NOT_SET)
|
||||
push_error(URL_NOT_SET)
|
||||
return
|
||||
request_headers.append("Content-Type: application/json")
|
||||
if key:
|
||||
request_headers.append("x-api-key: %s" % key)
|
||||
if request_timeout > 0.0:
|
||||
local_http_request.timeout = request_timeout
|
||||
var error = local_http_request.request(url, request_headers, method, body)
|
||||
if error != OK:
|
||||
request_failed.emit(REQUEST_FAILED)
|
||||
push_error("HTTP Request error: %d" % error)
|
||||
return
|
||||
if request_timeout > 0.0:
|
||||
_timeout_timer.start(request_timeout + 1.0)
|
||||
|
||||
func request_raw(data : PackedByteArray = [], request_headers : Array = []) -> void:
|
||||
var local_http_request : HTTPRequest = get_http_request()
|
||||
var key : String = get_api_key()
|
||||
var url : String = get_api_url()
|
||||
var method : int = get_api_method()
|
||||
if url.is_empty():
|
||||
request_failed.emit(URL_NOT_SET)
|
||||
push_error(URL_NOT_SET)
|
||||
return
|
||||
request_headers.append("Content-Type: application/json")
|
||||
if key:
|
||||
request_headers.append("x-api-key: %s" % key)
|
||||
if request_timeout > 0.0:
|
||||
local_http_request.timeout = request_timeout
|
||||
var error = local_http_request.request_raw(url, request_headers, method, data)
|
||||
if error != OK:
|
||||
request_failed.emit(REQUEST_FAILED)
|
||||
push_error("HTTP Request error: %d" % error)
|
||||
return
|
||||
if request_timeout > 0.0:
|
||||
_timeout_timer.start(request_timeout + 1.0)
|
||||
|
||||
func _on_request_completed(result, response_code, headers, body) -> void:
|
||||
# If already timed out on client-side, then return.
|
||||
if timed_out: return
|
||||
_timeout_timer.stop()
|
||||
if result == HTTPRequest.RESULT_SUCCESS:
|
||||
var body_string : String
|
||||
if body is PackedByteArray:
|
||||
body_string = body.get_string_from_utf8()
|
||||
elif body is String:
|
||||
body_string = body
|
||||
var json := JSON.new()
|
||||
var error = json.parse(body_string)
|
||||
if error != OK:
|
||||
request_failed.emit(PARSE_FAILED)
|
||||
push_error("Parse error: %d" % error)
|
||||
return
|
||||
var parsed_data = json.data
|
||||
response_received.emit(json.data)
|
||||
else:
|
||||
var error_message : String
|
||||
match(result):
|
||||
HTTPRequest.RESULT_CANT_CONNECT:
|
||||
error_message = RESULT_CANT_CONNECT
|
||||
HTTPRequest.RESULT_CANT_RESOLVE:
|
||||
error_message = RESULT_CANT_RESOLVE
|
||||
HTTPRequest.RESULT_CONNECTION_ERROR:
|
||||
error_message = RESULT_CONNECTION_ERROR
|
||||
HTTPRequest.RESULT_TIMEOUT:
|
||||
error_message = RESULT_TIMEOUT
|
||||
_:
|
||||
error_message = RESULT_SERVER_ERROR
|
||||
request_failed.emit(error_message)
|
||||
push_error("HTTP Result error: %d" % result)
|
||||
|
||||
func _on_http_request_request_completed(result, response_code, headers, body) -> void:
|
||||
_on_request_completed(result, response_code, headers, body)
|
||||
|
||||
func _on_timeout_timer_timeout() -> void:
|
||||
timed_out = true
|
||||
request_failed.emit(REQUEST_TIMEOUT)
|
||||
push_warning(REQUEST_TIMEOUT)
|
@ -0,0 +1 @@
|
||||
uid://s0j82xowl675
|
@ -0,0 +1,13 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://drhhakm62vjsy"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://s0j82xowl675" path="res://addons/maaacks_menus_template/base/scenes/utilities/api_client.gd" id="1_c5ofg"]
|
||||
|
||||
[node name="APIClient" type="Node"]
|
||||
script = ExtResource("1_c5ofg")
|
||||
|
||||
[node name="HTTPRequest" type="HTTPRequest" parent="."]
|
||||
|
||||
[node name="TimeoutTimer" type="Timer" parent="."]
|
||||
|
||||
[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"]
|
||||
[connection signal="timeout" from="TimeoutTimer" to="." method="_on_timeout_timer_timeout"]
|
@ -0,0 +1,285 @@
|
||||
@tool
|
||||
## Utility node for downloading and unzipping a file from a URL to an extraction destination.
|
||||
class_name DownloadAndExtract
|
||||
extends Node
|
||||
|
||||
## Sent when the run has completed.
|
||||
signal run_completed
|
||||
## Sent when a response is received from the server.
|
||||
signal response_received(response_body)
|
||||
## Sent when the run has failed or exited early for any reason.
|
||||
signal run_failed(error : String)
|
||||
## Sent when the zip file has finished saving.
|
||||
signal zip_saved
|
||||
|
||||
const TEMPORARY_ZIP_PATH = "res://temp.zip"
|
||||
const RESULT_CANT_CONNECT = "Failed to connect"
|
||||
const RESULT_CANT_RESOLVE = "Failed to resolve"
|
||||
const RESULT_CONNECTION_ERROR = "Connection error"
|
||||
const RESULT_TIMEOUT = "Connection timeout"
|
||||
const RESULT_SERVER_ERROR = "Server error"
|
||||
const REQUEST_FAILED = "Error in the request"
|
||||
const REQUEST_TIMEOUT = "Request timed out on the client side"
|
||||
const DOWNLOAD_IN_PROGRESS = "Download already in progress"
|
||||
const EXTRACT_IN_PROGRESS = "Extract already in progress"
|
||||
const DELETE_IN_PROGRESS = "Delete already in progress"
|
||||
const FAILED_TO_SAVE_ZIP_FILE = "Failed to save the zip file"
|
||||
const FAILED_TO_MAKE_EXTRACT_DIR = "Failed to make extract directory"
|
||||
const FAILED_TO_READ_ZIP_FILE = "Failed to read the zip file"
|
||||
const DOWNLOADED_ZIP_FILE_DOESNT_EXIST = "The downloaded ZIP file doesn't exist"
|
||||
const URL_NOT_SET = "URL parameter is not set"
|
||||
|
||||
enum Stage{
|
||||
NONE,
|
||||
DOWNLOAD,
|
||||
SAVE,
|
||||
EXTRACT,
|
||||
DELETE,
|
||||
}
|
||||
|
||||
## Location of the zip file to be downloaded.
|
||||
@export var zip_url : String
|
||||
## Path where the zipped files are to be extracted.
|
||||
@export_dir var extract_path : String
|
||||
@export_group("Advanced")
|
||||
## If not empty, zipped file paths that do not contain a match to the string will be ignored.
|
||||
@export var path_match_string : String = ""
|
||||
## Assuming zip file contains a single base directory, the flag copies all of the contents,
|
||||
## as if they were at the base of the zip file. It never makes the base directory locally.
|
||||
@export var skip_base_zip_dir : bool = false
|
||||
## Forces a download and extraction even if the files already exist.
|
||||
@export var force : bool = false
|
||||
## Path where the zip file will be stored.
|
||||
@export var zip_file_path : String = TEMPORARY_ZIP_PATH
|
||||
## Flag to delete a downloaded zip file after the contents are extracted.
|
||||
@export var delete_zip_file : bool = true
|
||||
## Ratio of processing time that should be spent on extracting files.
|
||||
@export_range(0.0, 1.0) var process_time_ratio : float = 0.75
|
||||
## Seconds of delay added between saving the zip file and extracting it.
|
||||
@export_range(0.0, 3.0) var extraction_delay : float = 0.25
|
||||
## Duration to wait before the request times out.
|
||||
@export var request_timeout : float = 0.0
|
||||
@export var _start_run_action : bool = false :
|
||||
set(value):
|
||||
if value and Engine.is_editor_hint():
|
||||
run()
|
||||
# For Godot 4.4
|
||||
# @export_tool_button("Download & Extract") var _start_run_action = run
|
||||
|
||||
|
||||
@onready var _http_request : HTTPRequest = $HTTPRequest
|
||||
@onready var _timeout_timer : Timer= $TimeoutTimer
|
||||
|
||||
## State flag for whether the connection has timed out on the client-side.
|
||||
var timed_out : bool = false
|
||||
## Current stage of the download and extract process.
|
||||
var stage : Stage = Stage.NONE
|
||||
var zip_reader : ZIPReader = ZIPReader.new()
|
||||
var zipped_file_paths : PackedStringArray = []
|
||||
var extracted_file_paths : Array[String] = []
|
||||
var skipped_file_paths : Array[String] = []
|
||||
var downloaded_zip_file : bool = false
|
||||
var base_zip_path : String = ""
|
||||
var _save_progress : float = 0.0
|
||||
|
||||
func get_http_request() -> HTTPRequest:
|
||||
return _http_request
|
||||
|
||||
func get_zip_url() -> String:
|
||||
return zip_url
|
||||
|
||||
func _zip_exists() -> bool:
|
||||
return FileAccess.file_exists(zip_file_path)
|
||||
|
||||
func get_request_method() -> int:
|
||||
return HTTPClient.METHOD_GET
|
||||
|
||||
## Sends the request to download the target zip file, and then extracts the contents.
|
||||
func run(request_headers : Array = []) -> void:
|
||||
if stage == Stage.DOWNLOAD:
|
||||
run_failed.emit(DOWNLOAD_IN_PROGRESS)
|
||||
push_warning(DOWNLOAD_IN_PROGRESS)
|
||||
return
|
||||
if _zip_exists() and not force:
|
||||
_extract_files.call_deferred()
|
||||
return
|
||||
var local_http_request : HTTPRequest = get_http_request()
|
||||
var url : String = get_zip_url()
|
||||
var method : int = get_request_method()
|
||||
if url.is_empty():
|
||||
run_failed.emit(URL_NOT_SET)
|
||||
push_error(URL_NOT_SET)
|
||||
return
|
||||
if request_timeout > 0.0:
|
||||
local_http_request.timeout = request_timeout
|
||||
var error = local_http_request.request(url, request_headers, method)
|
||||
if error != OK:
|
||||
run_failed.emit(REQUEST_FAILED)
|
||||
push_error("HTTP Request error: %d" % error)
|
||||
return
|
||||
if request_timeout > 0.0:
|
||||
_timeout_timer.start(request_timeout + 1.0)
|
||||
stage = Stage.DOWNLOAD
|
||||
|
||||
func _delete_zip_file() -> void:
|
||||
if not delete_zip_file or not downloaded_zip_file: return
|
||||
if stage == Stage.DELETE:
|
||||
run_failed.emit(DELETE_IN_PROGRESS)
|
||||
push_warning(DELETE_IN_PROGRESS)
|
||||
return
|
||||
stage = Stage.DELETE
|
||||
DirAccess.remove_absolute(zip_file_path)
|
||||
downloaded_zip_file = false
|
||||
|
||||
func _save_zip_file(body : PackedByteArray) -> void:
|
||||
stage = Stage.SAVE
|
||||
var file = FileAccess.open(zip_file_path, FileAccess.WRITE)
|
||||
if not file:
|
||||
run_failed.emit(FAILED_TO_SAVE_ZIP_FILE)
|
||||
push_error(FAILED_TO_SAVE_ZIP_FILE)
|
||||
return
|
||||
file.store_buffer(body)
|
||||
file.close()
|
||||
downloaded_zip_file = true
|
||||
zip_saved.emit()
|
||||
|
||||
func extract_path_exists() -> bool:
|
||||
return DirAccess.dir_exists_absolute(extract_path)
|
||||
|
||||
func _make_extract_path() -> void:
|
||||
var err := DirAccess.make_dir_recursive_absolute(extract_path)
|
||||
if err != OK:
|
||||
run_failed.emit(FAILED_TO_MAKE_EXTRACT_DIR)
|
||||
push_error(FAILED_TO_MAKE_EXTRACT_DIR)
|
||||
|
||||
func _extract_files() -> void:
|
||||
if stage == Stage.EXTRACT:
|
||||
run_failed.emit(EXTRACT_IN_PROGRESS)
|
||||
push_warning(EXTRACT_IN_PROGRESS)
|
||||
return
|
||||
stage = Stage.EXTRACT
|
||||
if not _zip_exists():
|
||||
run_failed.emit(DOWNLOADED_ZIP_FILE_DOESNT_EXIST)
|
||||
push_error(DOWNLOADED_ZIP_FILE_DOESNT_EXIST)
|
||||
return
|
||||
if not extract_path_exists(): _make_extract_path()
|
||||
var error = zip_reader.open(zip_file_path)
|
||||
if error != OK:
|
||||
run_failed.emit(FAILED_TO_READ_ZIP_FILE)
|
||||
push_error("ZIP Reader error: %d" % error)
|
||||
return
|
||||
zipped_file_paths = zip_reader.get_files()
|
||||
if skip_base_zip_dir:
|
||||
base_zip_path = zipped_file_paths[0]
|
||||
if not base_zip_path.ends_with("/"):
|
||||
push_warning("Skipping extracting base path, but it is not a directory.")
|
||||
zipped_file_paths.remove_at(0)
|
||||
|
||||
func _on_request_completed(result, response_code, headers, body) -> void:
|
||||
# If already timed out on client-side, then return.
|
||||
if timed_out: return
|
||||
_timeout_timer.stop()
|
||||
if _zip_exists(): _delete_zip_file()
|
||||
if result == HTTPRequest.RESULT_SUCCESS:
|
||||
if body is PackedByteArray:
|
||||
response_received.emit(body)
|
||||
_save_zip_file(body)
|
||||
_save_progress = 0.0
|
||||
var tween = create_tween()
|
||||
tween.tween_property(self, "_save_progress", 1.0, extraction_delay)
|
||||
await tween.finished
|
||||
_extract_files.call_deferred()
|
||||
else:
|
||||
var error_message : String
|
||||
match(result):
|
||||
HTTPRequest.RESULT_CANT_CONNECT:
|
||||
error_message = RESULT_CANT_CONNECT
|
||||
HTTPRequest.RESULT_CANT_RESOLVE:
|
||||
error_message = RESULT_CANT_RESOLVE
|
||||
HTTPRequest.RESULT_CONNECTION_ERROR:
|
||||
error_message = RESULT_CONNECTION_ERROR
|
||||
HTTPRequest.RESULT_TIMEOUT:
|
||||
error_message = RESULT_TIMEOUT
|
||||
_:
|
||||
error_message = RESULT_SERVER_ERROR
|
||||
run_failed.emit(error_message)
|
||||
push_error("HTTP Result error: %d" % result)
|
||||
|
||||
func _on_http_request_request_completed(result, response_code, headers, body) -> void:
|
||||
_on_request_completed(result, response_code, headers, body)
|
||||
|
||||
func _on_timeout_timer_timeout() -> void:
|
||||
timed_out = true
|
||||
run_failed.emit(REQUEST_TIMEOUT)
|
||||
push_warning(REQUEST_TIMEOUT)
|
||||
|
||||
func get_progress() -> float:
|
||||
if stage == Stage.DOWNLOAD:
|
||||
return get_download_progress()
|
||||
elif stage == Stage.SAVE:
|
||||
return get_save_progress()
|
||||
elif stage == Stage.EXTRACT:
|
||||
return get_extraction_progress()
|
||||
return 0.0
|
||||
|
||||
func get_save_progress() -> float:
|
||||
return _save_progress
|
||||
|
||||
func get_extraction_progress() -> float:
|
||||
if zipped_file_paths.size() == 0:
|
||||
return 0.0
|
||||
return float(extracted_file_paths.size()) / float(zipped_file_paths.size())
|
||||
|
||||
func get_download_progress() -> float:
|
||||
var body_size := _http_request.get_body_size()
|
||||
if body_size < 1: return 0.0
|
||||
return float(_http_request.get_downloaded_bytes()) / float(body_size)
|
||||
|
||||
func _zipped_files_remaining() -> int:
|
||||
return zipped_file_paths.size() - (extracted_file_paths.size() + skipped_file_paths.size())
|
||||
|
||||
func _extract_next_zipped_file() -> void:
|
||||
var path_index = extracted_file_paths.size() + skipped_file_paths.size()
|
||||
var zipped_file_path := zipped_file_paths.get(path_index)
|
||||
if path_match_string and not zipped_file_path.contains(path_match_string):
|
||||
skipped_file_paths.append(zipped_file_path)
|
||||
return
|
||||
var extract_path_dir := extract_path
|
||||
if not extract_path_dir.ends_with("/"):
|
||||
extract_path_dir += "/"
|
||||
var full_path := extract_path_dir
|
||||
if skip_base_zip_dir:
|
||||
full_path += zipped_file_path.replace(base_zip_path, "")
|
||||
else:
|
||||
full_path += zipped_file_path
|
||||
if full_path.ends_with("/"):
|
||||
if not DirAccess.dir_exists_absolute(full_path):
|
||||
DirAccess.make_dir_recursive_absolute(full_path)
|
||||
else:
|
||||
if not FileAccess.file_exists(full_path) or force:
|
||||
var file_access := FileAccess.open(full_path, FileAccess.WRITE)
|
||||
if file_access == null:
|
||||
skipped_file_paths.append(zipped_file_path)
|
||||
push_error("Failed to open file: %s" % full_path)
|
||||
return
|
||||
var file_contents = zip_reader.read_file(zipped_file_path)
|
||||
file_access.store_buffer(file_contents)
|
||||
file_access.close()
|
||||
extracted_file_paths.append(full_path)
|
||||
|
||||
func _finish_extraction() -> void:
|
||||
zip_reader.close()
|
||||
_delete_zip_file()
|
||||
stage = Stage.NONE
|
||||
run_completed.emit()
|
||||
|
||||
func _process(delta : float) -> void:
|
||||
if stage == Stage.EXTRACT:
|
||||
var frame_start_time : float = Time.get_unix_time_from_system()
|
||||
var frame_time : float = 0.0
|
||||
while (frame_time < delta * process_time_ratio):
|
||||
if _zipped_files_remaining() == 0:
|
||||
_finish_extraction()
|
||||
break
|
||||
_extract_next_zipped_file()
|
||||
frame_time = Time.get_unix_time_from_system() - frame_start_time
|
@ -0,0 +1 @@
|
||||
uid://bqu3bc0tttrfk
|
@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dlkmofxhavh10"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bqu3bc0tttrfk" path="res://addons/maaacks_menus_template/base/scenes/utilities/download_and_extract.gd" id="1_1few7"]
|
||||
|
||||
[node name="DownloadAndExtract" type="Node"]
|
||||
script = ExtResource("1_1few7")
|
||||
|
||||
[node name="HTTPRequest" type="HTTPRequest" parent="."]
|
||||
|
||||
[node name="TimeoutTimer" type="Timer" parent="."]
|
||||
one_shot = true
|
||||
|
||||
[connection signal="request_completed" from="HTTPRequest" to="." method="_on_http_request_request_completed"]
|
||||
[connection signal="timeout" from="TimeoutTimer" to="." method="_on_timeout_timer_timeout"]
|
Reference in New Issue
Block a user