winbrew_app\operations\update/
mod.rs1mod api;
46mod download;
47mod metadata;
48mod patch;
49mod planner;
50mod types;
51
52use anyhow::{Context, Result, bail};
53use std::path::Path;
54
55use self::types::CatalogDownloadPlan;
56
57use crate::core::fs::{cleanup_path, finalize_temp_file};
58use crate::core::network::{Client, build_client};
59use crate::core::paths::ResolvedPaths;
60
61const CATALOG_UPDATE_API_URL: &str = "https://api.winbrew.dev/v1/update";
62
63pub fn refresh_catalog<FStart, FProgress>(
69 paths: &ResolvedPaths,
70 on_start: FStart,
71 on_progress: FProgress,
72) -> Result<()>
73where
74 FStart: FnOnce(Option<u64>),
75 FProgress: FnMut(u64),
76{
77 refresh_catalog_with_api_url(paths, CATALOG_UPDATE_API_URL, on_start, on_progress)
78}
79
80#[doc(hidden)]
87pub fn refresh_catalog_with_api_url<FStart, FProgress>(
88 paths: &ResolvedPaths,
89 update_api_url: &str,
90 on_start: FStart,
91 on_progress: FProgress,
92) -> Result<()>
93where
94 FStart: FnOnce(Option<u64>),
95 FProgress: FnMut(u64),
96{
97 let catalog_path = paths.catalog_db.clone();
98 let catalog_dir = catalog_path
99 .parent()
100 .context("failed to resolve catalog database directory")?;
101
102 let catalog_temp_path = catalog_dir.join("catalog.db.download");
103 let metadata_temp_path = catalog_dir.join("metadata.json.download");
104 let metadata_path = catalog_dir.join("metadata.json");
105
106 let result = (|| -> Result<()> {
107 clear_temp_file(&catalog_temp_path)?;
108 clear_temp_file(&metadata_temp_path)?;
109
110 let client = build_client("winbrew-catalog-downloader")?;
111 let local_metadata = metadata::load_local_catalog_metadata(&metadata_path)?;
112
113 let selection =
114 api::fetch_catalog_update_selection(&client, update_api_url, local_metadata.as_ref())?;
115 let download_plan =
116 match planner::plan_catalog_download(local_metadata.as_ref(), selection)? {
117 Some(plan) => plan,
118 None => request_full_snapshot_plan(&client, update_api_url)?,
119 };
120
121 match &download_plan {
122 CatalogDownloadPlan::Current {
123 current_hash,
124 target_hash,
125 } => {
126 if current_hash != target_hash {
127 tracing::warn!(current_hash = %current_hash, target_hash = %target_hash, "update worker reported a current plan with mismatched hashes");
128 }
129
130 return Ok(());
131 }
132 CatalogDownloadPlan::Full { .. } => {
133 download::download_catalog_release(
134 &client,
135 &download_plan,
136 &catalog_temp_path,
137 &metadata_temp_path,
138 on_start,
139 on_progress,
140 )?;
141 }
142 CatalogDownloadPlan::Patch {
143 patch_urls,
144 expected_hash,
145 } => {
146 let previous_metadata = local_metadata
147 .as_ref()
148 .context("patch updates require local catalog metadata")?;
149
150 if let Err(err) = patch::apply_catalog_patch_release(
151 &client,
152 &catalog_path,
153 &catalog_temp_path,
154 &metadata_temp_path,
155 patch_urls,
156 expected_hash,
157 previous_metadata.current_hash.as_str(),
158 ) {
159 tracing::warn!(error = %err, "patch catalog update failed; falling back to full snapshot");
160 clear_temp_file(&catalog_temp_path)?;
161 clear_temp_file(&metadata_temp_path)?;
162
163 let fallback_plan = request_full_snapshot_plan(&client, update_api_url)?;
164 download::download_catalog_release(
165 &client,
166 &fallback_plan,
167 &catalog_temp_path,
168 &metadata_temp_path,
169 on_start,
170 on_progress,
171 )?;
172 }
173 }
174 }
175
176 finalize_temp_file(&catalog_temp_path, &catalog_path)?;
177 finalize_temp_file(&metadata_temp_path, &metadata_path)?;
178
179 Ok(())
180 })();
181
182 let _ = cleanup_path(&catalog_temp_path);
183 let _ = cleanup_path(&metadata_temp_path);
184
185 result
186}
187
188fn request_full_snapshot_plan(
189 client: &Client,
190 update_api_url: &str,
191) -> Result<CatalogDownloadPlan> {
192 let selection = api::fetch_full_snapshot_update_selection(client, update_api_url)?;
193
194 match planner::plan_catalog_download(None, selection)? {
195 Some(plan @ CatalogDownloadPlan::Full { .. }) => Ok(plan),
196 _ => bail!("update API did not return a full snapshot plan"),
197 }
198}
199
200fn clear_temp_file(path: &Path) -> Result<()> {
201 cleanup_path(path).context("failed to clear previous catalog download")
202}