public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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




             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 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