From: Michal Fox <me@dualfroz.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH storage] fix #6969: plugin: keep content directory when freeing its last file
Date: Sat, 26 Sep 2026 09:32:57 +0000 [thread overview]
Message-ID: <20260926093257.7-1-me@dualfroz.com> (raw)
After removing a volume, free_image tries to remove the parent
directory, so that no empty per-guest directories are left behind once
all images of a guest are gone. This was done unconditionally, so
deleting the last ISO, container template, snippet or import file also
removed the shared content directory like 'snippets' or 'template/iso'.
The directory gets re-created shortly after by pvestatd, but as
root:root with default permissions, losing any ownership or mode that
the admin set up, for example to allow a non-root user to upload
snippets.
Only clean up the parent directory for guest images, which are the only
volumes stored in per-guest directories.
Signed-off-by: Michal Fox <me@dualfroz.com>
---
src/PVE/Storage/Plugin.pm | 6 ++---
src/test/free_image_test.pm | 51 ++++++++++++++++++++++++++++++++++++
src/test/run_plugin_tests.pl | 1 +
3 files changed, 55 insertions(+), 3 deletions(-)
create mode 100644 src/test/free_image_test.pm
diff --git a/src/PVE/Storage/Plugin.pm b/src/PVE/Storage/Plugin.pm
index a9e1751..d499fd9 100644
--- a/src/PVE/Storage/Plugin.pm
+++ b/src/PVE/Storage/Plugin.pm
@@ -1166,9 +1166,9 @@ sub free_image {
}
# try to cleanup directory to not clutter storage with empty $vmid dirs if
- # all images from a guest got deleted
- my $dir = dirname($path);
- rmdir($dir);
+ # all images from a guest got deleted, but keep the shared content type dirs
+ my ($vtype) = $class->parse_volname($volname);
+ rmdir(dirname($path)) if $vtype eq 'images';
return undef;
}
diff --git a/src/test/free_image_test.pm b/src/test/free_image_test.pm
new file mode 100644
index 0000000..6161866
--- /dev/null
+++ b/src/test/free_image_test.pm
@@ -0,0 +1,51 @@
+package PVE::Storage::TestFreeImage;
+
+use strict;
+use warnings;
+
+use lib qw(..);
+
+use File::Path qw(make_path);
+use File::Temp;
+use PVE::Storage;
+use PVE::Tools qw(file_set_contents);
+use Test::More;
+
+my $storage_dir = File::Temp->newdir();
+my $scfg = { type => 'dir', path => "$storage_dir" };
+
+# each test is comprised of the following array keys:
+# [0] => volname of the only volume in its directory, freed by the test
+# [1] => parent directory of the volume, relative to the storage path
+# [2] => whether the now empty parent directory is expected to be kept
+my $tests = [
+ ['100/vm-100-disk-0.raw', 'images/100', 0],
+ ['iso/some.iso', 'template/iso', 1],
+ ['vztmpl/some.tar.zst', 'template/cache', 1],
+ ['snippets/hook.pl', 'snippets', 1],
+ ['import/some.ova', 'import', 1],
+];
+
+plan tests => 2 * scalar(@$tests);
+
+for my $tt (@$tests) {
+ my ($volname, $subdir, $keep_dir) = @$tt;
+
+ my $path = PVE::Storage::DirPlugin->filesystem_path($scfg, $volname);
+ make_path("$storage_dir/$subdir");
+ file_set_contents($path, '');
+
+ my $format = (PVE::Storage::DirPlugin->parse_volname($volname))[6];
+ PVE::Storage::DirPlugin->free_image('local', $scfg, $volname, 0, $format);
+
+ ok(!-e $path, "$volname - volume removed");
+ is(
+ !!-d "$storage_dir/$subdir",
+ !!$keep_dir,
+ "$volname - empty directory '$subdir' " . ($keep_dir ? 'kept' : 'removed'),
+ );
+}
+
+done_testing();
+
+1;
diff --git a/src/test/run_plugin_tests.pl b/src/test/run_plugin_tests.pl
index 8bce9d3..2e1c64f 100755
--- a/src/test/run_plugin_tests.pl
+++ b/src/test/run_plugin_tests.pl
@@ -17,6 +17,7 @@ my $res = $harness->runtests(
"get_subdir_test.pm",
"filesystem_path_test.pm",
"prune_backups_test.pm",
+ "free_image_test.pm",
);
exit -1 if !$res || $res->{failed} || $res->{parse_errors};
--
2.43.0
next reply other threads:[~2026-09-28 7:17 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-26 9:32 Michal Fox [this message]
2026-09-28 13:37 ` [PATCH storage] fix #6969: plugin: keep content directory when freeing its last file Max R. Carrara
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=20260926093257.7-1-me@dualfroz.com \
--to=me@dualfroz.com \
--cc=pve-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.