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"); } }