tdm_server_rust/utils/
cos_cdn.rs1use std::time::{SystemTime, UNIX_EPOCH};
17
18pub fn generate_cdn_url(cdn_domain: &str, cdn_key: &str, object_key: &str) -> Option<String> {
47 let domain = cdn_domain.trim();
48 let key = cdn_key.trim();
49 if domain.is_empty() || key.is_empty() {
50 return None;
51 }
52 let raw_path = if object_key.starts_with('/') {
53 object_key.to_string()
54 } else {
55 format!("/{object_key}")
56 };
57 let encoded_path = encode_cdn_path(&raw_path);
58 let ts = SystemTime::now()
59 .duration_since(UNIX_EPOCH)
60 .ok()?
61 .as_secs();
62 let sign_content = format!("{key}{encoded_path}{ts}");
63 let sign = format!("{:x}", md5::compute(sign_content.as_bytes()));
64 let domain = domain.trim_end_matches('/');
65 let protocol = if domain.starts_with("http://") || domain.starts_with("https://") {
66 ""
67 } else {
68 "https://"
69 };
70 Some(format!("{protocol}{domain}{encoded_path}?sign={sign}&t={ts}"))
71}
72
73fn encode_cdn_path(path: &str) -> String {
78 path.split('/')
79 .map(|seg| {
80 if seg.is_empty() {
81 String::new()
82 } else {
83 urlencoding::encode(seg)
84 .replace('!', "%21")
85 .replace('\'', "%27")
86 .replace('(', "%28")
87 .replace(')', "%29")
88 .replace('*', "%2A")
89 }
90 })
91 .collect::<Vec<_>>()
92 .join("/")
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn test_generate_cdn_url_has_sign_and_t() {
101 let url = generate_cdn_url(
102 "ossdev.yuriful.top",
103 "testkey",
104 "manga_1/episode_2/manga-raw/file.7z",
105 )
106 .unwrap();
107 assert!(url.contains("?sign="));
108 assert!(url.contains("&t="));
109 assert!(url.starts_with("https://ossdev.yuriful.top/"));
110 }
111}