use std::{
collections::{HashMap, HashSet},
future::Future,
pin::Pin,
};
use topcoat::context::Cx;
use crate::{form::FieldErrors, panel::state::current, schema::Schema};
pub trait Uploader: Send + Sync + 'static {
fn store(
&self,
filename: &str,
bytes: &[u8],
) -> impl Future<Output = Result<String, String>> + Send;
fn holds(&self, path: &str) -> impl Future<Output = bool> + Send {
async move {
let _ = path;
false
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct StagedUpload {
pub(crate) filename: String,
pub(crate) bytes: Vec<u8>,
}
pub(crate) struct InstalledUploader(Box<dyn DynUploader + Send + Sync>);
impl InstalledUploader {
pub(crate) fn new(uploader: impl Uploader) -> Self {
Self(Box::new(uploader))
}
}
type StoreFuture<'a> = Pin<Box<dyn Future<Output = Result<String, String>> + Send + 'a>>;
type HoldFuture<'a> = Pin<Box<dyn Future<Output = bool> + Send + 'a>>;
pub(crate) trait DynUploader: Send + Sync {
fn store<'a>(&'a self, filename: &'a str, bytes: &'a [u8]) -> StoreFuture<'a>;
fn holds<'a>(&'a self, path: &'a str) -> HoldFuture<'a>;
}
impl<U: Uploader> DynUploader for U {
fn store<'a>(&'a self, filename: &'a str, bytes: &'a [u8]) -> StoreFuture<'a> {
Box::pin(Uploader::store(self, filename, bytes))
}
fn holds<'a>(&'a self, path: &'a str) -> HoldFuture<'a> {
Box::pin(Uploader::holds(self, path))
}
}
pub(crate) fn installed(cx: &Cx) -> bool {
installed_uploader(cx).is_some()
}
type DynUploaderRef<'a> = &'a (dyn DynUploader + Send + Sync);
fn installed_uploader<'a>(cx: &'a Cx) -> Option<DynUploaderRef<'a>> {
current(cx)
.and_then(|panel| panel.uploads.as_ref())
.map(|installed| &*installed.0)
}
pub(crate) async fn holds(cx: &Cx, path: &str) -> bool {
match installed_uploader(cx) {
Some(uploader) => uploader.holds(path).await,
None => false,
}
}
pub(crate) async fn store_uploads(
cx: &Cx,
schema: &Schema,
files: &HashMap<String, StagedUpload>,
values: &mut HashMap<String, String>,
) -> (FieldErrors, HashSet<String>) {
let Some(uploader) = installed_uploader(cx) else {
return (FieldErrors::new(), HashSet::new());
};
let mut errors = FieldErrors::new();
let mut stored = HashSet::new();
for field in schema.fields().filter(|field| field.is_file()) {
let name = field.name().to_string();
let Some(staged) = files.get(&name) else {
continue;
};
match uploader.store(&staged.filename, &staged.bytes).await {
Ok(path) => {
values.insert(name.clone(), path);
stored.insert(name);
}
Err(reason) => {
values.remove(&name);
errors.add(
name,
format!("{} could not be uploaded: {reason}", field.label_str()),
);
}
}
}
(errors, stored)
}