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(())
    }
}