From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: <pbs-devel-bounces@lists.proxmox.com> Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 9DCC81FF165 for <inbox@lore.proxmox.com>; Thu, 8 May 2025 15:06:33 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 3D4ABFE47; Thu, 8 May 2025 15:06:51 +0200 (CEST) From: Christian Ebner <c.ebner@proxmox.com> To: pbs-devel@lists.proxmox.com Date: Thu, 8 May 2025 15:05:52 +0200 Message-Id: <20250508130555.494782-19-c.ebner@proxmox.com> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250508130555.494782-1-c.ebner@proxmox.com> References: <20250508130555.494782-1-c.ebner@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.121 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_1 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_3 0.1 random spam to be learned in bayes SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pbs-devel] [RFC v2 proxmox-backup 18/21] api: admin: implement endpoints to restore trashed contents X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion <pbs-devel.lists.proxmox.com> List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pbs-devel>, <mailto:pbs-devel-request@lists.proxmox.com?subject=unsubscribe> List-Archive: <http://lists.proxmox.com/pipermail/pbs-devel/> List-Post: <mailto:pbs-devel@lists.proxmox.com> List-Help: <mailto:pbs-devel-request@lists.proxmox.com?subject=help> List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel>, <mailto:pbs-devel-request@lists.proxmox.com?subject=subscribe> Reply-To: Proxmox Backup Server development discussion <pbs-devel@lists.proxmox.com> Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pbs-devel-bounces@lists.proxmox.com Sender: "pbs-devel" <pbs-devel-bounces@lists.proxmox.com> Implements the api endpoints to restore trashed contents contained within namespaces, backup groups or individual snapshots. Signed-off-by: Christian Ebner <c.ebner@proxmox.com> --- src/api2/admin/datastore.rs | 173 +++++++++++++++++++++++++++++++++++- 1 file changed, 172 insertions(+), 1 deletion(-) diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs index cbd24c729..eb033c3fc 100644 --- a/src/api2/admin/datastore.rs +++ b/src/api2/admin/datastore.rs @@ -51,7 +51,7 @@ use pbs_api_types::{ }; use pbs_client::pxar::{create_tar, create_zip}; use pbs_config::CachedUserInfo; -use pbs_datastore::backup_info::{BackupInfo, ListBackupFilter}; +use pbs_datastore::backup_info::{BackupInfo, ListBackupFilter, TRASH_MARKER_FILENAME}; use pbs_datastore::cached_chunk_reader::CachedChunkReader; use pbs_datastore::catalog::{ArchiveEntry, CatalogReader}; use pbs_datastore::data_blob::DataBlob; @@ -2727,6 +2727,165 @@ pub async fn unmount(store: String, rpcenv: &mut dyn RpcEnvironment) -> Result<V Ok(json!(upid)) } +#[api( + input: { + properties: { + store: { schema: DATASTORE_SCHEMA }, + ns: { type: BackupNamespace, }, + }, + }, + access: { + permission: &Permission::Anybody, + description: "Requires on /datastore/{store}[/{namespace}] either DATASTORE_MODIFY for any \ + or DATASTORE_BACKUP and being the owner of the group", + }, +)] +/// Recover trashed contents of a namespace. +pub fn recover_namespace( + store: String, + ns: BackupNamespace, + rpcenv: &mut dyn RpcEnvironment, +) -> Result<(), Error> { + let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; + let limited = check_ns_privs_full( + &store, + &ns, + &auth_id, + PRIV_DATASTORE_MODIFY, + PRIV_DATASTORE_BACKUP, + )?; + + let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?; + + for backup_group in datastore.iter_backup_groups(ns.clone())? { + let backup_group = backup_group?; + if limited { + let owner = datastore.get_owner(&ns, backup_group.group())?; + if check_backup_owner(&owner, &auth_id).is_err() { + continue; + } + } + do_recover_group(&backup_group)?; + } + + Ok(()) +} + +#[api( + input: { + properties: { + store: { schema: DATASTORE_SCHEMA }, + group: { + type: pbs_api_types::BackupGroup, + flatten: true, + }, + ns: { + type: BackupNamespace, + optional: true, + }, + }, + }, + access: { + permission: &Permission::Anybody, + description: "Requires on /datastore/{store}[/{namespace}] either DATASTORE_MODIFY for any \ + or DATASTORE_BACKUP and being the owner of the group", + }, +)] +/// Recover trashed contents of a backup group. +pub fn recover_group( + store: String, + group: pbs_api_types::BackupGroup, + ns: Option<BackupNamespace>, + rpcenv: &mut dyn RpcEnvironment, +) -> Result<(), Error> { + let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; + let ns = ns.unwrap_or_default(); + let datastore = check_privs_and_load_store( + &store, + &ns, + &auth_id, + PRIV_DATASTORE_MODIFY, + PRIV_DATASTORE_BACKUP, + Some(Operation::Write), + &group, + )?; + + let backup_group = datastore.backup_group(ns, group); + do_recover_group(&backup_group)?; + + Ok(()) +} + +fn do_recover_group(backup_group: &BackupGroup) -> Result<(), Error> { + let trashed_snapshots = backup_group.list_backups(ListBackupFilter::Trashed)?; + for snapshot in trashed_snapshots { + do_recover_snapshot(&snapshot.backup_dir)?; + } + + let group_trash_path = backup_group.full_group_path().join(TRASH_MARKER_FILENAME); + if let Err(err) = std::fs::remove_file(&group_trash_path) { + if err.kind() != std::io::ErrorKind::NotFound { + bail!("failed to remove group trash file {group_trash_path:?} - {err}"); + } + } + Ok(()) +} + +#[api( + input: { + properties: { + store: { schema: DATASTORE_SCHEMA }, + backup_dir: { + type: pbs_api_types::BackupDir, + flatten: true, + }, + ns: { + type: BackupNamespace, + optional: true, + }, + }, + }, + access: { + permission: &Permission::Anybody, + description: "Requires on /datastore/{store}[/{namespace}] either DATASTORE_MODIFY for any \ + or DATASTORE_BACKUP and being the owner of the group", + }, +)] +/// Recover trashed contents of a backup snapshot. +pub fn recover_snapshot( + store: String, + backup_dir: pbs_api_types::BackupDir, + ns: Option<BackupNamespace>, + rpcenv: &mut dyn RpcEnvironment, +) -> Result<(), Error> { + let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?; + let ns = ns.unwrap_or_default(); + let datastore = check_privs_and_load_store( + &store, + &ns, + &auth_id, + PRIV_DATASTORE_MODIFY, + PRIV_DATASTORE_BACKUP, + Some(Operation::Write), + &backup_dir.group, + )?; + + let snapshot = datastore.backup_dir(ns, backup_dir)?; + do_recover_snapshot(&snapshot)?; + + Ok(()) +} + +fn do_recover_snapshot(snapshot_dir: &BackupDir) -> Result<(), Error> { + let trash_path = snapshot_dir.full_path().join(TRASH_MARKER_FILENAME); + if let Err(err) = std::fs::remove_file(&trash_path) { + if err.kind() != std::io::ErrorKind::NotFound { + bail!("failed to remove trash file {trash_path:?} - {err}"); + } + } + Ok(()) +} + #[sortable] const DATASTORE_INFO_SUBDIRS: SubdirMap = &[ ( @@ -2792,6 +2951,18 @@ const DATASTORE_INFO_SUBDIRS: SubdirMap = &[ "pxar-file-download", &Router::new().download(&API_METHOD_PXAR_FILE_DOWNLOAD), ), + ( + "recover-group", + &Router::new().post(&API_METHOD_RECOVER_GROUP), + ), + ( + "recover-namespace", + &Router::new().post(&API_METHOD_RECOVER_NAMESPACE), + ), + ( + "recover-snapshot", + &Router::new().post(&API_METHOD_RECOVER_SNAPSHOT), + ), ("rrd", &Router::new().get(&API_METHOD_GET_RRD_STATS)), ( "snapshots", -- 2.39.5 _______________________________________________ pbs-devel mailing list pbs-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel