added firebase and rudimentary leaderboard support

This commit is contained in:
derek
2025-04-09 11:19:02 -05:00
parent ce08df66e6
commit 25eb9e725a
121 changed files with 4987 additions and 4 deletions

View File

@@ -0,0 +1,362 @@
## @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())

View File

@@ -0,0 +1 @@
uid://vxab4cp6e8hx

View File

@@ -0,0 +1,159 @@
## @meta-authors SIsilicon
## @meta-version 2.2
## A reference to a file or folder in the Firebase cloud storage.
## This object is used to interact with the cloud storage. You may get data from the server, as well as upload your own back to it.
@tool
class_name StorageReference
extends Node
## The default MIME type to use when uploading a file.
## Data sent with this type are interpreted as plain binary data. Note that firebase will generate an MIME type based checked the file extenstion if none is provided.
const DEFAULT_MIME_TYPE = "application/octet-stream"
## A dictionary of common MIME types based checked a file extension.
## Example: [code]MIME_TYPES.png[/code] will return [code]image/png[/code].
const MIME_TYPES = {
"bmp": "image/bmp",
"css": "text/css",
"csv": "text/csv",
"gd": "text/plain",
"htm": "text/html",
"html": "text/html",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"json": "application/json",
"mp3": "audio/mpeg",
"mpeg": "video/mpeg",
"ogg": "audio/ogg",
"ogv": "video/ogg",
"png": "image/png",
"shader": "text/plain",
"svg": "image/svg+xml",
"tif": "image/tiff",
"tiff": "image/tiff",
"tres": "text/plain",
"tscn": "text/plain",
"txt": "text/plain",
"wav": "audio/wav",
"webm": "video/webm",
"webp": "image/webp",
"xml": "text/xml",
}
## @default ""
## The stroage bucket this referenced file/folder is located in.
var bucket : String = ""
## @default ""
## The path to the file/folder relative to [member bucket].
var full_path : String = ""
## @default ""
## The name of the file/folder, including any file extension.
## Example: If the [member full_path] is [code]images/user/image.png[/code], then the [member name] would be [code]image.png[/code].
var file_name : String = ""
## The parent [StorageReference] one level up the file hierarchy.
## If the current [StorageReference] is the root (i.e. the [member full_path] is [code]""[/code]) then the [member parent] will be [code]null[/code].
var parent : StorageReference
## The root [StorageReference].
var root : StorageReference
## @type FirebaseStorage
## The Storage API that created this [StorageReference] to begin with.
var storage # FirebaseStorage (Can't static type due to cyclic reference)
## @args path
## @return StorageReference
## Returns a reference to another [StorageReference] relative to this one.
func child(path : String) -> StorageReference:
return storage.ref(full_path.path_join(path))
## @args data, metadata
## @return int
## Makes an attempt to upload data to the referenced file location. Returns Variant
func put_data(data : PackedByteArray, metadata := {}) -> Variant:
if not "Content-Length" in metadata and not Utilities.is_web():
metadata["Content-Length"] = data.size()
var headers := []
for key in metadata:
headers.append("%s: %s" % [key, metadata[key]])
return await storage._upload(data, headers, self, false)
## @args data, metadata
## @return int
## Like [method put_data], but [code]data[/code] is a [String].
func put_string(data : String, metadata := {}) -> Variant:
return await put_data(data.to_utf8_buffer(), metadata)
## @args file_path, metadata
## @return int
## Like [method put_data], but the data comes from a file at [code]file_path[/code].
func put_file(file_path : String, metadata := {}) -> Variant:
var file := FileAccess.open(file_path, FileAccess.READ)
var data := file.get_buffer(file.get_length())
if "Content-Type" in metadata:
metadata["Content-Type"] = MIME_TYPES.get(file_path.get_extension(), DEFAULT_MIME_TYPE)
return await put_data(data, metadata)
## @return Variant
## Makes an attempt to download the files from the referenced file location. Status checked this task is found in the returned [StorageTask].
func get_data() -> Variant:
var result = await storage._download(self, false, false)
return result
## @return StorageTask
## Like [method get_data], but the data in the returned [StorageTask] comes in the form of a [String].
func get_string() -> String:
var task := await get_data()
_on_task_finished(task, "stringify")
return task.data
## @return StorageTask
## Attempts to get the download url that points to the referenced file's data. Using the url directly may require an authentication header. Status checked this task is found in the returned [StorageTask].
func get_download_url() -> Variant:
return await storage._download(self, false, true)
## @return StorageTask
## Attempts to get the metadata of the referenced file. Status checked this task is found in the returned [StorageTask].
func get_metadata() -> Variant:
return await storage._download(self, true, false)
## @args metadata
## @return StorageTask
## Attempts to update the metadata of the referenced file. Any field with a value of [code]null[/code] will be deleted checked the server end. Status checked this task is found in the returned [StorageTask].
func update_metadata(metadata : Dictionary) -> Variant:
var data := JSON.stringify(metadata).to_utf8_buffer()
var headers := PackedStringArray(["Accept: application/json"])
return await storage._upload(data, headers, self, true)
## @return StorageTask
## Attempts to get the list of files and/or folders under the referenced folder This function is not nested unlike [method list_all]. Status checked this task is found in the returned [StorageTask].
func list() -> Array:
return await storage._list(self, false)
## @return StorageTask
## Attempts to get the list of files and/or folders under the referenced folder This function is nested unlike [method list]. Status checked this task is found in the returned [StorageTask].
func list_all() -> Array:
return await storage._list(self, true)
## @return StorageTask
## Attempts to delete the referenced file/folder. If successful, the reference will become invalid And can no longer be used. If you need to reference this location again, make a new reference with [method StorageTask.ref]. Status checked this task is found in the returned [StorageTask].
func delete() -> bool:
return await storage._delete(self)
func _to_string() -> String:
var string := "gs://%s/%s" % [bucket, full_path]
return string
func _on_task_finished(task : StorageTask, action : String) -> void:
match action:
"stringify":
if typeof(task.data) == TYPE_PACKED_BYTE_ARRAY:
task.data = task.data.get_string_from_utf8()

View File

@@ -0,0 +1 @@
uid://bpvs4eelnej5i

View File

@@ -0,0 +1,74 @@
## @meta-authors SIsilicon, Kyle 'backat50ft' Szklenski
## @meta-version 2.2
## An object that keeps track of an operation performed by [StorageReference].
@tool
class_name StorageTask
extends RefCounted
enum Task {
TASK_UPLOAD,
TASK_UPLOAD_META,
TASK_DOWNLOAD,
TASK_DOWNLOAD_META,
TASK_DOWNLOAD_URL,
TASK_LIST,
TASK_LIST_ALL,
TASK_DELETE,
TASK_MAX ## The number of [enum Task] constants.
}
## Emitted when the task is finished. Returns data depending checked the success and action of the task.
signal task_finished(data)
## Boolean to determine if this request involves metadata only
var is_meta : bool
## @enum Task
## @default -1
## @setter set_action
## The kind of operation this [StorageTask] is keeping track of.
var action : int = -1 : set = set_action
var ref # Should not be needed, damnit
## @default PackedByteArray()
## Data that the tracked task will/has returned.
var data = PackedByteArray() # data can be of any type.
## @default 0.0
## The percentage of data that has been received.
var progress : float = 0.0
## @default -1
## @enum HTTPRequest.Result
## The resulting status of the task. Anyting other than [constant HTTPRequest.RESULT_SUCCESS] means an error has occured.
var result : int = -1
## @default false
## Whether the task is finished processing.
var finished : bool = false
## @default PackedStringArray()
## The returned HTTP response headers.
var response_headers := PackedStringArray()
## @default 0
## @enum HTTPClient.ResponseCode
## The returned HTTP response code.
var response_code : int = 0
var _method : int = -1
var _url : String = ""
var _headers : PackedStringArray = PackedStringArray()
func set_action(value : int) -> void:
action = value
match action:
Task.TASK_UPLOAD:
_method = HTTPClient.METHOD_POST
Task.TASK_UPLOAD_META:
_method = HTTPClient.METHOD_PATCH
Task.TASK_DELETE:
_method = HTTPClient.METHOD_DELETE
_:
_method = HTTPClient.METHOD_GET

View File

@@ -0,0 +1 @@
uid://bfw61qsheaqjr