From: Christian Ebner <c.ebner@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [PATCH proxmox-backup] api/tools: avoid showing error on missing manifest during file listing
Date: Sun, 26 Apr 2026 10:03:46 +0200 [thread overview]
Message-ID: <20260426080346.159579-1-c.ebner@proxmox.com> (raw)
When listing the contents of a datastore, a missing manifest blob
file is currently being logged as error to the systemd journal [0].
The manifest missing is however normal operation in case of a still
ongoing backup. Therefore, refactor the code such that a missing
manifest is not treated as regular error by returning an Option::None
in the read_backup_index() helper, and handle this case accordingly.
The actual check for the missing manifest should further be improved,
but requires a more in depth refactoring, the changes here acting as
a stop gap for not showing the benign error the time being.
[0] error during snapshot file listing: 'unable to load blob '"/mnt/datastore/main/vm/400004/2026-04-24T15:20:18Z/index.json.blob"' - No such file or directory (os error 2)'
Reported-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
---
src/api2/admin/datastore.rs | 18 ++++++---
src/tools/mod.rs | 79 ++++++++++++++++++++++---------------
2 files changed, 60 insertions(+), 37 deletions(-)
diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs
index a814c076c..3e52cd581 100644
--- a/src/api2/admin/datastore.rs
+++ b/src/api2/admin/datastore.rs
@@ -413,11 +413,13 @@ pub async fn list_snapshot_files(
&backup_dir.group,
)?;
- let snapshot = datastore.backup_dir(ns, backup_dir)?;
+ let snapshot = datastore.backup_dir(ns, backup_dir.clone())?;
let info = BackupInfo::new(snapshot)?;
- let (_manifest, files) = get_all_snapshot_files(&info)?;
+ let Some((_manifest, files)) = get_all_snapshot_files(&info)? else {
+ bail!("manifest not found for snapshot '{backup_dir}'");
+ };
Ok(files)
})
@@ -1500,7 +1502,9 @@ pub fn download_file_decoded(
required_string_param(¶m, "file-name")?.try_into()?;
let backup_dir = datastore.backup_dir(backup_ns.clone(), backup_dir_api.clone())?;
- let (manifest, files) = read_backup_index(&backup_dir)?;
+ let Some((manifest, files)) = read_backup_index(&backup_dir)? else {
+ bail!("manifest not found for snapshot '{}'", backup_dir.dir());
+ };
for file in files {
if file.filename == file_name.as_ref() && file.crypt_mode == Some(CryptMode::Encrypt) {
bail!("cannot decode '{}' - is encrypted", file_name);
@@ -1725,7 +1729,9 @@ pub async fn catalog(
let backup_dir = datastore.backup_dir(ns, backup_dir)?;
- let (manifest, files) = read_backup_index(&backup_dir)?;
+ let Some((manifest, files)) = read_backup_index(&backup_dir)? else {
+ bail!("manifest not found for snapshot '{}'", backup_dir.dir());
+ };
for file in files {
if file.filename == file_name.as_ref() && file.crypt_mode == Some(CryptMode::Encrypt) {
bail!("cannot decode '{file_name}' - is encrypted");
@@ -1866,7 +1872,9 @@ pub fn pxar_file_download(
(pxar_name.to_owned(), file_path.to_owned())
};
let pxar_name: BackupArchiveName = std::str::from_utf8(&pxar_name)?.try_into()?;
- let (manifest, files) = read_backup_index(&backup_dir)?;
+ let Some((manifest, files)) = read_backup_index(&backup_dir)? else {
+ bail!("manifest not found for snapshot '{}'", backup_dir.dir());
+ };
for file in files {
if file.filename == pxar_name.as_ref() && file.crypt_mode == Some(CryptMode::Encrypt) {
bail!("cannot decode '{}' - is encrypted", pxar_name);
diff --git a/src/tools/mod.rs b/src/tools/mod.rs
index 56ae4bc10..7c2bcfd61 100644
--- a/src/tools/mod.rs
+++ b/src/tools/mod.rs
@@ -46,8 +46,19 @@ pub fn setup_safe_path_env() {
pub(crate) fn read_backup_index(
backup_dir: &BackupDir,
-) -> Result<(BackupManifest, Vec<BackupContent>), Error> {
- let (manifest, index_size) = backup_dir.load_manifest()?;
+) -> Result<Option<(BackupManifest, Vec<BackupContent>)>, Error> {
+ let (manifest, index_size) = match backup_dir.load_manifest() {
+ Ok((manifest, index_size)) => (manifest, index_size),
+ Err(err) => {
+ let mut manifest_path = backup_dir.full_path();
+ manifest_path.push(MANIFEST_BLOB_NAME.as_ref());
+ if !manifest_path.exists() {
+ return Ok(None);
+ } else {
+ return Err(err);
+ }
+ }
+ };
let mut result = Vec::new();
for item in manifest.files() {
@@ -67,13 +78,15 @@ pub(crate) fn read_backup_index(
size: Some(index_size),
});
- Ok((manifest, result))
+ Ok(Some((manifest, result)))
}
pub(crate) fn get_all_snapshot_files(
info: &BackupInfo,
-) -> Result<(BackupManifest, Vec<BackupContent>), Error> {
- let (manifest, mut files) = read_backup_index(&info.backup_dir)?;
+) -> Result<Option<(BackupManifest, Vec<BackupContent>)>, Error> {
+ let Some((manifest, mut files)) = read_backup_index(&info.backup_dir)? else {
+ return Ok(None);
+ };
let file_set = files.iter().fold(HashSet::new(), |mut acc, item| {
acc.insert(item.filename.clone());
@@ -91,7 +104,7 @@ pub(crate) fn get_all_snapshot_files(
});
}
- Ok((manifest, files))
+ Ok(Some((manifest, files)))
}
/// Helper to transform `BackupInfo` to `SnapshotListItem` with given owner.
@@ -104,7 +117,7 @@ pub(crate) fn backup_info_to_snapshot_list_item(
let owner = Some(owner.to_owned());
match get_all_snapshot_files(info) {
- Ok((manifest, files)) => {
+ Ok(Some((manifest, files))) => {
// extract the first line from notes
let comment: Option<String> = manifest.unprotected["notes"]
.as_str()
@@ -129,7 +142,7 @@ pub(crate) fn backup_info_to_snapshot_list_item(
let size = Some(files.iter().map(|x| x.size.unwrap_or(0)).sum());
- SnapshotListItem {
+ return SnapshotListItem {
backup,
comment,
verification,
@@ -138,31 +151,33 @@ pub(crate) fn backup_info_to_snapshot_list_item(
size,
owner,
protected,
- }
- }
- Err(err) => {
- eprintln!("error during snapshot file listing: '{err}'");
- let files = info
- .files
- .iter()
- .map(|filename| BackupContent {
- filename: filename.to_owned(),
- size: None,
- crypt_mode: None,
- })
- .collect();
-
- SnapshotListItem {
- backup,
- comment: None,
- verification: None,
- fingerprint: None,
- files,
- size: None,
- owner,
- protected,
- }
+ };
}
+ Ok(None) => (),
+ Err(err) => eprintln!("error during snapshot file listing: '{err}'"),
+ }
+
+ // no manifest (possibly ongoing backup) or manifest read error,
+ // fallback to listing without any additional metadata.
+ let files = info
+ .files
+ .iter()
+ .map(|filename| BackupContent {
+ filename: filename.to_owned(),
+ size: None,
+ crypt_mode: None,
+ })
+ .collect();
+
+ SnapshotListItem {
+ backup,
+ comment: None,
+ verification: None,
+ fingerprint: None,
+ files,
+ size: None,
+ owner,
+ protected,
}
}
--
2.47.3
reply other threads:[~2026-04-26 8:04 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
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=20260426080346.159579-1-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