Each content type has a predefined location in a storage where they can be found. iso and vztmpl files can now be found and and also uploaded in subdirectories of those locations, too.
Add a parameter "recursive" (default off) to the API at storage/{storage}/content, to perform this search. Signed-off-by: Dominic Jäger <d.jae...@proxmox.com> --- Changes since RFC: * Separate tests for existing and new stuff into 2 patches * Die when .. appears in a path * Make feature opt-in (relevant for symlinks) PVE/API2/Storage/Content.pm | 9 +++- PVE/API2/Storage/Status.pm | 12 +++--- PVE/Storage.pm | 4 +- PVE/Storage/Plugin.pm | 85 +++++++++++++++++++++++++++++++++---- test/run_plugin_tests.pl | 57 ++++++++++++++++++++++++- 5 files changed, 149 insertions(+), 18 deletions(-) diff --git a/PVE/API2/Storage/Content.pm b/PVE/API2/Storage/Content.pm index 63fa4fc..cd302e9 100644 --- a/PVE/API2/Storage/Content.pm +++ b/PVE/API2/Storage/Content.pm @@ -44,6 +44,12 @@ __PACKAGE__->register_method ({ optional => 1, completion => \&PVE::Cluster::complete_vmid, }), + recursive => { + description => "Search recursively.", + type => 'boolean', + optional => 1, + default => 0, + }, }, }, returns => { @@ -102,7 +108,8 @@ __PACKAGE__->register_method ({ my $cfg = PVE::Storage::config(); - my $vollist = PVE::Storage::volume_list($cfg, $storeid, $param->{vmid}, $param->{content}); + my $vollist = PVE::Storage::volume_list($cfg, $storeid, $param->{vmid}, + $param->{content}, {recursive => $param->{recursive}}); my $res = []; foreach my $item (@$vollist) { diff --git a/PVE/API2/Storage/Status.pm b/PVE/API2/Storage/Status.pm index 14f5930..b8caf9c 100644 --- a/PVE/API2/Storage/Status.pm +++ b/PVE/API2/Storage/Status.pm @@ -403,25 +403,25 @@ __PACKAGE__->register_method ({ my $filename = $param->{filename}; chomp $filename; - $filename =~ s/^.*[\/\\]//; - $filename =~ s/[^-a-zA-Z0-9_.]/_/g; + # Prevent entering other locations without permission + die "Filename must not contain two dots '..'" if $filename =~ m/\.\./; + $filename =~ s/^[\/|\\]*//; # files are uploaded relative to storage path + $filename =~ s/[^-a-zA-Z0-9_.\/\\]/_/g; my $path; - if ($content eq 'iso') { - if ($filename !~ m![^/]+$PVE::Storage::iso_extension_re$!) { + if ($filename !~ m!$PVE::Storage::iso_extension_re$!) { raise_param_exc({ filename => "missing '.iso' or '.img' extension" }); } $path = PVE::Storage::get_iso_dir($cfg, $param->{storage}); } elsif ($content eq 'vztmpl') { - if ($filename !~ m![^/]+\.tar\.[gx]z$!) { + if ($filename !~ m!\.tar\.[gx]z$!) { raise_param_exc({ filename => "missing '.tar.gz' or '.tar.xz' extension" }); } $path = PVE::Storage::get_vztmpl_dir($cfg, $param->{storage}); } else { raise_param_exc({ content => "upload content type '$content' not allowed" }); } - die "storage '$param->{storage}' does not support '$content' content\n" if !$scfg->{content}->{$content}; diff --git a/PVE/Storage.pm b/PVE/Storage.pm index 60b8310..9ec40a5 100755 --- a/PVE/Storage.pm +++ b/PVE/Storage.pm @@ -866,7 +866,7 @@ sub template_list { } sub volume_list { - my ($cfg, $storeid, $vmid, $content) = @_; + my ($cfg, $storeid, $vmid, $content, $param) = @_; my @ctypes = qw(rootdir images vztmpl iso backup snippets); @@ -880,7 +880,7 @@ sub volume_list { activate_storage($cfg, $storeid); - my $res = $plugin->list_volumes($storeid, $scfg, $vmid, $cts); + my $res = $plugin->list_volumes($storeid, $scfg, $vmid, $cts, $param); @$res = sort {lc($a->{volid}) cmp lc ($b->{volid}) } @$res; diff --git a/PVE/Storage/Plugin.pm b/PVE/Storage/Plugin.pm index 8c0dae1..e81c89f 100644 --- a/PVE/Storage/Plugin.pm +++ b/PVE/Storage/Plugin.pm @@ -415,6 +415,9 @@ sub parse_name_dir { die "unable to parse volume filename '$name'\n"; } +# for iso and vztmpl returns +# [0] format +# [1] notdir (=basename+suffix; no directories) sub parse_volname { my ($class, $volname) = @_; @@ -428,9 +431,11 @@ sub parse_volname { my ($vmid, $name) = ($1, $2); my (undef, $format, $isBase) = parse_name_dir($name); return ('images', $name, $vmid, undef, undef, $isBase, $format); - } elsif ($volname =~ m!^iso/([^/]+$PVE::Storage::iso_extension_re)$!) { + } elsif ($volname =~ m!^iso/(?:.+/)*([^/]+$PVE::Storage::iso_extension_re)$!) { + die "volname must not contain two dots '..'" if $volname =~ m!\.\.!; return ('iso', $1); - } elsif ($volname =~ m!^vztmpl/([^/]+\.tar\.[gx]z)$!) { + } elsif ($volname =~ m!^vztmpl/(?:.+/)*([^/]+\.tar\.[gx]z)$!) { + die "volname must not contain two dots '..'" if $volname =~ m!\.\.!; return ('vztmpl', $1); } elsif ($volname =~ m!^rootdir/(\d+)$!) { return ('rootdir', $1, $1); @@ -485,7 +490,14 @@ sub filesystem_path { $dir .= "/$vmid" if $vtype eq 'images'; - my $path = "$dir/$name"; + my $path; + if ($vtype eq 'iso' || $vtype eq 'vztmpl') { + # remove vtype => file name relative to storage path + $volname =~ m!^$vtype/(.+)!; + $path = "$dir/$1"; + } else { + $path = "$dir/$name"; + } return wantarray ? ($path, $vmid, $vtype) : $path; } @@ -910,6 +922,61 @@ sub list_images { return $res; } + +# returns if file name fits $type +# [0] volname +# [1] format +# undef otherwise +sub get_file_properties { + my ($fn, $storage_path, $type) = @_; + die "fn must not contain two dots '..'" if $fn =~ m/\.\./; + if ($type eq 'iso') { + if ($fn =~ m!$storage_path/(.+$PVE::Storage::iso_extension_re)$!i) { + return ["$type/$1", $type]; + } else { + return undef; + } + } elsif ($type eq 'vztmpl') { + if ($fn =~ m!$storage_path/(.+\.tar\.([gx]z))$!) { + return ["$type/$1", "t$2"]; + } else { + return undef; + } + } else { + die "Invalid parameter type=$type"; + } +} + +# $type = <iso|vztmpl> +sub get_subdir_files_rec { + my ($sid, $path, $type, $storage_path) = @_; + die "Invalid parameter" if not ($type eq 'iso' || $type eq 'vztmpl'); + die "Path must not contain ../" if $path =~ m!.*\.\./.*!; + + # Each call needs to know the original storage path + if (!defined $storage_path) { + $storage_path = $path; + } + + my @result = (); + + # Symlinks have to be set up via shell. At least as long recursive scanning + # is opt-in handling loops is out of scope. Instead, just follow the links. + if (-d $path) { + foreach my $fn (<$path/*>) { + push @result, get_subdir_files_rec($sid, $fn, $type, $storage_path); + } + } else { + my $properties = get_file_properties($path, $storage_path, $type); + if ($properties) { + my $file = { volid => "$sid:$$properties[0]", format => $$properties[1] }; + $file->{size} = -s $path // 0; + push @result, $file; + } + } + return @result; +} + # list templates ($tt = <iso|vztmpl|backup|snippets>) my $get_subdir_files = sub { my ($sid, $path, $tt, $vmid) = @_; @@ -982,7 +1049,7 @@ my $get_subdir_files = sub { }; sub list_volumes { - my ($class, $storeid, $scfg, $vmid, $content_types) = @_; + my ($class, $storeid, $scfg, $vmid, $content_types, $param) = @_; my $res = []; my $vmlist = PVE::Cluster::get_vmlist(); @@ -994,10 +1061,12 @@ sub list_volumes { } elsif ($scfg->{path}) { my $path = $class->get_subdir($scfg, $type); - if ($type eq 'iso' && !defined($vmid)) { - $data = $get_subdir_files->($storeid, $path, 'iso'); - } elsif ($type eq 'vztmpl'&& !defined($vmid)) { - $data = $get_subdir_files->($storeid, $path, 'vztmpl'); + if (($type eq 'iso' || $type eq 'vztmpl') && !defined($vmid)) { + if ($param->{recursive}) { + $data = [get_subdir_files_rec($storeid, $path, $type)]; + } else { + $data = $get_subdir_files->($storeid, $path, $type); + } } elsif ($type eq 'backup') { $data = $get_subdir_files->($storeid, $path, 'backup', $vmid); } elsif ($type eq 'snippets') { diff --git a/test/run_plugin_tests.pl b/test/run_plugin_tests.pl index cd93430..7f627b1 100755 --- a/test/run_plugin_tests.pl +++ b/test/run_plugin_tests.pl @@ -3,7 +3,7 @@ use strict; use warnings; use lib ('.', '..'); -use Test::More tests => 32; +use Test::More tests => 42; use Test::MockModule qw(new); use File::Temp qw(tempdir); use File::Path qw(make_path); @@ -13,16 +13,23 @@ use PVE::Storage; my $plugin = 'PVE::Storage::Plugin'; my $basename = 'test'; +my $subdir_regular = 'some/more/subdirectories/'; my $iso_type = 'iso'; my $iso_suffix = '.iso'; my $iso_notdir = "$basename$iso_suffix"; my $iso_volname = "$iso_type/$iso_notdir"; +my $iso_volname_with_subdirs = "$iso_type/$subdir_regular$iso_notdir"; +# Subdirectories should be treated like normal folders, even if their name +# is equal to our predefined storage schema +my $subdirs_named_iso = 'a/template/iso/b/template/iso/c/template/iso/'; my $vztmpl_type = 'vztmpl'; my $vztmpl_suffix = '.tar.gz'; my $vztmpl_notdir = "$basename$vztmpl_suffix"; my $vztmpl_volname = "$vztmpl_type/$vztmpl_notdir"; +my $vztmpl_volname_with_subdirs = "$vztmpl_type/$subdir_regular$vztmpl_notdir"; +my $subdir_named_cache = 'a/template/cache/b/template/cache/c/template/cache/'; my $iso_with_dots = "$iso_type/../$iso_notdir"; my $vztmpl_with_dots = "$vztmpl_type/../$vztmpl_notdir"; @@ -48,11 +55,25 @@ is (($plugin->parse_volname($iso_volname))[$type_index], $iso_type, 'parse_volname: type for iso'); is (($plugin->parse_volname($iso_volname))[$notdir_index], $iso_notdir, 'parse_volname: notdir for iso'); +is (($plugin->parse_volname($iso_volname_with_subdirs))[$type_index], + $iso_type, 'parse_volname: type for iso in subdir'); +is (($plugin->parse_volname($iso_volname_with_subdirs))[$notdir_index], + $iso_notdir, 'parse_volname: notdir for iso in subdir'); +eval { $plugin->parse_volname($iso_with_dots) }; +like ($@, qr/volname must not contain two dots/, + 'parse_volname: forbid two dots .. for iso'); is (($plugin->parse_volname($vztmpl_volname))[$type_index], $vztmpl_type, 'parse_volname: type for vztmpl'); is (($plugin->parse_volname($vztmpl_volname))[$notdir_index], $vztmpl_notdir, 'parse_volname: notdir for vztmpl'); +is (($plugin->parse_volname($vztmpl_volname_with_subdirs))[$type_index], + $vztmpl_type, 'parse_volname: type for vztmpl in subdir'); +is (($plugin->parse_volname($vztmpl_volname_with_subdirs))[$notdir_index], + $vztmpl_notdir, 'parse_volname: notdir for vztmpl in subdir'); +eval { $plugin->parse_volname($vztmpl_with_dots) }; +like ($@, qr/volname must not contain two dots/, + 'parse_volname: forbid two dots .. for vztmpl'); is (($plugin->parse_volname($raw_image_volname))[$type_index], $image_type, 'parse_volname: type for raw image'); @@ -91,9 +112,15 @@ is ($plugin->get_subdir($scfg_with_path, 'rootdir'), is ($plugin->filesystem_path($scfg_with_path, $iso_volname), "$scfg_with_path->{path}/template/$iso_volname", 'filesystem_path for iso'); +is ($plugin->filesystem_path($scfg_with_path, $iso_volname_with_subdirs), + "$scfg_with_path->{path}/template/$iso_volname_with_subdirs", + 'filesystem_path for iso in subdirs'); is ($plugin->filesystem_path($scfg_with_path, $vztmpl_volname), "$scfg_with_path->{path}/template/cache/$vztmpl_notdir", 'filesystem_path for vztmpl'); +is ($plugin->filesystem_path($scfg_with_path, $vztmpl_volname_with_subdirs), + "$scfg_with_path->{path}/template/cache/$subdir_regular$vztmpl_notdir", + 'filesystem_path for vztmpl in subdirs'); is ($plugin->filesystem_path($scfg_with_path, $raw_image_volname), "$scfg_with_path->{path}/images/$raw_image_volname", 'filesystem_path for raw images'); @@ -142,12 +169,40 @@ my $paths = [ ]; add_test_files($paths); +my $storage_dir_rec = File::Temp->newdir(); +my $recursive_paths = [ + "$storage_dir_rec/template/cache", + "$storage_dir_rec/template/iso", + "$storage_dir_rec/images/$vmid", + "$storage_dir_rec/template/cache/$subdir_regular", + "$storage_dir_rec/template/cache/$subdirs_named_iso", + "$storage_dir_rec/template/cache/$subdir_named_cache", + "$storage_dir_rec/template/iso/$subdir_regular", + "$storage_dir_rec/template/iso/$subdirs_named_iso", + "$storage_dir_rec/template/iso/$subdir_named_cache", +]; +add_test_files($recursive_paths); + note 'Tests without recursion'; # note "Temp dir is:\n", `tree $storage_dir_no_rec`; my $scfg_no_rec = { path => $storage_dir_no_rec }; test_list_volumes($scfg_no_rec, [$iso_type], [$iso_volname]); test_list_volumes($scfg_no_rec, [$vztmpl_type], [$vztmpl_volname]); +note 'Tests with recursion'; +# note "Temp dir is:\n", `tree $storage_dir_rec`; +my $scfg_rec = { path => $storage_dir_rec }; +my $expected_volnames_iso = [$iso_volname, $iso_volname_with_subdirs, + "$iso_type/$subdir_named_cache$iso_notdir", + "$iso_type/$subdirs_named_iso$iso_notdir"]; +my $expected_volnames_vztmpl = [$vztmpl_volname, $vztmpl_volname_with_subdirs, + "$vztmpl_type/$subdir_named_cache$vztmpl_notdir", + "$vztmpl_type/$subdirs_named_iso$vztmpl_notdir"]; +test_list_volumes($scfg_rec, [$iso_type], $expected_volnames_iso, + { recursive => 1 }); +test_list_volumes($scfg_rec, [$vztmpl_type],$expected_volnames_vztmpl, + { recursive => 1 }); + my $scfg_with_type = { path => $storage_dir_no_rec, type => 'dir' }; my $plugin_mock = Test::MockModule->new('PVE::Cluster'); $plugin_mock->redefine('get_vmlist' => sub { return undef }); -- 2.20.1 _______________________________________________ pve-devel mailing list pve-devel@pve.proxmox.com https://pve.proxmox.com/cgi-bin/mailman/listinfo/pve-devel