memvid_cli/
org_ticket_cache.rs1use std::fs;
7use std::path::PathBuf;
8
9use anyhow::{Context, Result};
10
11use crate::api::{fetch_org_ticket, OrgTicket, OrgTicketResponse};
12use crate::config::CliConfig;
13
14const ORG_TICKET_FILE: &str = "org_ticket.json";
15
16#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
18pub struct CachedOrgTicket {
19 pub ticket: OrgTicket,
20 pub plan_id: String,
21 pub plan_name: String,
22 pub org_id: String,
23 pub org_name: String,
24 pub total_storage_bytes: u64,
25 pub subscription_status: String,
26}
27
28impl CachedOrgTicket {
29 pub fn is_expired(&self) -> bool {
31 self.ticket.is_expired()
32 }
33
34 pub fn capacity_bytes(&self) -> u64 {
36 self.ticket.capacity_bytes
37 }
38
39 pub fn is_paid(&self) -> bool {
41 self.ticket.is_paid()
42 }
43}
44
45fn cache_path(config: &CliConfig) -> PathBuf {
47 config.cache_dir.join(ORG_TICKET_FILE)
48}
49
50pub fn load(config: &CliConfig) -> Result<CachedOrgTicket> {
52 let path = cache_path(config);
53 let data = fs::read(&path)
54 .with_context(|| "no cached org ticket available")?;
55 let cached: CachedOrgTicket = serde_json::from_slice(&data)
56 .with_context(|| format!("failed to parse cached org ticket: {}", path.display()))?;
57 Ok(cached)
58}
59
60pub fn store(config: &CliConfig, response: &OrgTicketResponse) -> Result<()> {
62 let path = cache_path(config);
63 if let Some(parent) = path.parent() {
64 fs::create_dir_all(parent).with_context(|| {
65 format!(
66 "failed to create org ticket cache directory: {}",
67 parent.display()
68 )
69 })?;
70 }
71
72 let cached = CachedOrgTicket {
73 ticket: response.ticket.clone(),
74 plan_id: response.plan.id.clone(),
75 plan_name: response.plan.name.clone(),
76 org_id: response.organisation.id.clone(),
77 org_name: response.organisation.name.clone(),
78 total_storage_bytes: response.organisation.total_storage_bytes,
79 subscription_status: response.subscription.status.clone(),
80 };
81
82 let tmp = path.with_extension("json.tmp");
83 let data = serde_json::to_vec_pretty(&cached)?;
84 fs::write(&tmp, data)
85 .with_context(|| format!("failed to write org ticket cache file: {}", tmp.display()))?;
86 fs::rename(&tmp, &path).with_context(|| {
87 format!(
88 "failed to atomically persist org ticket cache file: {}",
89 path.display()
90 )
91 })?;
92 Ok(())
93}
94
95pub fn clear(config: &CliConfig) -> Result<()> {
97 let path = cache_path(config);
98 if path.exists() {
99 fs::remove_file(&path).with_context(|| {
100 format!("failed to remove org ticket cache file: {}", path.display())
101 })?;
102 }
103 Ok(())
104}
105
106pub fn get_or_refresh(config: &CliConfig) -> Result<CachedOrgTicket> {
114 if let Ok(cached) = load(config) {
116 if !cached.is_expired() {
117 return Ok(cached);
118 }
119 log::debug!("Cached org ticket expired, refreshing...");
120 }
121
122 refresh(config)
124}
125
126pub fn refresh(config: &CliConfig) -> Result<CachedOrgTicket> {
128 log::debug!("Fetching org ticket from API...");
129 let response = fetch_org_ticket(config)?;
130 store(config, &response)?;
131 load(config)
132}
133
134pub fn get_optional(config: &CliConfig) -> Option<CachedOrgTicket> {
143 if config.api_key.is_none() {
145 log::debug!("No API key set, using free tier limits");
146 return None;
147 }
148
149 let needs_fetch = match load(config) {
151 Ok(cached) => cached.is_expired(),
152 Err(_) => true,
153 };
154
155 if needs_fetch {
156 log::debug!("Auto-syncing plan ticket from dashboard...");
158 match refresh(config) {
159 Ok(ticket) => {
160 log::info!(
161 "Plan synced: {} ({})",
162 ticket.plan_name,
163 format_capacity(ticket.capacity_bytes())
164 );
165 return Some(ticket);
166 }
167 Err(err) => {
168 let err_str = err.to_string();
170 if err_str.contains("Invalid API key") || err_str.contains("401") {
171 log::debug!("API key invalid, using free tier limits");
172 } else {
173 log::warn!("Failed to sync plan (using free tier limits): {}", err);
175 }
176 return None;
177 }
178 }
179 }
180
181 match load(config) {
183 Ok(ticket) => Some(ticket),
184 Err(_) => None,
185 }
186}
187
188fn format_capacity(bytes: u64) -> String {
190 if bytes >= 1024 * 1024 * 1024 * 1024 {
191 format!("{:.1} TB", bytes as f64 / (1024.0 * 1024.0 * 1024.0 * 1024.0))
192 } else if bytes >= 1024 * 1024 * 1024 {
193 format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
194 } else if bytes >= 1024 * 1024 {
195 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
196 } else if bytes >= 1024 {
197 format!("{:.1} KB", bytes as f64 / 1024.0)
198 } else {
199 format!("{} B", bytes)
200 }
201}