blob: 2d71b570b5c086b97496b632aaa930b8effbc225 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::handler::device_storage::DeviceStorageCompatible,
anyhow::{format_err, Error},
fuchsia_syslog::{fx_log_err, fx_log_info},
serde::de::DeserializeOwned,
serde::Serialize,
std::fs,
std::io::Write,
std::path::PathBuf,
};
// Structs that implement this can be supported by StoreAccessor. The file name
// provided should be a valid file name.
pub trait StoreAccessorCompatible:
Serialize + DeserializeOwned + Clone + PartialEq + DeviceStorageCompatible
{
const FILE_NAME: &'static str;
}
// This struct is used to read/write data into a file for persistent storage.
pub struct StoreAccessor<T: StoreAccessorCompatible> {
file_path: PathBuf,
current_data: Option<T>,
}
impl<T: StoreAccessorCompatible> StoreAccessor<T> {
pub fn new(current_data: Option<T>, file_directory: PathBuf) -> Self {
let mut path = file_directory.clone();
path.push(&T::FILE_NAME);
return StoreAccessor { file_path: path, current_data: current_data };
}
pub async fn get_value(&mut self) -> T {
if self.current_data == None {
self.current_data = Some(self.read_or_create_file().await);
}
self.current_data.as_ref().unwrap().clone()
}
async fn read_or_create_file(&mut self) -> T {
match fs::read_to_string(&self.file_path) {
Ok(value) => T::deserialize_from(&value),
Err(_) => {
fx_log_info!("store file doesn't exist, creating file");
if let Err(e) = self.set_value(&T::default_value()).await {
fx_log_err!("unable to create file: {}", e);
};
T::default_value()
}
}
}
pub async fn set_value(&mut self, new_value: &T) -> Result<(), Error> {
if self.current_data.as_ref() == Some(new_value) {
return Ok(());
}
// Save the current data. Even if persistent storage fails, it's still saved
// locally.
self.current_data = Some(new_value.clone());
// To prevent corrupted writes, create and write to a temporary file. After that,
// replace the data file with the temporary one.
let temp_file_path = self.file_path.with_extension("tmp");
let mut file = match fs::OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(temp_file_path.clone())
{
Ok(f) => f,
Err(e) => {
fx_log_err!("unable open or create {}, {}", temp_file_path.to_str().unwrap(), e);
return Err(format_err!("unable to write to storage"));
}
};
if let Err(e) = file.write_all(new_value.serialize_to().as_bytes()) {
fx_log_err!("unable to write to file {}, {}", temp_file_path.to_str().unwrap(), e);
return Err(format_err!("unable to write to storage"));
}
if let Err(e) = fs::rename(temp_file_path, &self.file_path) {
fx_log_err!("unable to replace file with temp, {}", e);
return Err(format_err!("unable to write to storage"));
}
Ok(())
}
}
#[cfg(test)]
pub mod testing {
use super::*;
use serde::{Deserialize, Serialize};
use tempfile::TempDir;
#[derive(PartialEq, Debug, Clone, Copy, Serialize, Deserialize)]
struct TestData {
pub value: f32,
}
impl DeviceStorageCompatible for TestData {
const KEY: &'static str = "test_key";
fn default_value() -> Self {
TestData { value: 0.0 }
}
}
impl StoreAccessorCompatible for TestData {
const FILE_NAME: &'static str = "test_store_data.store";
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_read_and_write_new_file() {
let tmp_dir = TempDir::new().expect("should have created tempdir");
let mut store = StoreAccessor::<TestData>::new(None, tmp_dir.path().to_path_buf());
assert_eq!(TestData::default_value(), store.get_value().await);
// Check to see if the file is created with the default value written to it.
let data_file_path = tmp_dir.path().join(&TestData::FILE_NAME);
assert!(data_file_path.exists());
assert_eq!(
TestData::default_value().serialize_to(),
fs::read_to_string(&data_file_path).expect("should read in file")
);
// Change the value.
let new_val = TestData { value: 5.0 };
store.set_value(&new_val).await.expect("should set new value");
assert_eq!(new_val, store.get_value().await);
// Check that the file is written.
assert_eq!(
new_val.serialize_to(),
fs::read_to_string(&data_file_path).expect("should read in file")
);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_load_data_from_file() {
let mut store = StoreAccessor::<TestData>::new(None, PathBuf::from("/pkg/data/"));
assert_eq!(TestData { value: 10.0 }, store.get_value().await);
}
}