1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
use crate::errors::*;
#[cfg(engine)]
use tokio::{
fs::{create_dir_all, File},
io::{AsyncReadExt, AsyncWriteExt},
};
/// A trait for implementations of stores that the Perseus engine can use for
/// mutable data, which may need to be altered while the server is running. In
/// exported apps, this is irrelevant, since there is no server process to speak
/// of. This store is used in particular by the revalidation and incremental
/// generation strategies, which will both update build artifacts for future
/// caching. By default, [`FsMutableStore`] is used for simplicity and low
/// latency, though this is only viable in deployments with writable
/// filesystems. Notably, this precludes usage in serverless functions.
///
/// However, this is written deliberately as a trait with exposed, isolated
/// error types (see [`StoreError`]), so that users can write their own
/// implementations of this. For instance, you could manage mutable artifacts in
/// a database, though this should be as low-latency as possible, since reads
/// and writes are required at extremely short-notice as new user requests
/// arrive.
///
/// **Warning:** the `NotFound` error is integral to Perseus' internal
/// operation, and must be returned if an asset does not exist. Do NOT return
/// any other error if everything else worked, but an asset simply did not
/// exist, or the entire render system will come crashing down around you!
#[async_trait::async_trait]
pub trait MutableStore: std::fmt::Debug + Clone + Send + Sync {
/// Reads data from the named asset.
async fn read(&self, name: &str) -> Result<String, StoreError>;
/// Writes data to the named asset. This will create a new asset if one
/// doesn't exist already.
async fn write(&self, name: &str, content: &str) -> Result<(), StoreError>;
}
/// The default [`MutableStore`], which simply uses the filesystem. This is
/// suitable for development and production environments with
/// writable filesystems (in which it's advised), but this is of course not
/// usable on production read-only filesystems, and another implementation of
/// [`MutableStore`] should be preferred.
///
/// Note: the `.write()` methods on this implementation will create any missing
/// parent directories automatically.
#[derive(Clone, Debug)]
pub struct FsMutableStore {
#[cfg(engine)]
root_path: String,
}
#[cfg(engine)]
impl FsMutableStore {
/// Creates a new filesystem configuration manager. You should provide a
/// path like `dist/mutable` here. Make sure that this is not the same
/// path as the [`ImmutableStore`](super::ImmutableStore), as this will
/// cause potentially problematic overlap between the two systems.
#[cfg(engine)]
pub fn new(root_path: String) -> Self {
Self { root_path }
}
}
#[async_trait::async_trait]
impl MutableStore for FsMutableStore {
#[cfg(engine)]
async fn read(&self, name: &str) -> Result<String, StoreError> {
let asset_path = format!("{}/{}", self.root_path, name);
let file_res = File::open(&asset_path).await;
let mut file = match file_res {
Ok(file) => file,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Err(StoreError::NotFound { name: asset_path })
}
Err(err) => {
return Err(StoreError::ReadFailed {
name: asset_path,
source: err.into(),
})
}
};
let metadata = file.metadata().await;
match metadata {
Ok(_) => {
let mut contents = String::new();
file.read_to_string(&mut contents)
.await
.map_err(|err| StoreError::ReadFailed {
name: asset_path,
source: err.into(),
})?;
Ok(contents)
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
Err(StoreError::NotFound { name: asset_path })
}
Err(err) => Err(StoreError::ReadFailed {
name: asset_path,
source: err.into(),
}),
}
}
// This creates a directory structure as necessary
#[cfg(engine)]
async fn write(&self, name: &str, content: &str) -> Result<(), StoreError> {
let asset_path = format!("{}/{}", self.root_path, name);
let mut dir_tree: Vec<&str> = asset_path.split('/').collect();
dir_tree.pop();
create_dir_all(dir_tree.join("/"))
.await
.map_err(|err| StoreError::WriteFailed {
name: asset_path.clone(),
source: err.into(),
})?;
// This will either create the file or truncate it if it already exists
let mut file = File::create(&asset_path)
.await
.map_err(|err| StoreError::WriteFailed {
name: asset_path.clone(),
source: err.into(),
})?;
file.write_all(content.as_bytes())
.await
.map_err(|err| StoreError::WriteFailed {
name: asset_path.clone(),
source: err.into(),
})?;
// TODO Can we use `sync_data()` here to reduce I/O?
file.sync_all()
.await
.map_err(|err| StoreError::WriteFailed {
name: asset_path,
source: err.into(),
})?;
Ok(())
}
#[cfg(client)]
async fn read(&self, _name: &str) -> Result<String, StoreError> {
Ok(String::new())
}
#[cfg(client)]
async fn write(&self, _name: &str, _content: &str) -> Result<(), StoreError> {
Ok(())
}
}