upload_things/
presigned.rs1enum UploadParams {
2 Expires(u128),
3 Identifier(String),
4 FileName(String),
5 FileSize(u64),
6 Slug(String),
7 Signature(String),
8}
9impl Into<(String, String)> for UploadParams {
10 fn into(self) -> (String, String) {
11 match self {
12 Self::Expires(value) => ("expires".to_string(), value.to_string()),
13 Self::Identifier(value) => ("x-ut-identifier".to_string(), value),
14 Self::FileName(value) => ("x-ut-file-name".to_string(), value),
15 Self::FileSize(value) => ("x-ut-file-size".to_string(), value.to_string()),
16 Self::Slug(value) => ("x-ut-slug".to_string(), value),
17 Self::Signature(value) => ("signature".to_string(), format!("hmac-sha256={}", value)),
18 }
19 }
20}
21
22const ONE_HOUR_DELAY_MILLIS: u128 = 3600 * 1000;
23#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
24pub struct UtPreSignedUrl {
25 pub url: String,
26 pub file_key: String,
27 pub expires: u128,
28 #[serde(skip)]
29 pub region: crate::UploadRegion,
30}
31impl Default for UtPreSignedUrl {
32 fn default() -> Self {
33 let expires = std::time::SystemTime::now()
34 .duration_since(std::time::UNIX_EPOCH)
35 .expect("No time in platform")
36 .as_millis()
37 + ONE_HOUR_DELAY_MILLIS;
38 Self {
39 url: "".to_string(),
40 file_key: "".to_string(),
41 expires,
42 region: crate::UploadRegion::UsWestSeattle,
43 }
44 }
45}
46impl UtPreSignedUrl {
47 pub fn try_into_request(
48 self,
49 file_data: web_sys::FormData,
50 ) -> Result<web_sys::Request, web_sys::wasm_bindgen::JsValue> {
51 let init = web_sys::RequestInit::new();
52 init.set_method("PUT");
53 init.set_mode(web_sys::RequestMode::Cors);
54 init.set_body(&file_data.into());
55 let request = web_sys::Request::new_with_str_and_init(&self.url, &init)?;
56 Ok(request)
57 }
58 pub fn presigned_url(
59 &mut self,
60 request: crate::UtRequest,
61 api_key: String,
62 app_id: String,
63 ) -> anyhow::Result<()> {
64 let query_params = vec![
65 UploadParams::Expires(self.expires).into(),
66 UploadParams::Identifier(app_id).into(),
67 UploadParams::FileName(request.file_name).into(),
68 UploadParams::FileSize(request.file_size).into(),
69 UploadParams::Slug(request.slug).into(),
70 ];
71 let mut url = self.new_url(&query_params)?;
72 self.url = url.to_string();
73 let signature = self.generate_signature(api_key)?;
74 let (query, signature) = UploadParams::Signature(signature).into();
75 url.query_pairs_mut().append_pair(&query, &signature);
76 self.url = url.to_string();
77 Ok(())
78 }
79 fn new_url(&self, query_params: &[(String, String)]) -> anyhow::Result<url::Url> {
80 let mut url = url::Url::parse(&format!(
82 "https://{}.ingest.uploadthing.com/{}",
83 self.region.alias(),
84 self.file_key
85 ))?;
86 for (key, value) in query_params {
87 url.query_pairs_mut().append_pair(key, value);
88 }
89 Ok(url)
90 }
91 fn generate_signature(&self, api_key: String) -> anyhow::Result<String> {
92 type HmacSha256 = hmac::Hmac<sha2::Sha256>;
93 use hmac::Mac;
94 let mut mac = HmacSha256::new_from_slice(api_key.as_bytes())?;
95 mac.update(self.url.to_string().as_bytes());
96 let result = mac.finalize();
97 let signature_bytes = result.into_bytes();
98 Ok(signature_bytes
99 .iter()
100 .map(|byte| format!("{:02x}", byte))
101 .collect::<String>())
102 }
103 pub fn generate_file_key(&mut self, app_id: String) -> anyhow::Result<()> {
104 use base64::prelude::*;
105 let app_hash = Self::djb2_hash(&app_id);
106 let alphabet: Vec<char> = Self::shuffle(sqids::DEFAULT_ALPHABET, &app_id)
107 .chars()
108 .collect();
109 let sqids = sqids::Sqids::builder()
110 .alphabet(alphabet)
111 .min_length(12)
112 .build()?;
113 let encoded_app_id = sqids.encode(&vec![app_hash.abs() as u64])?;
114 let file_seed = uuid::Uuid::new_v4().to_string();
115 let encoded_file_seed = BASE64_URL_SAFE.encode(file_seed.as_bytes());
116 self.file_key = format!("{}{}", encoded_app_id, encoded_file_seed);
117 Ok(())
118 }
119 fn djb2_hash(s: &str) -> i32 {
120 let mut h: i64 = 5381;
121 for &byte in s.as_bytes().iter().rev() {
122 h = (h * 33) ^ (byte as i64);
123 h &= 0xFFFFFFFF;
125 }
126 h = (h & 0xBFFFFFFF) | ((h >> 1) & 0x40000000);
128
129 if h >= 0x80000000 {
130 h -= 0x100000000;
131 }
132 h as i32
133 }
134 fn shuffle(input: &str, seed: &str) -> String {
135 let mut chars: Vec<char> = input.chars().collect();
136 let seed_num = Self::djb2_hash(seed);
137 for i in 0..chars.len() {
138 let j = ((seed_num % (i as i32 + 1)) + i as i32) as usize % chars.len();
139 let temp = chars[i];
140 chars[i] = chars[j];
141 chars[j] = temp;
142 }
143 chars.iter().collect()
144 }
145}
146impl TryFrom<&web_sys::wasm_bindgen::JsValue> for UtPreSignedUrl {
147 type Error = web_sys::wasm_bindgen::JsValue;
148 fn try_from(value: &web_sys::wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
149 let js_str = web_sys::js_sys::JSON::stringify(value)?;
150 let str = js_str
151 .as_string()
152 .ok_or_else(|| web_sys::wasm_bindgen::JsValue::from_str("Failed to stringify JSON"))?;
153 let presigned_url: Self = serde_json::from_str(&str).map_err(|e| {
154 web_sys::wasm_bindgen::JsValue::from_str(&format!("Failed to parse JSON: {}", e))
155 })?;
156 Ok(presigned_url)
157 }
158}
159impl TryFrom<&String> for UtPreSignedUrl {
160 type Error = serde_json::Error;
161 fn try_from(value: &String) -> Result<Self, Self::Error> {
162 serde_json::from_str(value)
163 }
164}
165impl TryFrom<String> for UtPreSignedUrl {
166 type Error = serde_json::Error;
167 fn try_from(value: String) -> Result<Self, Self::Error> {
168 serde_json::from_str(&value)
169 }
170}
171impl TryFrom<web_sys::wasm_bindgen::JsValue> for UtPreSignedUrl {
172 type Error = web_sys::wasm_bindgen::JsValue;
173 fn try_from(value: web_sys::wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
174 Self::try_from(&value)
175 }
176}
177
178#[cfg(test)]
179#[test]
180fn shuffle() {
181 let input = sqids::DEFAULT_ALPHABET;
182 let seed = "73bwh5z2wi";
183 let shuffled = UtPreSignedUrl::shuffle(input, &seed);
184 let expected = "Ha7cdM3yek9jLh6lb85oPwNAgKrIztFfXqnxismQGW2UTvuJOS4CZpRB10VDYE";
185 assert_eq!(shuffled, expected);
186}
187#[cfg(test)]
188#[test]
189fn hash() {
190 let seed = "73bwh5z2wi";
191 let expected = "gL3R2N9BwZXI";
192 let app_hash = UtPreSignedUrl::djb2_hash(&seed);
193 let sqids = sqids::Sqids::builder()
194 .alphabet(
195 UtPreSignedUrl::shuffle(sqids::DEFAULT_ALPHABET, &seed)
196 .chars()
197 .collect(),
198 )
199 .min_length(12)
200 .build()
201 .expect("Failed to build sqids");
202 let encoded_app_id = sqids
203 .encode(&vec![app_hash.abs() as u64])
204 .expect("Failed to encode");
205 assert_eq!(encoded_app_id, expected);
206}