mimick/api_client/
upload_helpers.rs1use std::path::Path;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use chrono::{SecondsFormat, TimeZone, Utc};
11use reqwest::Client;
12
13pub(super) fn mime_for_path(path: &Path) -> &'static str {
15 crate::media_kinds::mime_for_path(path)
16}
17
18pub(super) async fn apply_asset_timezone_fixup(
20 client: &Client,
21 api_key: &str,
22 base_url: &str,
23 asset_id: &str,
24 time_zone: &str,
25) {
26 let url = format!("{}/api/assets", base_url);
27 let body = serde_json::json!({
28 "ids": [asset_id],
29 "timeZone": time_zone,
30 });
31
32 match client
33 .put(&url)
34 .header("x-api-key", api_key)
35 .header("Accept", "application/json")
36 .json(&body)
37 .send()
38 .await
39 {
40 Ok(resp) if resp.status().is_success() => {
41 log::debug!("Updated timezone for asset {} to {}", asset_id, time_zone);
42 }
43 Ok(resp) => {
44 let status = resp.status();
45 let body = resp.text().await.unwrap_or_default();
46 log::warn!(
47 "Uploaded asset {} but failed to update timezone [{}]: {}",
48 asset_id,
49 status,
50 body
51 );
52 }
53 Err(e) => {
54 log::warn!(
55 "Uploaded asset {} but timezone update request failed: {}",
56 asset_id,
57 e
58 );
59 }
60 }
61}
62
63pub(super) fn file_timestamps(meta: &std::fs::Metadata) -> (u64, u64) {
65 let now = SystemTime::now()
66 .duration_since(UNIX_EPOCH)
67 .unwrap_or_default()
68 .as_secs();
69
70 let created = meta.created().ok().and_then(system_time_to_unix_secs);
71 let modified = meta.modified().ok().and_then(system_time_to_unix_secs);
72 let (created, modified) = normalize_file_timestamps(created, modified, now);
73
74 (created, modified)
75}
76
77pub(super) fn system_time_to_unix_secs(time: SystemTime) -> Option<u64> {
79 time.duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs())
80}
81
82pub(super) fn normalize_file_timestamps(
84 created: Option<u64>,
85 modified: Option<u64>,
86 now: u64,
87) -> (u64, u64) {
88 let created = match (created, modified) {
92 (Some(created), Some(modified)) => created.min(modified),
93 (Some(created), None) => created,
94 (None, Some(modified)) => modified,
95 (None, None) => now,
96 };
97
98 let modified = modified.unwrap_or(created);
99
100 (created, modified)
101}
102
103pub(super) fn unix_to_utc_iso8601(secs: u64) -> String {
105 Utc.timestamp_opt(secs as i64, 0)
106 .single()
107 .map(|dt| dt.to_rfc3339_opts(SecondsFormat::Millis, true))
108 .unwrap_or_else(|| "1970-01-01T00:00:00.000+00:00".to_string())
109}
110
111pub(super) fn local_timezone_name() -> Option<String> {
113 if let Ok(tz) = std::env::var("TZ") {
114 let tz = tz.trim().trim_start_matches(':');
115 if looks_like_iana_timezone(tz) {
116 return Some(tz.to_string());
117 }
118 }
119
120 if let Ok(target) = std::fs::read_link("/etc/localtime")
121 && let Some(path) = target.to_str()
122 && let Some((_, tz)) = path.split_once("/zoneinfo/")
123 && looks_like_iana_timezone(tz)
124 {
125 return Some(tz.to_string());
126 }
127
128 if let Ok(tz) = std::fs::read_to_string("/etc/timezone") {
129 let tz = tz.trim();
130 if looks_like_iana_timezone(tz) {
131 return Some(tz.to_string());
132 }
133 }
134
135 None
136}
137
138pub(super) fn looks_like_iana_timezone(value: &str) -> bool {
140 !value.is_empty() && value.contains('/') && !value.contains(' ')
141}