Skip to content

Commit

Permalink
Adds Observed trait for checking an object is modified (#53)
Browse files Browse the repository at this point in the history
* Adds Observed trait for checking an object is modified, impl in Content
  • Loading branch information
Jerboa-app authored Apr 28, 2024
1 parent 49848f4 commit 46d5e0f
Show file tree
Hide file tree
Showing 11 changed files with 251 additions and 28 deletions.
59 changes: 50 additions & 9 deletions src/content/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::cmp::min;
use std::time::SystemTime;

use serde::{Deserialize, Serialize};

use crate::filesystem::file::File;
use crate::filesystem::file::{read_file_bytes, read_file_utf8, write_file_bytes, FileNotReadError};
use crate::util::dump_bytes;
use crate::filesystem::file::{file_hash, File, Observed};
use crate::filesystem::file::{read_file_bytes, read_file_utf8, write_file_bytes, FileError};
use crate::util::{dump_bytes, hash};

use self::mime_type::infer_mime_type;

Expand All @@ -23,14 +24,28 @@ pub mod mime_type;
///
/// - The body is unpopulated until [Content::load_from_file] is called
/// - The body may be converted to a utf8 string using [Content::utf8_body]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Content
{
uri: String,
body: Vec<u8>,
content_type: String,
disk_path: String,
cache_period_seconds: u16
cache_period_seconds: u16,
hash: Vec<u8>,
last_refreshed: SystemTime
}

impl PartialEq for Content
{
fn eq(&self, other: &Content) -> bool
{
return self.uri == other.uri && self.body == other.body &&
self.content_type == other.content_type &&
self.disk_path == other.disk_path &&
self.cache_period_seconds == other.cache_period_seconds &&
self.hash == other.hash
}
}

impl File for Content
Expand All @@ -51,6 +66,24 @@ impl File for Content
}
}

impl Observed for Content
{
fn is_stale(&self) -> bool
{
// this is 4x slower than using the modified date
// but the modified date fails when is_stale is called
// very soon after creation/modification, plus may
// not be guaranteed cross platform, this is.
// We can check 100,000 files in 447 millis
return file_hash(&self.disk_path) != self.hash
}

fn refresh(&mut self)
{
let _ = self.load_from_file();
}
}

impl Content
{
pub fn new(uri: &str, disk_path: &str, cache: u16) -> Content
Expand All @@ -61,18 +94,26 @@ impl Content
body: vec![],
disk_path: disk_path.to_string(),
content_type: infer_mime_type(disk_path).to_string(),
cache_period_seconds: cache
cache_period_seconds: cache,
hash: vec![],
last_refreshed: SystemTime::now()
}
}

pub fn load_from_file(&mut self) -> Result<(), FileNotReadError>
pub fn load_from_file(&mut self) -> Result<(), FileError>
{
match self.read_bytes()
{
Some(data) => {self.body = data; Ok(())}
Some(data) =>
{
self.body = data.clone();
self.hash = hash(data);
self.last_refreshed = SystemTime::now();
Ok(())
}
None =>
{
Err(FileNotReadError { why: format!("Could not read bytes from {}", self.disk_path)})
Err(FileError { why: format!("Could not read bytes from {}", self.disk_path)})
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/content/pages/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use axum::response::{IntoResponse, Response, Html};
use regex::Regex;
use serde::{Deserialize, Serialize};

use crate::{content::Content, filesystem::file::{File, FileNotReadError}};
use crate::{content::Content, filesystem::file::{File, FileError}};

/// An HTML webpage
///
Expand Down Expand Up @@ -48,7 +48,7 @@ impl Page
}
}

pub fn load_from_file(&mut self) -> Result<(), FileNotReadError>
pub fn load_from_file(&mut self) -> Result<(), FileError>
{
self.content.load_from_file()
}
Expand Down
4 changes: 2 additions & 2 deletions src/content/resources/resource.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use axum::response::{Html, IntoResponse, Response};
use serde::{Serialize, Deserialize};

use crate::{content::Content, filesystem::file::FileNotReadError};
use crate::{content::Content, filesystem::file::FileError};

/// A non-HTML resource
///
Expand Down Expand Up @@ -43,7 +43,7 @@ impl Resource
}
}

pub fn load_from_file(&mut self) -> Result<(), FileNotReadError>
pub fn load_from_file(&mut self) -> Result<(), FileError>
{
self.content.load_from_file()
}
Expand Down
21 changes: 19 additions & 2 deletions src/filesystem/file.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::{fmt, fs, io::{Read, Write}};

use crate::util::hash;

#[derive(Debug, Clone)]
pub struct FileNotReadError
pub struct FileError
{
pub why: String
}

impl fmt::Display for FileNotReadError {
impl fmt::Display for FileError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.why)
}
Expand All @@ -20,6 +22,12 @@ pub trait File
fn read_utf8(&self) -> Option<String>;
}

pub trait Observed
{
fn is_stale(&self) -> bool;
fn refresh(&mut self);
}

pub fn write_file_bytes(path: &str, data: &[u8])
{
let mut file = fs::File::create(path).unwrap();
Expand Down Expand Up @@ -68,4 +76,13 @@ pub fn read_file_bytes(path: &str) -> Option<Vec<u8>>
},
Ok(_) => Some(s)
}
}

pub fn file_hash(path: &str) -> Vec<u8>
{
match read_file_bytes(path)
{
Some(d) => hash(d),
None => vec![]
}
}
5 changes: 5 additions & 0 deletions src/filesystem/observed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub trait Observed
{
fn stale(&self) -> bool;
fn refresh(&mut self);
}
30 changes: 26 additions & 4 deletions src/util.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use core::fmt;
use std::{fmt::Write, io::{Read, Write as ioWrite}};
use libflate::deflate::{Encoder, Decoder};
use openssl::sha::Sha256;
use regex::Regex;

pub fn dump_bytes(v: &[u8]) -> String
Expand Down Expand Up @@ -86,20 +87,34 @@ pub fn compress(bytes: &[u8]) -> Result<Vec<u8>, CompressionError>
}
}

pub fn decompress(bytes: Vec<u8>) -> Result<String, CompressionError>
pub fn decompress(bytes: Vec<u8>) -> Result<Vec<u8>, CompressionError>
{
let mut decoder = Decoder::new(&bytes[..]);
let mut decoded_data = Vec::new();

match decoder.read_to_end(&mut decoded_data)
{
Ok(_) => (),
Ok(_) => Ok(decoded_data),
Err(e) =>
{
return Err(CompressionError { why: format!("Error decoding data: {}", e) })
Err(CompressionError { why: format!("Error decoding data: {}", e) })
}
}

}

pub fn compress_string(s: &String) -> Result<Vec<u8>, CompressionError>
{
compress(s.as_bytes())
}

pub fn decompress_utf8_string(compressed: Vec<u8>) -> Result<String, CompressionError>
{
let decoded_data = match decompress(compressed)
{
Ok(d) => d,
Err(e) => return Err(e)
};

match std::str::from_utf8(&decoded_data)
{
Ok(s) => Ok(s.to_string()),
Expand All @@ -109,3 +124,10 @@ pub fn decompress(bytes: Vec<u8>) -> Result<String, CompressionError>
}
}
}

pub fn hash(v: Vec<u8>) -> Vec<u8>
{
let mut sha = Sha256::new();
sha.update(&v);
sha.finish().to_vec()
}
59 changes: 59 additions & 0 deletions tests/test_content.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
mod common;

#[cfg(test)]
mod test_content
{
use std::{fs::remove_file, path::Path};

use busser::{content::Content, filesystem::file::{file_hash, write_file_bytes, Observed}, util::read_bytes};

#[test]
fn test_load_content()
{
let mut content = Content::new("tests/pages/a.html", "tests/pages/a.html", 3600);

assert_eq!(content.get_uri(), "tests/pages/a.html".to_string());
assert!(content.utf8_body().is_ok_and(|b| b == "".to_string()));

assert!(content.load_from_file().is_ok());
assert!(content.utf8_body().is_ok_and(|b| b == "this is /a".to_string()));

let file = "test_load_content";
let path = Path::new("file");
if path.exists()
{
let _ = remove_file(file);
}
let mut content_missing = Content::new(file, file, 3600);
assert!(content_missing.load_from_file().is_err());
}

#[test]
fn test_observed_content()
{
let path = "test_observed_content";
let test_content = "this is some test content";
let test_content_hash = "2d5bb7c3afbe68c05bcd109d890dca28ceb0105bf529ea1111f9ef8b44b217b9".to_string();
let modified_test_content = "this is some modified content";
let modified_test_content_hash = "c4ea4898725c3390549d40a19a26a57730730b42050def80f1d157581e33b2db".to_string();

write_file_bytes(path, test_content.as_bytes());

let mut content = Content::new(path, path, 3600);

assert!(content.load_from_file().is_ok());
assert!(!content.is_stale());
assert_eq!(file_hash(path), read_bytes(test_content_hash));
assert!(content.utf8_body().is_ok_and(|b| b == test_content.to_string()));
write_file_bytes(path, modified_test_content.as_bytes());

assert!(content.is_stale());
assert_eq!(file_hash(path), read_bytes(modified_test_content_hash));
content.refresh();
assert!(content.utf8_body().is_ok_and(|b| b == modified_test_content.to_string()));

let _ = remove_file(path);
}

}

25 changes: 19 additions & 6 deletions tests/test_filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ mod common;
mod filesystem
{

use std::fs::remove_file;
use std::{fs::remove_file, path::Path};

use busser::filesystem::{file::{read_file_bytes, read_file_utf8, write_file_bytes, FileNotReadError}, folder::list_dir_by};
use busser::filesystem::{file::{read_file_bytes, read_file_utf8, write_file_bytes}, folder::list_dir_by};
use regex::Regex;


Expand All @@ -15,15 +15,29 @@ mod filesystem
{
let expected = "this is /a".as_bytes();
let actual = read_file_bytes("tests/pages/a.html").unwrap();
assert_eq!(actual, expected)
assert_eq!(actual, expected);

let path = Path::new("test_file_error");
if path.exists()
{
let _ = remove_file(path);
}
assert!(read_file_bytes(path.to_str().unwrap()).is_none());
}

#[test]
fn test_read_utf8()
{
let expected = "this is /a";
let actual = read_file_utf8("tests/pages/a.html").unwrap();
assert_eq!(actual, expected)
assert_eq!(actual, expected);

let path = Path::new("test_file_error");
if path.exists()
{
let _ = remove_file(path);
}
assert!(read_file_utf8(path.to_str().unwrap()).is_none());
}

#[test]
Expand All @@ -36,7 +50,7 @@ mod filesystem
let actual = read_file_utf8("test_write_bytes").unwrap();
assert_eq!(actual, expected);

remove_file("test_write_bytes");
let _ = remove_file("test_write_bytes");
}

#[test]
Expand All @@ -48,6 +62,5 @@ mod filesystem
assert!(actual.contains(&"tests/pages/data/jpg.jpg".to_string()));
assert!(actual.contains(&"tests/pages/data/png.jpg".to_string()));
assert_eq!(actual.len(), 2);

}
}
3 changes: 1 addition & 2 deletions tests/test_page_load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ mod test_page_load
let pages = get_pages(Some("tests/pages"), None);

assert_eq!(pages.len(), 3);

let paths = HashMap::from(
[
("tests/pages/a.html", "this is /a"),
Expand All @@ -31,7 +31,6 @@ mod test_page_load
assert_eq!(actual_body, expected_body)
}


}

}
Loading

0 comments on commit 46d5e0f

Please sign in to comment.