yugioh-binder/src/utils.rs

183 lines
5.2 KiB
Rust

use gloo::net::http;
use thiserror::Error;
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::{js_sys, JsFuture};
use web_sys::{
js_sys::Object, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetFileOptions,
FileSystemWritableFileStream, WorkerGlobalScope,
};
const GITHUB_API: &str = "https://api.github.com";
pub async fn github_fetch_last_updated(
owner: &str,
repo: &str,
path: &str,
) -> Result<OffsetDateTime, GithubError> {
let commit: Vec<serde_json::Value> = http::Request::get(&format!(
"{GITHUB_API}/repos/{owner}/{repo}/commits?path={path}&page=1&per_page=1"
))
.send()
.await?
.json()
.await?;
commit[0]
.get("commit")
.and_then(|commit| commit.get("committer"))
.and_then(|committer| committer.get("date"))
.map(|date| {
OffsetDateTime::parse(
date.as_str().ok_or(GithubError::MissingCommitDateError)?,
&Rfc3339,
)
.map_err(|err| err.into())
})
.unwrap_or(Err(GithubError::MissingCommitDateError))
}
pub async fn github_fetch_file(
owner: &str,
repo: &str,
path: &str,
) -> Result<Vec<u8>, GithubError> {
http::Request::get(&format!(
"{GITHUB_API}/repos/{owner}/{repo}/contents/{path}"
))
.header("Accept", "application/vnd.github.raw+json")
.send()
.await?
.binary()
.await
.map_err(|err| err.into())
}
#[derive(Debug, Error)]
pub enum GithubError {
#[error("no file returned from the given GitHub API")]
NoSuchFile,
#[error("failed to fetch from GitHub API")]
RequestError(#[from] gloo::net::Error),
#[error("file fetched from the GitHub API does not have a commit date")]
MissingCommitDateError,
#[error("date/time format reported for GitHub commit is invalid")]
InvalidDateTimeError(#[from] time::error::Parse),
}
/// Read a file from the in-browser OPFS filesystem, creating the file
/// if it does not exist.
pub async fn read_opfs_file(path: &str) -> Result<String, OpfsError> {
let dir: FileSystemDirectoryHandle = JsFuture::from(
js_sys::global()
.dyn_into::<WorkerGlobalScope>()
.map_err(OpfsError::NotSupported)?
.navigator()
.storage()
.get_directory(),
)
.await
.map_err(OpfsError::HandleAccess)?
.into();
let opt = FileSystemGetFileOptions::new();
opt.set_create(true);
let file_handle: FileSystemFileHandle =
JsFuture::from(dir.get_file_handle_with_options(path, &opt))
.await
.map_err(OpfsError::HandleAccess)?
.into();
let file: web_sys::File = JsFuture::from(file_handle.get_file())
.await
.map_err(OpfsError::HandleAccess)?
.into();
Ok(gloo::file::futures::read_as_text(&gloo::file::Blob::from(file)).await?)
}
/// Write contents to a file in the in-browser OPFS, creating the file
/// if it does not exist.
pub async fn write_opfs_file(path: &str, contents: &str) -> Result<(), OpfsError> {
let dir: FileSystemDirectoryHandle = JsFuture::from(
js_sys::global()
.dyn_into::<WorkerGlobalScope>()
.map_err(OpfsError::NotSupported)?
.navigator()
.storage()
.get_directory(),
)
.await
.map_err(OpfsError::HandleAccess)?
.into();
let opt = FileSystemGetFileOptions::new();
opt.set_create(true);
let file_handle: FileSystemFileHandle =
JsFuture::from(dir.get_file_handle_with_options(path, &opt))
.await
.map_err(OpfsError::HandleAccess)?
.into();
let write_handle: FileSystemWritableFileStream = JsFuture::from(file_handle.create_writable())
.await
.map_err(OpfsError::HandleAccess)?
.into();
JsFuture::from(
write_handle
.write_with_str(contents)
.map_err(OpfsError::WriteFile)?,
)
.await
.map_err(OpfsError::WriteFile)?;
JsFuture::from(write_handle.close())
.await
.map_err(OpfsError::WriteFile)?;
Ok(())
}
#[derive(Debug, Error)]
pub enum OpfsError {
#[error("OPFS is not supported by this browser: {0:?}")]
NotSupported(Object),
#[error("could not get OPFS directory path: {0:?}")]
HandleAccess(JsValue),
#[error("could not read OPFS file")]
ReadFile(#[from] gloo::file::FileReadError),
#[error("could not write OPFS file: {0:?}")]
WriteFile(JsValue),
}
#[cfg(all(
any(target_arch = "wasm32", target_arch = "wasm64"),
target_os = "unknown",
test
))]
mod worker_tests {
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_dedicated_worker);
use super::{read_opfs_file, write_opfs_file};
#[wasm_bindgen_test]
async fn test_read_write_opfs() {
let empty = read_opfs_file("test.txt")
.await
.expect("file read must succeed");
assert_eq!(empty, "");
write_opfs_file("test.txt", "contents")
.await
.expect("file write must succeed");
let nonempty = read_opfs_file("test.txt")
.await
.expect("file read must succeed");
assert_eq!(nonempty, "contents");
}
}