use strict;
use warnings;
-use File::Path;
use File::Basename;
-use PVE::Tools;
-use PVE::INotify;
+use File::Path;
+use POSIX qw(ENOENT);
+
use PVE::Cluster;
+use PVE::Exception qw(raise_param_exc);
+use PVE::INotify;
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RESTHandler;
+use PVE::RPCEnvironment;
use PVE::RRD;
-use PVE::Storage;
+use PVE::Tools qw(run_command);
+
use PVE::API2::Storage::Content;
-use PVE::API2::Storage::PruneBackups;
use PVE::API2::Storage::FileRestore;
-use PVE::RESTHandler;
-use PVE::RPCEnvironment;
-use PVE::JSONSchema qw(get_standard_option);
-use PVE::Exception qw(raise_param_exc);
+use PVE::API2::Storage::PruneBackups;
+use PVE::Storage;
use base qw(PVE::RESTHandler);
my $res = [
{ subdir => 'content' },
+ { subdir => 'download-url' },
{ subdir => 'file-restore' },
{ subdir => 'prunebackups' },
{ subdir => 'rrd' },
content => {
description => "Content type.",
type => 'string', format => 'pve-storage-content',
+ enum => ['iso', 'vztmpl'],
},
filename => {
- description => "The name of the file to create.",
+ description => "The name of the file to create. Caution: This will be normalized!",
+ maxLength => 255,
type => 'string',
},
+ checksum => {
+ description => "The expected checksum of the file.",
+ type => 'string',
+ requires => 'checksum-algorithm',
+ optional => 1,
+ },
+ 'checksum-algorithm' => {
+ description => "The algorithm to calculate the checksum of the file.",
+ type => 'string',
+ enum => ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'],
+ requires => 'checksum',
+ optional => 1,
+ },
tmpfilename => {
description => "The source file name. This parameter is usually set by the REST handler. You can only overwrite it when connecting to the trusted port on localhost.",
type => 'string',
my $size = -s $tmpfilename;
die "temporary file '$tmpfilename' does not exist\n" if !defined($size);
- my $filename = $param->{filename};
-
- chomp $filename;
- $filename =~ s/^.*[\/\\]//;
- $filename =~ s/[^-a-zA-Z0-9_.]/_/g;
+ my $filename = PVE::Storage::normalize_content_filename($param->{filename});
my $path;
if ($content eq 'iso') {
if ($filename !~ m![^/]+$PVE::Storage::iso_extension_re$!) {
- raise_param_exc({ filename => "missing '.iso' or '.img' extension" });
+ raise_param_exc({ filename => "wrong file extension" });
}
$path = PVE::Storage::get_iso_dir($cfg, $param->{storage});
} elsif ($content eq 'vztmpl') {
- if ($filename !~ m![^/]+\.tar\.[gx]z$!) {
- raise_param_exc({ filename => "missing '.tar.gz' or '.tar.xz' extension" });
+ if ($filename !~ m![^/]+$PVE::Storage::vztmpl_extension_re$!) {
+ raise_param_exc({ filename => "wrong file extension" });
}
$path = PVE::Storage::get_vztmpl_dir($cfg, $param->{storage});
} else {
# best effort to match apl_download behaviour
chmod 0644, $tmpfilename;
- # we simply overwrite the destination file if it already exists
+ my $err_cleanup = sub { unlink $dest, $tmpfilename; die "cleanup failed: $!" if $! && $! != ENOENT };
my $cmd;
if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
my @remcmd = ('/usr/bin/ssh', @ssh_options, $remip, '--');
- eval {
- # activate remote storage
- PVE::Tools::run_command([@remcmd, '/usr/sbin/pvesm', 'status',
- '--storage', $param->{storage}]);
+ eval { # activate remote storage
+ run_command([@remcmd, '/usr/sbin/pvesm', 'status', '--storage', $param->{storage}]);
};
die "can't activate storage '$param->{storage}' on node '$node': $@\n" if $@;
- PVE::Tools::run_command([@remcmd, '/bin/mkdir', '-p', '--', PVE::Tools::shell_quote($dirname)],
- errmsg => "mkdir failed");
+ run_command(
+ [@remcmd, '/bin/mkdir', '-p', '--', PVE::Tools::shell_quote($dirname)],
+ errmsg => "mkdir failed",
+ );
$cmd = ['/usr/bin/scp', @ssh_options, '-p', '--', $tmpfilename, "[$remip]:" . PVE::Tools::shell_quote($dest)];
+
+ $err_cleanup = sub { run_command([@remcmd, 'rm', '-f', '--', $dest, $tmpfilename]) };
} else {
PVE::Storage::activate_storage($cfg, $param->{storage});
File::Path::make_path($dirname);
$cmd = ['cp', '--', $tmpfilename, $dest];
}
+ # NOTE: we simply overwrite the destination file if it already exists
my $worker = sub {
my $upid = shift;
print "starting file import from: $tmpfilename\n";
+
+ eval {
+ my ($checksum, $checksum_algorithm) = $param->@{'checksum', 'checksum-algorithm'};
+ if ($checksum_algorithm) {
+ print "calculating checksum...";
+
+ my $checksum_got = PVE::Tools::get_file_hash($checksum_algorithm, $tmpfilename);
+
+ if (lc($checksum_got) eq lc($checksum)) {
+ print "OK, checksum verified\n";
+ } else {
+ print "\n"; # the front end expects the error to reside at the last line without any noise
+ die "checksum mismatch: got '$checksum_got' != expect '$checksum'\n";
+ }
+ }
+ };
+ if (my $err = $@) {
+ # unlinks only the temporary file from the http server
+ unlink $tmpfilename;
+ warn "unable to clean up temporory file '$tmpfilename' - $!\n"
+ if $! && $! != ENOENT;
+ die $err;
+ }
+
print "target node: $node\n";
print "target file: $dest\n";
print "file size is: $size\n";
print "command: " . join(' ', @$cmd) . "\n";
- eval { PVE::Tools::run_command($cmd, errmsg => 'import failed'); };
+ eval { run_command($cmd, errmsg => 'import failed'); };
+
+ unlink $tmpfilename; # the temporary file got only uploaded locally, no need to rm remote
+ warn "unable to clean up temporary file '$tmpfilename' - $!\n" if $! && $! != ENOENT;
+
if (my $err = $@) {
- unlink $dest;
+ eval { $err_cleanup->() };
+ warn "$@" if $@;
die $err;
}
print "finished file import successfully\n";
};
- my $upid = $rpcenv->fork_worker('imgcopy', undef, $user, $worker);
+ return $rpcenv->fork_worker('imgcopy', undef, $user, $worker);
+ }});
- # apache removes the temporary file on return, so we need
- # to wait here to make sure the worker process starts and
- # opens the file before it gets removed.
- sleep(1);
+__PACKAGE__->register_method({
+ name => 'download_url',
+ path => '{storage}/download-url',
+ method => 'POST',
+ description => "Download templates and ISO images by using an URL.",
+ proxyto => 'node',
+ permissions => {
+ check => [ 'and',
+ ['perm', '/storage/{storage}', [ 'Datastore.AllocateTemplate' ]],
+ ['perm', '/', [ 'Sys.Audit', 'Sys.Modify' ]],
+ ],
+ },
+ protected => 1,
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ storage => get_standard_option('pve-storage-id'),
+ url => {
+ description => "The URL to download the file from.",
+ type => 'string',
+ pattern => 'https?://.*',
+ },
+ content => {
+ description => "Content type.", # TODO: could be optional & detected in most cases
+ type => 'string', format => 'pve-storage-content',
+ enum => ['iso', 'vztmpl'],
+ },
+ filename => {
+ description => "The name of the file to create. Caution: This will be normalized!",
+ maxLength => 255,
+ type => 'string',
+ },
+ checksum => {
+ description => "The expected checksum of the file.",
+ type => 'string',
+ requires => 'checksum-algorithm',
+ optional => 1,
+ },
+ 'checksum-algorithm' => {
+ description => "The algorithm to calculate the checksum of the file.",
+ type => 'string',
+ enum => ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'],
+ requires => 'checksum',
+ optional => 1,
+ },
+ 'verify-certificates' => {
+ description => "If false, no SSL/TLS certificates will be verified.",
+ type => 'boolean',
+ optional => 1,
+ default => 1,
+ },
+ },
+ },
+ returns => {
+ type => "string"
+ },
+ code => sub {
+ my ($param) = @_;
- return $upid;
- }});
+ my $rpcenv = PVE::RPCEnvironment::get();
+ my $user = $rpcenv->get_user();
+
+ my $cfg = PVE::Storage::config();
+
+ my ($node, $storage) = $param->@{'node', 'storage'};
+ my $scfg = PVE::Storage::storage_check_enabled($cfg, $storage, $node);
+
+ die "can't upload to storage type '$scfg->{type}', not a file based storage!\n"
+ if !defined($scfg->{path});
+
+ my ($content, $url) = $param->@{'content', 'url'};
+
+ die "storage '$storage' is not configured for content-type '$content'\n"
+ if !$scfg->{content}->{$content};
+
+ my $filename = PVE::Storage::normalize_content_filename($param->{filename});
+
+ my $path;
+ if ($content eq 'iso') {
+ if ($filename !~ m![^/]+$PVE::Storage::iso_extension_re$!) {
+ raise_param_exc({ filename => "wrong file extension" });
+ }
+ $path = PVE::Storage::get_iso_dir($cfg, $storage);
+ } elsif ($content eq 'vztmpl') {
+ if ($filename !~ m![^/]+$PVE::Storage::vztmpl_extension_re$!) {
+ raise_param_exc({ filename => "wrong file extension" });
+ }
+ $path = PVE::Storage::get_vztmpl_dir($cfg, $storage);
+ } else {
+ raise_param_exc({ content => "upload content-type '$content' is not allowed" });
+ }
+
+ PVE::Storage::activate_storage($cfg, $storage);
+ File::Path::make_path($path);
+
+ my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
+ my $opts = {
+ hash_required => 0,
+ verify_certificates => $param->{'verify-certificates'} // 1,
+ http_proxy => $dccfg->{http_proxy},
+ };
+
+ my ($checksum, $checksum_algorithm) = $param->@{'checksum', 'checksum-algorithm'};
+ if ($checksum) {
+ $opts->{"${checksum_algorithm}sum"} = $checksum;
+ $opts->{hash_required} = 1;
+ }
+
+ my $worker = sub {
+ PVE::Tools::download_file_from_url("$path/$filename", $url, $opts);
+ };
+
+ my $worker_id = PVE::Tools::encode_text($filename); # must not pass : or the like as w-ID
+
+ return $rpcenv->fork_worker('download', $worker_id, $user, $worker);
+ }});
1;