upload_things/
presigned.rs

1enum 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        // Generate the upload URL (example format)
81        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            // Simulate 32-bit integer overflow
124            h &= 0xFFFFFFFF;
125        }
126        // Convert to signed 32-bit integer with the same bit manipulation
127        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}