91 lines
3.1 KiB
GDScript
91 lines
3.1 KiB
GDScript
@tool
|
|
extends Node
|
|
class_name CghDownloader
|
|
## Threaded file download with polled progress (Godot has no progress signal).
|
|
## Emits progress every frame while a download is in flight.
|
|
|
|
signal progress(pct: float, downloaded: int, total: int)
|
|
signal finished(path: String)
|
|
signal failed(message: String)
|
|
|
|
var _http: HTTPRequest
|
|
var _active := false
|
|
var _dest := ""
|
|
var _last_got := 0 # bytes seen last frame — to detect a dead/stalled connection
|
|
var _stall := 0.0 # seconds with zero new bytes
|
|
const STALL_LIMIT := 30.0 # a hung download must not block every later one forever
|
|
|
|
func _ready() -> void:
|
|
_http = HTTPRequest.new()
|
|
_http.use_threads = true
|
|
add_child(_http)
|
|
_http.request_completed.connect(_on_done)
|
|
|
|
func download(url: String, filename: String, headers := PackedStringArray()) -> void:
|
|
if _active:
|
|
failed.emit("A download is already in progress.")
|
|
return
|
|
CghConfig.ensure_dirs()
|
|
# Save into the SHARED CGHEVEN folder so AE/Blender/etc. see it too (file scanner).
|
|
var dir := CghConfig.primary_download_dir()
|
|
DirAccess.make_dir_recursive_absolute(dir)
|
|
_dest = dir.path_join(CghConfig.safe_download_name(filename))
|
|
# Clear any orphaned/stuck request first (mirrors api_client) so a previously hung
|
|
# download that left the node "processing" can't block this new one from starting.
|
|
_http.cancel_request()
|
|
_http.download_file = _dest
|
|
var err := _http.request(url, headers, HTTPClient.METHOD_GET)
|
|
if err != OK:
|
|
failed.emit("Download could not start (err %d)" % err)
|
|
return
|
|
_active = true
|
|
_last_got = 0
|
|
_stall = 0.0
|
|
set_process(true)
|
|
|
|
func _process(dt: float) -> void:
|
|
if not _active:
|
|
set_process(false)
|
|
return
|
|
var got := _http.get_downloaded_bytes()
|
|
var total := _http.get_body_size() # -1 on chunked / no Content-Length
|
|
# Dead-connection guard: if no new bytes arrive for STALL_LIMIT seconds, give up so
|
|
# the download queue isn't blocked forever (a hung request never fires _on_done).
|
|
if got > _last_got:
|
|
_last_got = got
|
|
_stall = 0.0
|
|
else:
|
|
_stall += dt
|
|
if _stall >= STALL_LIMIT:
|
|
_http.cancel_request()
|
|
_active = false
|
|
set_process(false)
|
|
failed.emit("Download stalled (no data for %ds) — check your connection and retry." % int(STALL_LIMIT))
|
|
return
|
|
if total > 0:
|
|
progress.emit(float(got) / float(total) * 100.0, got, total)
|
|
else:
|
|
progress.emit(-1.0, got, -1) # indeterminate -> spinner
|
|
|
|
func cancel() -> void:
|
|
if _active:
|
|
_http.cancel_request()
|
|
_active = false
|
|
set_process(false)
|
|
|
|
func _on_done(result: int, code: int, _h: PackedStringArray, _b: PackedByteArray) -> void:
|
|
_active = false
|
|
set_process(false)
|
|
if result != HTTPRequest.RESULT_SUCCESS or code < 200 or code >= 300:
|
|
var hint := ""
|
|
if code == 401 or code == 403:
|
|
hint = " — access denied (file link may be expired/invalid on the server)"
|
|
elif code == 404:
|
|
hint = " — file not found on the server"
|
|
elif result != HTTPRequest.RESULT_SUCCESS:
|
|
hint = " — network/connection issue"
|
|
failed.emit("Download failed for %s%s [result %d, HTTP %d]" % [_dest.get_file(), hint, result, code])
|
|
return
|
|
CghDownloads.mark_saved(_dest.get_file())
|
|
finished.emit(_dest)
|