1use std::{
5 collections::{HashMap, HashSet},
6 future::Future,
7 pin::Pin,
8};
9
10use topcoat::context::Cx;
11
12use crate::{form::FieldErrors, panel::state::current, schema::Schema};
13
14pub trait Uploader: Send + Sync + 'static {
21 fn store(
24 &self,
25 filename: &str,
26 bytes: &[u8],
27 ) -> impl Future<Output = Result<String, String>> + Send;
28
29 fn holds(&self, path: &str) -> impl Future<Output = bool> + Send {
32 async move {
33 let _ = path;
34 false
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
41pub(crate) struct StagedUpload {
42 pub(crate) filename: String,
43 pub(crate) bytes: Vec<u8>,
44}
45
46pub(crate) struct InstalledUploader(Box<dyn DynUploader + Send + Sync>);
48
49impl InstalledUploader {
50 pub(crate) fn new(uploader: impl Uploader) -> Self {
51 Self(Box::new(uploader))
52 }
53}
54
55type StoreFuture<'a> = Pin<Box<dyn Future<Output = Result<String, String>> + Send + 'a>>;
57
58type HoldFuture<'a> = Pin<Box<dyn Future<Output = bool> + Send + 'a>>;
60
61pub(crate) trait DynUploader: Send + Sync {
63 fn store<'a>(&'a self, filename: &'a str, bytes: &'a [u8]) -> StoreFuture<'a>;
64 fn holds<'a>(&'a self, path: &'a str) -> HoldFuture<'a>;
65}
66
67impl<U: Uploader> DynUploader for U {
68 fn store<'a>(&'a self, filename: &'a str, bytes: &'a [u8]) -> StoreFuture<'a> {
69 Box::pin(Uploader::store(self, filename, bytes))
70 }
71
72 fn holds<'a>(&'a self, path: &'a str) -> HoldFuture<'a> {
73 Box::pin(Uploader::holds(self, path))
74 }
75}
76
77pub(crate) fn installed(cx: &Cx) -> bool {
79 installed_uploader(cx).is_some()
80}
81
82type DynUploaderRef<'a> = &'a (dyn DynUploader + Send + Sync);
83
84fn installed_uploader<'a>(cx: &'a Cx) -> Option<DynUploaderRef<'a>> {
85 current(cx)
86 .and_then(|panel| panel.uploads.as_ref())
87 .map(|installed| &*installed.0)
88}
89
90pub(crate) async fn holds(cx: &Cx, path: &str) -> bool {
92 match installed_uploader(cx) {
93 Some(uploader) => uploader.holds(path).await,
94 None => false,
95 }
96}
97
98pub(crate) async fn store_uploads(
103 cx: &Cx,
104 schema: &Schema,
105 files: &HashMap<String, StagedUpload>,
106 values: &mut HashMap<String, String>,
107) -> (FieldErrors, HashSet<String>) {
108 let Some(uploader) = installed_uploader(cx) else {
109 return (FieldErrors::new(), HashSet::new());
110 };
111 let mut errors = FieldErrors::new();
112 let mut stored = HashSet::new();
113 for field in schema.fields().filter(|field| field.is_file()) {
116 let name = field.name().to_string();
117 let Some(staged) = files.get(&name) else {
118 continue;
119 };
120 match uploader.store(&staged.filename, &staged.bytes).await {
121 Ok(path) => {
122 values.insert(name.clone(), path);
123 stored.insert(name);
124 }
125 Err(reason) => {
126 values.remove(&name);
127 errors.add(
128 name,
129 format!("{} could not be uploaded: {reason}", field.label_str()),
130 );
131 }
132 }
133 }
134 (errors, stored)
135}