363 lines
11 KiB
GDScript
363 lines
11 KiB
GDScript
## @meta-authors SIsilicon
|
|
## @meta-version 2.2
|
|
## The Storage API for Firebase.
|
|
## This object handles all firebase storage tasks, variables and references. To use this API, you must first create a [StorageReference] with [method ref]. With the reference, you can then query and manipulate the file or folder in the cloud storage.
|
|
##
|
|
## [i]Note: In HTML builds, you must configure [url=https://firebase.google.com/docs/storage/web/download-files#cors_configuration]CORS[/url] in your storage bucket.[i]
|
|
@tool
|
|
class_name FirebaseStorage
|
|
extends Node
|
|
|
|
const _API_VERSION : String = "v0"
|
|
|
|
## @arg-types int, int, PackedStringArray
|
|
## @arg-enums HTTPRequest.Result, HTTPClient.ResponseCode
|
|
## Emitted when a [StorageTask] has finished with an error.
|
|
signal task_failed(result, response_code, data)
|
|
|
|
## The current storage bucket the Storage API is referencing.
|
|
var bucket : String
|
|
|
|
## @default false
|
|
## Whether a task is currently being processed.
|
|
var requesting : bool = false
|
|
|
|
var _auth : Dictionary
|
|
var _config : Dictionary
|
|
|
|
var _references : Dictionary = {}
|
|
|
|
var _base_url : String = ""
|
|
var _extended_url : String = "/[API_VERSION]/b/[APP_ID]/o/[FILE_PATH]"
|
|
var _root_ref : StorageReference
|
|
|
|
var _http_client : HTTPClient = HTTPClient.new()
|
|
var _pending_tasks : Array = []
|
|
|
|
var _current_task : StorageTask
|
|
var _response_code : int
|
|
var _response_headers : PackedStringArray
|
|
var _response_data : PackedByteArray
|
|
var _content_length : int
|
|
var _reading_body : bool
|
|
|
|
func _notification(what : int) -> void:
|
|
if what == NOTIFICATION_INTERNAL_PROCESS:
|
|
_internal_process(get_process_delta_time())
|
|
|
|
func _internal_process(_delta : float) -> void:
|
|
if not requesting:
|
|
set_process_internal(false)
|
|
return
|
|
|
|
var task = _current_task
|
|
|
|
match _http_client.get_status():
|
|
HTTPClient.STATUS_DISCONNECTED:
|
|
_http_client.connect_to_host(_base_url, 443, TLSOptions.client()) # Uhh, check if this is going to work. I assume not.
|
|
|
|
HTTPClient.STATUS_RESOLVING, \
|
|
HTTPClient.STATUS_REQUESTING, \
|
|
HTTPClient.STATUS_CONNECTING:
|
|
_http_client.poll()
|
|
|
|
HTTPClient.STATUS_CONNECTED:
|
|
var err := _http_client.request_raw(task._method, task._url, task._headers, task.data)
|
|
if err:
|
|
_finish_request(HTTPRequest.RESULT_CONNECTION_ERROR)
|
|
|
|
HTTPClient.STATUS_BODY:
|
|
if _http_client.has_response() or _reading_body:
|
|
_reading_body = true
|
|
|
|
# If there is a response...
|
|
if _response_headers.is_empty():
|
|
_response_headers = _http_client.get_response_headers() # Get response headers.
|
|
_response_code = _http_client.get_response_code()
|
|
|
|
for header in _response_headers:
|
|
if "Content-Length" in header:
|
|
_content_length = header.trim_prefix("Content-Length: ").to_int()
|
|
break
|
|
|
|
_http_client.poll()
|
|
var chunk = _http_client.read_response_body_chunk() # Get a chunk.
|
|
if chunk.size() == 0:
|
|
# Got nothing, wait for buffers to fill a bit.
|
|
pass
|
|
else:
|
|
_response_data += chunk # Append to read buffer.
|
|
if _content_length != 0:
|
|
task.progress = float(_response_data.size()) / _content_length
|
|
|
|
if _http_client.get_status() != HTTPClient.STATUS_BODY:
|
|
task.progress = 1.0
|
|
_finish_request(HTTPRequest.RESULT_SUCCESS)
|
|
else:
|
|
task.progress = 1.0
|
|
_finish_request(HTTPRequest.RESULT_SUCCESS)
|
|
|
|
HTTPClient.STATUS_CANT_CONNECT:
|
|
_finish_request(HTTPRequest.RESULT_CANT_CONNECT)
|
|
HTTPClient.STATUS_CANT_RESOLVE:
|
|
_finish_request(HTTPRequest.RESULT_CANT_RESOLVE)
|
|
HTTPClient.STATUS_CONNECTION_ERROR:
|
|
_finish_request(HTTPRequest.RESULT_CONNECTION_ERROR)
|
|
HTTPClient.STATUS_TLS_HANDSHAKE_ERROR:
|
|
_finish_request(HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR)
|
|
|
|
## @args path
|
|
## @arg-defaults ""
|
|
## @return StorageReference
|
|
## Returns a reference to a file or folder in the storage bucket. It's this reference that should be used to control the file/folder checked the server end.
|
|
func ref(path := "") -> StorageReference:
|
|
if _config == null or _config.is_empty():
|
|
return null
|
|
|
|
# Create a root storage reference if there's none
|
|
# and we're not making one.
|
|
if path != "" and not _root_ref:
|
|
_root_ref = ref()
|
|
|
|
path = _simplify_path(path)
|
|
if not _references.has(path):
|
|
var ref := StorageReference.new()
|
|
_references[path] = ref
|
|
ref.bucket = bucket
|
|
ref.full_path = path
|
|
ref.file_name = path.get_file()
|
|
ref.parent = ref(path.path_join(".."))
|
|
ref.root = _root_ref
|
|
ref.storage = self
|
|
add_child(ref)
|
|
return ref
|
|
else:
|
|
return _references[path]
|
|
|
|
func _set_config(config_json : Dictionary) -> void:
|
|
_config = config_json
|
|
if bucket != _config.storageBucket:
|
|
bucket = _config.storageBucket
|
|
_http_client.close()
|
|
_check_emulating()
|
|
|
|
|
|
func _check_emulating() -> void :
|
|
## Check emulating
|
|
if not Firebase.emulating:
|
|
_base_url = "https://firebasestorage.googleapis.com"
|
|
else:
|
|
var port : String = _config.emulators.ports.storage
|
|
if port == "":
|
|
Firebase._printerr("You are in 'emulated' mode, but the port for Storage has not been configured.")
|
|
else:
|
|
_base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port })
|
|
|
|
|
|
func _upload(data : PackedByteArray, headers : PackedStringArray, ref : StorageReference, meta_only : bool) -> Variant:
|
|
if _is_invalid_authentication():
|
|
Firebase._printerr("Error uploading to storage: Invalid authentication")
|
|
return 0
|
|
|
|
var task := StorageTask.new()
|
|
task.ref = ref
|
|
task._url = _get_file_url(ref)
|
|
task.action = StorageTask.Task.TASK_UPLOAD_META if meta_only else StorageTask.Task.TASK_UPLOAD
|
|
task._headers = headers
|
|
task.data = data
|
|
_process_request(task)
|
|
return await task.task_finished
|
|
|
|
func _download(ref : StorageReference, meta_only : bool, url_only : bool) -> Variant:
|
|
if _is_invalid_authentication():
|
|
Firebase._printerr("Error downloading from storage: Invalid authentication")
|
|
return 0
|
|
|
|
var info_task := StorageTask.new()
|
|
info_task.ref = ref
|
|
info_task._url = _get_file_url(ref)
|
|
info_task.action = StorageTask.Task.TASK_DOWNLOAD_URL if url_only else StorageTask.Task.TASK_DOWNLOAD_META
|
|
_process_request(info_task)
|
|
|
|
if url_only or meta_only:
|
|
return await info_task.task_finished
|
|
|
|
var task := StorageTask.new()
|
|
task.ref = ref
|
|
task._url = _get_file_url(ref) + "?alt=media&token="
|
|
task.action = StorageTask.Task.TASK_DOWNLOAD
|
|
_pending_tasks.append(task)
|
|
|
|
var data = await info_task.task_finished
|
|
if info_task.result == OK:
|
|
task._url += info_task.data.downloadTokens
|
|
else:
|
|
task.data = info_task.data
|
|
task.response_headers = info_task.response_headers
|
|
task.response_code = info_task.response_code
|
|
task.result = info_task.result
|
|
task.finished = true
|
|
task.task_finished.emit(null)
|
|
task_failed.emit(task.result, task.response_code, task.data)
|
|
_pending_tasks.erase(task)
|
|
return null
|
|
|
|
return await task.task_finished
|
|
|
|
func _list(ref : StorageReference, list_all : bool) -> Array:
|
|
if _is_invalid_authentication():
|
|
Firebase._printerr("Error getting object list from storage: Invalid authentication")
|
|
return []
|
|
|
|
var task := StorageTask.new()
|
|
task.ref = ref
|
|
task._url = _get_file_url(_root_ref).trim_suffix("/")
|
|
task.action = StorageTask.Task.TASK_LIST_ALL if list_all else StorageTask.Task.TASK_LIST
|
|
_process_request(task)
|
|
return await task.task_finished
|
|
|
|
func _delete(ref : StorageReference) -> bool:
|
|
if _is_invalid_authentication():
|
|
Firebase._printerr("Error deleting object from storage: Invalid authentication")
|
|
return false
|
|
|
|
var task := StorageTask.new()
|
|
task.ref = ref
|
|
task._url = _get_file_url(ref)
|
|
task.action = StorageTask.Task.TASK_DELETE
|
|
_process_request(task)
|
|
var data = await task.task_finished
|
|
|
|
return data == null
|
|
|
|
func _process_request(task : StorageTask) -> void:
|
|
if requesting:
|
|
_pending_tasks.append(task)
|
|
return
|
|
requesting = true
|
|
|
|
var headers = Array(task._headers)
|
|
headers.append("Authorization: Bearer " + _auth.idtoken)
|
|
task._headers = PackedStringArray(headers)
|
|
|
|
_current_task = task
|
|
_response_code = 0
|
|
_response_headers = PackedStringArray()
|
|
_response_data = PackedByteArray()
|
|
_content_length = 0
|
|
_reading_body = false
|
|
|
|
if not _http_client.get_status() in [HTTPClient.STATUS_CONNECTED, HTTPClient.STATUS_DISCONNECTED]:
|
|
_http_client.close()
|
|
set_process_internal(true)
|
|
|
|
func _finish_request(result : int) -> void:
|
|
var task := _current_task
|
|
requesting = false
|
|
|
|
task.result = result
|
|
task.response_code = _response_code
|
|
task.response_headers = _response_headers
|
|
|
|
match task.action:
|
|
StorageTask.Task.TASK_DOWNLOAD:
|
|
task.data = _response_data
|
|
|
|
StorageTask.Task.TASK_DELETE:
|
|
_references.erase(task.ref.full_path)
|
|
for child in get_children():
|
|
if child.full_path == task.ref.full_path:
|
|
child.queue_free()
|
|
break
|
|
if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY:
|
|
task.data = null
|
|
|
|
StorageTask.Task.TASK_DOWNLOAD_URL:
|
|
var json = Utilities.get_json_data(_response_data)
|
|
if json != null and json.has("error"):
|
|
Firebase._printerr("Error getting object download url: "+json["error"].message)
|
|
if json != null and json.has("downloadTokens"):
|
|
task.data = _base_url + _get_file_url(task.ref) + "?alt=media&token=" + json.downloadTokens
|
|
else:
|
|
task.data = ""
|
|
|
|
StorageTask.Task.TASK_LIST, StorageTask.Task.TASK_LIST_ALL:
|
|
var json = Utilities.get_json_data(_response_data)
|
|
var items := []
|
|
if json != null and json.has("error"):
|
|
Firebase._printerr("Error getting data from storage: "+json["error"].message)
|
|
if json != null and json.has("items"):
|
|
for item in json.items:
|
|
var item_name : String = item.name
|
|
if item.bucket != bucket:
|
|
continue
|
|
if not item_name.begins_with(task.ref.full_path):
|
|
continue
|
|
if task.action == StorageTask.Task.TASK_LIST:
|
|
var dir_path : Array = item_name.split("/")
|
|
var slash_count : int = task.ref.full_path.count("/")
|
|
item_name = ""
|
|
for i in slash_count + 1:
|
|
item_name += dir_path[i]
|
|
if i != slash_count and slash_count != 0:
|
|
item_name += "/"
|
|
if item_name in items:
|
|
continue
|
|
|
|
items.append(item_name)
|
|
task.data = items
|
|
|
|
_:
|
|
var json = Utilities.get_json_data(_response_data)
|
|
task.data = json
|
|
|
|
var next_task = _get_next_pending_task()
|
|
|
|
task.finished = true
|
|
task.task_finished.emit(task.data) # I believe this parameter has been missing all along, but not sure. Caused weird results at times with a yield/await returning null, but the task containing data.
|
|
if typeof(task.data) == TYPE_DICTIONARY and task.data.has("error"):
|
|
task_failed.emit(task.result, task.response_code, task.data)
|
|
|
|
if next_task and not next_task.finished:
|
|
_process_request(next_task)
|
|
|
|
func _get_next_pending_task() -> StorageTask:
|
|
if _pending_tasks.is_empty():
|
|
return null
|
|
|
|
return _pending_tasks.pop_front()
|
|
|
|
func _get_file_url(ref : StorageReference) -> String:
|
|
var url := _extended_url.replace("[APP_ID]", ref.bucket)
|
|
url = url.replace("[API_VERSION]", _API_VERSION)
|
|
return url.replace("[FILE_PATH]", ref.full_path.uri_encode())
|
|
|
|
# Removes any "../" or "./" in the file path.
|
|
func _simplify_path(path : String) -> String:
|
|
var dirs := path.split("/")
|
|
var new_dirs := []
|
|
for dir in dirs:
|
|
if dir == "..":
|
|
new_dirs.pop_back()
|
|
elif dir == ".":
|
|
pass
|
|
else:
|
|
new_dirs.push_back(dir)
|
|
|
|
var new_path := "/".join(PackedStringArray(new_dirs))
|
|
new_path = new_path.replace("//", "/")
|
|
new_path = new_path.replace("\\", "/")
|
|
return new_path
|
|
|
|
func _on_FirebaseAuth_login_succeeded(auth_token : Dictionary) -> void:
|
|
_auth = auth_token
|
|
|
|
func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void:
|
|
_auth = auth_result
|
|
|
|
func _on_FirebaseAuth_logout() -> void:
|
|
_auth = {}
|
|
|
|
func _is_invalid_authentication() -> bool:
|
|
return (_config == null or _config.is_empty()) or (_auth == null or _auth.is_empty())
|