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().duration_since(UNIX_EPOCH).ok()?.as_secs();
59 let sign_content = format!("{key}{encoded_path}{ts}");
60 let sign = format!("{:x}", md5::compute(sign_content.as_bytes()));
61 let domain = domain.trim_end_matches('/');
62 let protocol = if domain.starts_with("http://") || domain.starts_with("https://") {
63 ""
64 } else {
65 "https://"
66 };
67 Some(format!(
68 "{protocol}{domain}{encoded_path}?sign={sign}&t={ts}"
69 ))
70}
71
72fn encode_cdn_path(path: &str) -> String {
77 path.split('/')
78 .map(|seg| {
79 if seg.is_empty() {
80 String::new()
81 } else {
82 urlencoding::encode(seg)
83 .replace('!', "%21")
84 .replace('\'', "%27")
85 .replace('(', "%28")
86 .replace(')', "%29")
87 .replace('*', "%2A")
88 }
89 })
90 .collect::<Vec<_>>()
91 .join("/")
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn test_generate_cdn_url_has_sign_and_t() {
100 let url = generate_cdn_url(
101 "ossdev.yuriful.top",
102 "testkey",
103 "manga_1/episode_2/manga-raw/file.7z",
104 )
105 .unwrap();
106 assert!(url.contains("?sign="));
107 assert!(url.contains("&t="));
108 assert!(url.starts_with("https://ossdev.yuriful.top/"));
109 }
110}