public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Christian Ebner <c.ebner@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH proxmox-backup v5 28/46] datastore: prune groups/snapshots from s3 object store backend
Date: Thu,  3 Jul 2025 15:18:19 +0200	[thread overview]
Message-ID: <20250703131837.786811-32-c.ebner@proxmox.com> (raw)
In-Reply-To: <20250703131837.786811-1-c.ebner@proxmox.com>

When pruning a backup group or a backup snapshot for a datastore with
S3 object store backend, remove the associated objects by removing
them based on the prefix.

In order to exclude protected contents, add a filtering based on the
presence of the protected marker.

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
---
 pbs-datastore/src/backup_info.rs | 51 +++++++++++++++++++++++++++++---
 pbs-datastore/src/datastore.rs   | 44 +++++++++++++++++++++++----
 src/api2/admin/datastore.rs      | 24 ++++++++++-----
 3 files changed, 102 insertions(+), 17 deletions(-)

diff --git a/pbs-datastore/src/backup_info.rs b/pbs-datastore/src/backup_info.rs
index 1331a58dc..270c15836 100644
--- a/pbs-datastore/src/backup_info.rs
+++ b/pbs-datastore/src/backup_info.rs
@@ -9,6 +9,7 @@ use std::time::Duration;
 use anyhow::{bail, format_err, Context, Error};
 use const_format::concatcp;
 
+use pbs_s3_client::{S3PathPrefix, S3_CONTENT_PREFIX};
 use proxmox_sys::fs::{lock_dir_noblock, lock_dir_noblock_shared, replace_file, CreateOptions};
 use proxmox_systemd::escape_unit;
 
@@ -18,11 +19,12 @@ use pbs_api_types::{
 };
 use pbs_config::{open_backup_lockfile, BackupLockGuard};
 
+use crate::datastore::GROUP_NOTES_FILE_NAME;
 use crate::manifest::{BackupManifest, MANIFEST_LOCK_NAME};
 use crate::{DataBlob, DataStore, DatastoreBackend};
 
 pub const DATASTORE_LOCKS_DIR: &str = "/run/proxmox-backup/locks";
-const PROTECTED_MARKER_FILENAME: &str = ".protected";
+pub const PROTECTED_MARKER_FILENAME: &str = ".protected";
 
 proxmox_schema::const_regex! {
     pub BACKUP_FILES_AND_PROTECTED_REGEX = concatcp!(r"^(.*\.([fd]idx|blob)|\", PROTECTED_MARKER_FILENAME, ")$");
@@ -218,7 +220,7 @@ impl BackupGroup {
     ///
     /// Returns `BackupGroupDeleteStats`, containing the number of deleted snapshots
     /// and number of protected snaphsots, which therefore were not removed.
-    pub fn destroy(&self) -> Result<BackupGroupDeleteStats, Error> {
+    pub fn destroy(&self, backend: &DatastoreBackend) -> Result<BackupGroupDeleteStats, Error> {
         let _guard = self
             .lock()
             .with_context(|| format!("while destroying group '{self:?}'"))?;
@@ -232,10 +234,30 @@ impl BackupGroup {
                 delete_stats.increment_protected_snapshots();
                 continue;
             }
-            snap.destroy(false)?;
+            // also for S3 cleanup local only, the actual S3 objects will be removed below,
+            // reducing the number of required API calls.
+            snap.destroy(false, &DatastoreBackend::Filesystem)?;
             delete_stats.increment_removed_snapshots();
         }
 
+        if let DatastoreBackend::S3(s3_client) = backend {
+            let path = self.relative_group_path();
+            let group_prefix = path
+                .to_str()
+                .ok_or_else(|| format_err!("invalid group path prefix"))?;
+            let prefix = format!("{S3_CONTENT_PREFIX}/{group_prefix}");
+            let delete_objects_error = proxmox_async::runtime::block_on(
+                s3_client.delete_objects_by_prefix_with_suffix_filter(
+                    &S3PathPrefix::Some(prefix),
+                    PROTECTED_MARKER_FILENAME,
+                    &["owner", GROUP_NOTES_FILE_NAME],
+                ),
+            )?;
+            if delete_objects_error {
+                bail!("deleting objects failed");
+            }
+        }
+
         // Note: make sure the old locking mechanism isn't used as `remove_dir_all` is not safe in
         // that case
         if delete_stats.all_removed() && !*OLD_LOCKING {
@@ -588,7 +610,7 @@ impl BackupDir {
     /// Destroy the whole snapshot, bails if it's protected
     ///
     /// Setting `force` to true skips locking and thus ignores if the backup is currently in use.
-    pub fn destroy(&self, force: bool) -> Result<(), Error> {
+    pub fn destroy(&self, force: bool, backend: &DatastoreBackend) -> Result<(), Error> {
         let (_guard, _manifest_guard);
         if !force {
             _guard = self
@@ -601,6 +623,20 @@ impl BackupDir {
             bail!("cannot remove protected snapshot"); // use special error type?
         }
 
+        if let DatastoreBackend::S3(s3_client) = backend {
+            let path = self.relative_path();
+            let snapshot_prefix = path
+                .to_str()
+                .ok_or_else(|| format_err!("invalid snapshot path"))?;
+            let prefix = format!("{S3_CONTENT_PREFIX}/{snapshot_prefix}");
+            let delete_objects_error = proxmox_async::runtime::block_on(
+                s3_client.delete_objects_by_prefix(&S3PathPrefix::Some(prefix)),
+            )?;
+            if delete_objects_error {
+                bail!("deleting objects failed");
+            }
+        }
+
         let full_path = self.full_path();
         log::info!("removing backup snapshot {:?}", full_path);
         std::fs::remove_dir_all(&full_path).map_err(|err| {
@@ -630,6 +666,13 @@ impl BackupDir {
         // do to rectify the situation.
         if guard.is_ok() && group.list_backups()?.is_empty() && !*OLD_LOCKING {
             group.remove_group_dir()?;
+            if let DatastoreBackend::S3(s3_client) = backend {
+                let path = group.relative_group_path().join("owner");
+                let owner_key = path
+                    .to_str()
+                    .ok_or_else(|| format_err!("invalid group path prefix"))?;
+                proxmox_async::runtime::block_on(s3_client.delete_object(owner_key.into()))?;
+            }
         } else if let Err(err) = guard {
             log::debug!("{err:#}");
         }
diff --git a/pbs-datastore/src/datastore.rs b/pbs-datastore/src/datastore.rs
index e83941bfe..fe14e5bd5 100644
--- a/pbs-datastore/src/datastore.rs
+++ b/pbs-datastore/src/datastore.rs
@@ -29,8 +29,11 @@ use pbs_api_types::{
     S3ClientConfig, S3ClientSecretsConfig, UPID,
 };
 use pbs_config::BackupLockGuard;
+use pbs_s3_client::{S3PathPrefix, S3_CONTENT_PREFIX};
 
-use crate::backup_info::{BackupDir, BackupGroup, BackupInfo, OLD_LOCKING};
+use crate::backup_info::{
+    BackupDir, BackupGroup, BackupInfo, OLD_LOCKING, PROTECTED_MARKER_FILENAME,
+};
 use crate::chunk_store::ChunkStore;
 use crate::dynamic_index::{DynamicIndexReader, DynamicIndexWriter};
 use crate::fixed_index::{FixedIndexReader, FixedIndexWriter};
@@ -42,7 +45,7 @@ use crate::DataBlob;
 static DATASTORE_MAP: LazyLock<Mutex<HashMap<String, Arc<DataStoreImpl>>>> =
     LazyLock::new(|| Mutex::new(HashMap::new()));
 
-const GROUP_NOTES_FILE_NAME: &str = "notes";
+pub const GROUP_NOTES_FILE_NAME: &str = "notes";
 const NAMESPACE_MARKER_FILENAME: &str = ".namespace";
 
 /// checks if auth_id is owner, or, if owner is a token, if
@@ -656,7 +659,9 @@ impl DataStore {
         let mut stats = BackupGroupDeleteStats::default();
 
         for group in self.iter_backup_groups(ns.to_owned())? {
-            let delete_stats = group?.destroy()?;
+            let group = group?;
+            let backend = self.backend()?;
+            let delete_stats = group.destroy(&backend)?;
             stats.add(&delete_stats);
             removed_all_groups = removed_all_groups && delete_stats.all_removed();
         }
@@ -690,6 +695,8 @@ impl DataStore {
         let store = self.name();
         let mut removed_all_requested = true;
         let mut stats = BackupGroupDeleteStats::default();
+        let backend = self.backend()?;
+
         if delete_groups {
             log::info!("removing whole namespace recursively below {store}:/{ns}",);
             for ns in self.recursive_iter_backup_ns(ns.to_owned())? {
@@ -697,6 +704,24 @@ impl DataStore {
                 stats.add(&delete_stats);
                 removed_all_requested = removed_all_requested && removed_ns_groups;
             }
+
+            if let DatastoreBackend::S3(s3_client) = &backend {
+                let ns_dir = ns.path();
+                let ns_prefix = ns_dir
+                    .to_str()
+                    .ok_or_else(|| format_err!("invalid namespace path prefix"))?;
+                let prefix = format!("{S3_CONTENT_PREFIX}/{ns_prefix}");
+                let delete_objects_error = proxmox_async::runtime::block_on(
+                    s3_client.delete_objects_by_prefix_with_suffix_filter(
+                        &S3PathPrefix::Some(prefix),
+                        PROTECTED_MARKER_FILENAME,
+                        &["owner", GROUP_NOTES_FILE_NAME],
+                    ),
+                )?;
+                if delete_objects_error {
+                    bail!("deleting objects failed");
+                }
+            }
         } else {
             log::info!("pruning empty namespace recursively below {store}:/{ns}");
         }
@@ -732,6 +757,15 @@ impl DataStore {
                         log::warn!("failed to remove namespace {ns} - {err}")
                     }
                 }
+                if let DatastoreBackend::S3(s3_client) = &backend {
+                    // Only remove the namespace marker, if it was empty,
+                    // than this is the same as the namespace being removed.
+                    let ns_dir = ns.path().join(NAMESPACE_MARKER_FILENAME);
+                    let ns_key = ns_dir
+                        .to_str()
+                        .ok_or_else(|| format_err!("invalid namespace path"))?;
+                    proxmox_async::runtime::block_on(s3_client.delete_object(ns_key.into()))?;
+                }
             }
         }
 
@@ -749,7 +783,7 @@ impl DataStore {
     ) -> Result<BackupGroupDeleteStats, Error> {
         let backup_group = self.backup_group(ns.clone(), backup_group.clone());
 
-        backup_group.destroy()
+        backup_group.destroy(&self.backend()?)
     }
 
     /// Remove a backup directory including all content
@@ -761,7 +795,7 @@ impl DataStore {
     ) -> Result<(), Error> {
         let backup_dir = self.backup_dir(ns.clone(), backup_dir.clone())?;
 
-        backup_dir.destroy(force)
+        backup_dir.destroy(force, &self.backend()?)
     }
 
     /// Returns the time of the last successful backup
diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs
index 753978425..e82014fbb 100644
--- a/src/api2/admin/datastore.rs
+++ b/src/api2/admin/datastore.rs
@@ -422,7 +422,7 @@ pub async fn delete_snapshot(
 
         let snapshot = datastore.backup_dir(ns, backup_dir)?;
 
-        snapshot.destroy(false)?;
+        snapshot.destroy(false, &datastore.backend()?)?;
 
         Ok(Value::Null)
     })
@@ -1088,13 +1088,21 @@ pub fn prune(
             });
 
             if !keep {
-                if let Err(err) = backup_dir.destroy(false) {
-                    warn!(
-                        "failed to remove dir {:?}: {}",
-                        backup_dir.relative_path(),
-                        err,
-                    );
-                }
+                match datastore.backend() {
+                    Ok(backend) => {
+                        if let Err(err) = backup_dir.destroy(false, &backend) {
+                            warn!(
+                                "failed to remove dir {:?}: {}",
+                                backup_dir.relative_path(),
+                                err,
+                            );
+                        }
+                    }
+                    Err(err) => warn!(
+                        "failed to remove dir {:?}: {err}",
+                        backup_dir.relative_path()
+                    ),
+                };
             }
         }
         prune_result
-- 
2.47.2



_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel


  parent reply	other threads:[~2025-07-03 13:24 UTC|newest]

Thread overview: 57+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-07-03 13:17 [pbs-devel] [PATCH proxmox{, -backup} v5 00/49] fix #2943: S3 storage backend for datastores Christian Ebner
2025-07-03 13:17 ` [pbs-devel] [PATCH proxmox v5 1/3] pbs-api-types: add types for S3 client configs and secrets Christian Ebner
2025-07-04 11:37   ` Thomas Lamprecht
2025-07-04 11:56     ` Christian Ebner
2025-07-03 13:17 ` [pbs-devel] [PATCH proxmox v5 2/3] pbs-api-types: extend datastore config by backend config enum Christian Ebner
2025-07-03 13:17 ` [pbs-devel] [PATCH proxmox v5 3/3] pbs-api-types: maintenance: add new maintenance mode S3 refresh Christian Ebner
2025-07-03 13:17 ` [pbs-devel] [PATCH proxmox-backup v5 01/46] api: fix minor formatting issues Christian Ebner
2025-07-04 11:11   ` [pbs-devel] applied: " Thomas Lamprecht
2025-07-03 13:17 ` [pbs-devel] [PATCH proxmox-backup v5 02/46] bin: sort submodules alphabetically Christian Ebner
2025-07-04 11:11   ` [pbs-devel] applied: " Thomas Lamprecht
2025-07-03 13:17 ` [pbs-devel] [PATCH proxmox-backup v5 03/46] datastore: ignore missing owner file when removing group directory Christian Ebner
2025-07-04 11:11   ` [pbs-devel] applieapplied: " Thomas Lamprecht
2025-07-03 13:17 ` [pbs-devel] [PATCH proxmox-backup v5 04/46] verify: refactor verify related functions to be methods of worker Christian Ebner
2025-07-04 11:16   ` [pbs-devel] applied: " Thomas Lamprecht
2025-07-03 13:17 ` [pbs-devel] [PATCH proxmox-backup v5 05/46] s3 client: add crate for AWS s3 compatible object store client Christian Ebner
2025-07-03 13:17 ` [pbs-devel] [PATCH proxmox-backup v5 06/46] s3 client: implement AWS signature v4 request authentication Christian Ebner
2025-07-03 13:17 ` [pbs-devel] [PATCH proxmox-backup v5 07/46] s3 client: add dedicated type for s3 object keys Christian Ebner
2025-07-03 13:17 ` [pbs-devel] [PATCH proxmox-backup v5 08/46] s3 client: add type for last modified timestamp in responses Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 09/46] s3 client: add helper to parse http date headers Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 10/46] s3 client: implement methods to operate on s3 objects in bucket Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 11/46] config: introduce s3 object store client configuration Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 12/46] api: config: implement endpoints to manipulate and list s3 configs Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 13/46] api: datastore: check s3 backend bucket access on datastore create Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 14/46] api/cli: add endpoint and command to check s3 client connection Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 15/46] datastore: allow to get the backend for a datastore Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 16/46] api: backup: store datastore backend in runtime environment Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 17/46] api: backup: conditionally upload chunks to s3 object store backend Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 18/46] api: backup: conditionally upload blobs " Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 19/46] api: backup: conditionally upload indices " Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 20/46] api: backup: conditionally upload manifest " Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 21/46] sync: pull: conditionally upload content to s3 backend Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 22/46] api: reader: fetch chunks based on datastore backend Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 23/46] datastore: local chunk reader: read chunks based on backend Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 24/46] verify worker: add datastore backed to verify worker Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 25/46] verify: implement chunk verification for stores with s3 backend Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 26/46] datastore: create namespace marker in " Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 27/46] datastore: create/delete protected marker file on s3 storage backend Christian Ebner
2025-07-03 13:18 ` Christian Ebner [this message]
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 29/46] datastore: get and set owner for s3 store backend Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 30/46] datastore: implement garbage collection for s3 backend Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 31/46] ui: add datastore type selector and reorganize component layout Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 32/46] ui: add s3 client edit window for configuration create/edit Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 33/46] ui: add s3 client view for configuration Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 34/46] ui: expose the s3 client view in the navigation tree Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 35/46] ui: add s3 client selector and bucket field for s3 backend setup Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 36/46] tools: lru cache: add removed callback for evicted cache nodes Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 37/46] tools: async lru cache: implement insert, remove and contains methods Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 38/46] datastore: add local datastore cache for network attached storages Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 39/46] api: backup: use local datastore cache on s3 backend chunk upload Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 40/46] api: reader: use local datastore cache on s3 backend chunk fetching Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 41/46] datastore: local chunk reader: get cached chunk from local cache store Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 42/46] api: backup: add no-cache flag to bypass local datastore cache Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 43/46] api/datastore: implement refresh endpoint for stores with s3 backend Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 44/46] cli: add dedicated subcommand for datastore s3 refresh Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 45/46] ui: render s3 refresh as valid maintenance type and task description Christian Ebner
2025-07-03 13:18 ` [pbs-devel] [PATCH proxmox-backup v5 46/46] ui: expose s3 refresh button for datastores backed by object store Christian Ebner
2025-07-08 17:05 ` [pbs-devel] superseded: [PATCH proxmox{, -backup} v5 00/49] fix #2943: S3 storage backend for datastores Christian Ebner

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250703131837.786811-32-c.ebner@proxmox.com \
    --to=c.ebner@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal