use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
thread,
};
use gled_model::asset::picture::is_picture_path;
use gled_protocol::{bytes::SharedBytes, state::EngineEvent};
use kanal::{Sender, unbounded};
use tracing::warn;
use crate::storage::{STORAGE_DIR, action::StorageAction};
const MAX_BYTES: u64 = 32 * 1024 * 1024;
type Request = (PathBuf, Sender<EngineEvent>);
#[derive(Debug)]
pub(super) struct Pictures {
uploads: HashMap<PathBuf, SharedBytes>,
reader: Option<Sender<Request>>,
}
impl Default for Pictures {
fn default() -> Self {
let (reader, requests) = unbounded::<Request>();
let started = thread::Builder::new()
.name("gled:pictures".to_owned())
.spawn(move || {
while let Ok((path, reply)) = requests.recv() {
let picture = read(&STORAGE_DIR, &path);
let _ = reply.send(EngineEvent::Picture { path, picture });
}
});
if let Err(err) = &started {
warn!("Could not start reading pictures: {err}");
}
Self {
uploads: HashMap::new(),
reader: started.ok().map(|_| reader),
}
}
}
impl Pictures {
pub(super) fn request(&self, path: PathBuf, reply: &Sender<EngineEvent>) {
let answer = |picture| {
let _ = reply.send(EngineEvent::Picture {
path: path.clone(),
picture,
});
};
if !is_picture_path(&path) {
return answer(Err("no picture in the asset repository".to_owned()));
}
if let Some(bytes) = self.uploads.get(&path) {
return answer(Ok(bytes.clone()));
}
match &self.reader {
Some(reader) => {
let _ = reader.send((path, reply.clone()));
}
None => answer(Err("the engine reads no pictures".to_owned())),
}
}
pub(super) fn save(&mut self, path: PathBuf, bytes: SharedBytes) {
if !is_picture_path(&path) || bytes.0.len() as u64 > MAX_BYTES {
warn!("Refused to store {} as a picture", path.display());
return;
}
self.uploads.insert(path.clone(), bytes.clone());
StorageAction::SavePicture { path, bytes }.enqueue();
}
}
fn read(repository: &Path, path: &Path) -> Result<SharedBytes, String> {
let file = repository.join(path);
let size = std::fs::metadata(&file)
.map_err(|err| format!("{}: {err}", path.display()))?
.len();
if size > MAX_BYTES {
return Err(format!("{} is too large to show", path.display()));
}
std::fs::read(&file)
.map(|bytes| SharedBytes(Arc::new(bytes)))
.map_err(|err| format!("{}: {err}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_picture_is_read_from_the_repository() {
let repository =
std::env::temp_dir().join(format!("gled-pictures-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(repository.join("pictures")).expect("creates the repository");
std::fs::write(repository.join("pictures/stage.png"), b"png").expect("writes");
let bytes = read(&repository, Path::new("pictures/stage.png")).expect("reads it");
assert_eq!(*bytes.0, b"png");
assert!(read(&repository, Path::new("pictures/gone.png")).is_err());
}
#[test]
fn a_path_leaving_the_repository_is_refused() {
let pictures = Pictures {
uploads: HashMap::new(),
reader: None,
};
let (reply, answers) = unbounded();
pictures.request(PathBuf::from("../secret.png"), &reply);
let Ok(Some(EngineEvent::Picture { picture, .. })) = answers.try_recv() else {
panic!("an answer");
};
assert!(picture.is_err());
}
#[test]
fn an_upload_is_answered_from_memory() {
let mut pictures = Pictures {
uploads: HashMap::new(),
reader: None,
};
let path = PathBuf::from("pictures/stage.png");
let bytes = SharedBytes(Arc::new(b"png".to_vec()));
pictures.uploads.insert(path.clone(), bytes.clone());
let (reply, answers) = unbounded();
pictures.request(path, &reply);
let Ok(Some(EngineEvent::Picture { picture, .. })) = answers.try_recv() else {
panic!("an answer");
};
assert_eq!(picture, Ok(bytes));
}
}