[pve-devel] [PATCH proxmox-offline-mirror 1/2] helper: allow URL as source for offline key activation

Hannes Laimer h.laimer at proxmox.com
Fri Oct 17 08:39:28 CEST 2025


If the HTTP server is setup like in our exmaple, `.mirror-state` is
accessible over HTTP. With this the `offline-key` subcommand also
accepts an URL as a source. So instead of reading the `.mirror-state`
file directly from the fs, we'd load it over HTTP if an URL specified.
This removes to need for mounting the medium directly just for key
activation.

Signed-off-by: Hannes Laimer <h.laimer at proxmox.com>
---
 src/bin/proxmox-offline-mirror-helper.rs | 63 ++++++++++++++++++------
 1 file changed, 49 insertions(+), 14 deletions(-)

diff --git a/src/bin/proxmox-offline-mirror-helper.rs b/src/bin/proxmox-offline-mirror-helper.rs
index 07537f0..849465c 100644
--- a/src/bin/proxmox-offline-mirror-helper.rs
+++ b/src/bin/proxmox-offline-mirror-helper.rs
@@ -5,6 +5,8 @@ use std::{collections::HashMap, path::Path};
 
 use anyhow::{Error, bail, format_err};
 
+use proxmox_http::client::sync::Client;
+use proxmox_http::{HttpClient, HttpOptions, ProxyConfig};
 use proxmox_offline_mirror::types::Snapshot;
 use proxmox_subscription::{ProductType, SubscriptionInfo};
 use proxmox_sys::command::run_command;
@@ -24,6 +26,48 @@ use proxmox_offline_mirror::helpers::tty::{
 };
 use proxmox_offline_mirror::medium::{self, MediumState, generate_repo_snippet};
 
+fn load_mirror_state(source: &str) -> Result<MediumState, Error> {
+    if source.starts_with("http://") || source.starts_with("https://") {
+        let state_url = if source.ends_with('/') {
+            format!("{}.mirror-state", source)
+        } else {
+            format!("{}/.mirror-state", source)
+        };
+
+        let options = HttpOptions {
+            user_agent: Some(
+                concat!("proxmox-offline-mirror-helper/", env!("CARGO_PKG_VERSION")).to_string(),
+            ),
+            proxy_config: ProxyConfig::from_proxy_env()?,
+            ..Default::default()
+        };
+        let client = Client::new(options);
+
+        let response = client.get(&state_url, None)?;
+        if !response.status().is_success() {
+            bail!(
+                "Failed to download mirror state from {}: {}",
+                state_url,
+                response.status()
+            );
+        }
+
+        let body: Vec<u8> = response.into_body();
+        serde_json::from_slice(&body).map_err(Error::from)
+    } else {
+        let mountpoint = Path::new(source);
+        if !mountpoint.exists() {
+            bail!("Medium mountpoint doesn't exist.");
+        }
+
+        let mut statefile = mountpoint.to_path_buf();
+        statefile.push(".mirror-state");
+
+        let raw = file_get_contents(&statefile)?;
+        serde_json::from_slice(&raw).map_err(Error::from)
+    }
+}
+
 fn set_subscription_key(
     product: &ProductType,
     subscription: &SubscriptionInfo,
@@ -264,9 +308,9 @@ async fn setup(_param: Value) -> Result<(), Error> {
 #[api(
     input: {
         properties: {
-            mountpoint: {
+            source: {
                 type: String,
-                description: "Path to medium mountpoint",
+                description: "Path to medium mountpoint or URL for key activation",
             },
             product: {
                 type: ProductType,
@@ -277,7 +321,7 @@ async fn setup(_param: Value) -> Result<(), Error> {
 )]
 /// Configures and offline subscription key
 async fn setup_offline_key(
-    mountpoint: String,
+    source: String,
     product: Option<ProductType>,
     _param: Value,
 ) -> Result<(), Error> {
@@ -288,17 +332,8 @@ async fn setup_offline_key(
         );
     }
 
-    let mountpoint = Path::new(&mountpoint);
-    if !mountpoint.exists() {
-        bail!("Medium mountpoint doesn't exist.");
-    }
-
-    let mut statefile = mountpoint.to_path_buf();
-    statefile.push(".mirror-state");
-
-    println!("Loading state from {statefile:?}..");
-    let raw = file_get_contents(&statefile)?;
-    let state: MediumState = serde_json::from_slice(&raw)?;
+    println!("Loading state from {}..", source);
+    let state = load_mirror_state(&source)?;
     println!(
         "Last sync timestamp: {}",
         epoch_to_rfc3339_utc(state.last_sync)?
-- 
2.47.3





More information about the pve-devel mailing list