class_name FileCache extends RefCounted ## Downloaded files kept in a folder next to the settings, with an [code]index.json[/code] ## that records for each file when the server last changed it and when it was ## downloaded: ## [codeblock] ## {"Crono.png": {"modified": 1730000000, "downloaded": 1730100000}} ## [/codeblock] ## Used by [CharacterCache] and [CoverCache], which decide when a file is fresh. const INDEX_FILE_NAME: String = "index.json" ## The change time of a file when the server can't tell it. const UNKNOWN_TIME: int = -1 ## The folder's name in [method Settings.config_dir]. var folder_name: String # What index.json holds: key -> {"modified": int, "downloaded": int} var _index: Dictionary = {} func _init(new_folder_name: String) -> void: folder_name = new_folder_name ## The folder the files are kept in. func folder() -> String: return Settings.config_dir().path_join(folder_name) ## Reads [code]index.json[/code]. A missing or broken index starts empty. func load_index() -> void: _index = {} var path: String = folder().path_join(INDEX_FILE_NAME) if FileAccess.file_exists(path): var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(path)) if parsed is Dictionary: _index = parsed ## The keys of the files in the index. func keys() -> Array: return _index.keys() ## Whether the file for [param key] is in the index and on disk. func has_file(key: String) -> bool: return _index.has(key) && FileAccess.file_exists(file_path(key)) ## When the server last changed the file for [param key], as recorded when it was ## downloaded, or [constant UNKNOWN_TIME]. func modified(key: String) -> int: return int(_index.get(key, {}).get("modified", UNKNOWN_TIME)) ## How many seconds ago the file for [param key] was downloaded. func age(key: String) -> float: return Time.get_unix_time_from_system() - float(_index.get(key, {}).get("downloaded", 0)) ## The file for [param key]. func read(key: String) -> PackedByteArray: return FileAccess.get_file_as_bytes(file_path(key)) ## Writes [param bytes] as the file for [param key], last changed on the server at ## [param modified_time], and saves the index. Returns false if it could not be written. func store(key: String, bytes: PackedByteArray, modified_time: int) -> bool: DirAccess.make_dir_recursive_absolute(folder()) var file: FileAccess = FileAccess.open(file_path(key), FileAccess.WRITE) if file == null: GameLog.warning("Could not cache ", key, " in ", folder_name, ": ", error_string(FileAccess.get_open_error())) return false file.store_buffer(bytes) file.close() _index[key] = { "modified": modified_time, "downloaded": int(Time.get_unix_time_from_system()), } save_index() return true ## Deletes the file for [param key] and its entry, and saves the index. func remove(key: String) -> void: DirAccess.remove_absolute(file_path(key)) if _index.erase(key): save_index() ## Deletes every file whose key is not in [param kept], e.g. files of things the server ## no longer has. func keep_only(kept: Dictionary) -> void: var removed: bool = false for key: String in _index.keys(): if !kept.has(key): DirAccess.remove_absolute(file_path(key)) _index.erase(key) removed = true if removed: save_index() ## Deletes every file in the folder, the index too, e.g. to free the space they take. func clear() -> void: _index = {} if !DirAccess.dir_exists_absolute(folder()): return for file_name: String in DirAccess.get_files_at(folder()): DirAccess.remove_absolute(folder().path_join(file_name)) ## How many bytes the files in [param dir] take, leaving out the paths in [param skip] ## and any subfolders. 0 when the folder doesn't exist. static func folder_size(dir: String, skip: PackedStringArray = []) -> int: if !DirAccess.dir_exists_absolute(dir): return 0 var total: int = 0 for file_name: String in DirAccess.get_files_at(dir): var path: String = dir.path_join(file_name) var file: FileAccess = FileAccess.open(path, FileAccess.READ) if file != null && !skip.has(path): total += file.get_length() return total func save_index() -> void: DirAccess.make_dir_recursive_absolute(folder()) var file: FileAccess = FileAccess.open(folder().path_join(INDEX_FILE_NAME), FileAccess.WRITE) if file != null: file.store_string(JSON.stringify(_index, "\t", true)) ## Where the file for [param key] is. Keys come from the server, so they are made safe ## as file names. func file_path(key: String) -> String: return folder().path_join(key.validate_filename())