##############################################################################
##
#W download.gi GAP4 package `Utils' Thomas Breuer
##
#Y Copyright (C) 2022, The GAP Group
#############################################################################
##
#V Download_Methods
##
## Use the following tools (in this order).
##
## - If the curlInterface package is available then call its
## 'DownloadURL' function.
## - If the URL starts with 'http://' and if the IO package is available
## then call the 'SingleHTTPRequest' function from this package.
## - If a 'wget' executable is available then call it.
## - If a 'curl' executable is available then call it.
##
## Note that currently the methods are *NOT* consistent in the case of
## failures:
##
## - The function 'SingleHTTPRequest' does not follow redirects as indicated
## by HTTP status codes 301 and 302.
## This happens for example if one asks for the file at
## 'http://www.gap-system.org/Packages/utils.html'.
##
BindGlobal(
"Download_Methods", [] );
Add( Download_Methods, rec(
name:=
"via DownloadURL (from the curlInterface package)",
isAvailable:= {} -> IsPackageLoaded(
"curlInterface" ) and
CompareVersionNumbers(
InstalledPackageVersion(
"curlInterface" ),
"2.3.0" ),
download:=
function( url, opt )
local res;
opt:= ShallowCopy( opt );
if not IsBound( opt.failOnError )
then
opt.failOnError:= true;
fi;
# 'DownloadURL' handles the options 'verifyCert' and 'maxTime'.
res:= ValueGlobal(
"DownloadURL" )( url, opt );
if res.success = true and
IsBound( opt.target ) and IsString( opt.target )
then
FileString( opt.target, res.result );
Unbind( res.result );
fi;
return res;
end ) );
Add( Download_Methods, rec(
name:=
"via SingleHTTPRequest (from the IO package)",
isAvailable:= {} -> IsBoundGlobal(
"SingleHTTPRequest" ),
download:=
function( url, opt )
local rurl, pos, domain, uri, res;
if not StartsWith( url,
"http://" )
then
return rec( success:= false, error:=
"protocol is not http" );
elif IsBound( opt.maxTime ) and opt.maxTime <>
0 then
return rec( success:= false, error:=
"no support for given timeout" );
fi;
rurl:= ReplacedString( url,
"http://",
"" );
pos:= Position( rurl,
'/' );
domain:= rurl{ [
1 .. pos-
1 ] };
uri:= rurl{ [ pos .. Length( rurl ) ] };
if IsBound( opt.target ) and IsString( opt.target )
then
res:= ValueGlobal(
"SingleHTTPRequest" )( domain,
80,
"GET", uri,
rec(), false, opt.target );
else
res:= ValueGlobal(
"SingleHTTPRequest" )( domain,
80,
"GET", uri,
rec(), false, false );
fi;
if res.statuscode =
0 then
return rec( success:= false,
error:= res.status );
elif res.statuscode >=
400 then
return rec( success:= false,
error:= Concatenation(
"HTTP error code ",
String( res.statuscode ) ) );
elif not ( IsBound( opt.target ) and IsString( opt.target ) )
then
return rec( success:= true, result:= res.body );
else
return rec( success:= true );
fi;
end ) );
Add( Download_Methods, rec(
name:=
"via wget",
isAvailable:=
function()
local exec;
exec:= Filename( DirectoriesSystemPrograms(),
"wget" );
return exec <> fail and IsExecutableFile( exec );
end,
download:=
function( url, opt )
local res, outstream, exec, args, code;
if IsBound( opt.maxTime ) and opt.maxTime <>
0 then
# wget 1.20.3 ignores a given timeout.
# (wget 1.21.3 would support timeout.)
return rec( success:= false, error:=
"no support for given timeout" );
fi;
res:=
"";
outstream:= OutputTextString( res, true );
exec:= Filename( DirectoriesSystemPrograms(),
"wget" );
if IsBound( opt.target ) and IsString( opt.target )
then
args:= [
"--quiet",
"-O", opt.target, url ];
else
args:= [
"--quiet",
"-O",
"-", url ];
fi;
if IsBound( opt.verifyCert ) and opt.verifyCert = false
then
Add( args,
"--no-check-certificate" );
fi;
if IsBound( opt.maxTime ) and IsPosInt( opt.maxTime )
then
Add( args, Concatenation(
"--timeout=", String( opt.maxTime ) ) );
fi;
code:= Process( DirectoryCurrent(), exec, InputTextNone(), outstream, args );
CloseStream( outstream );
if code <>
0 then
# wget may have created the target file; try to remove it
if IsBound( opt.target ) and IsString( opt.target ) and
IsExistingFile( opt.target ) and RemoveFile( opt.target ) <> true
then
Error(
"Download cannot remove unwanted file ", opt.target );
fi;
return rec( success:= false,
error:= Concatenation(
"Process returned ", String( code ) ) );
elif not ( IsBound( opt.target ) and IsString( opt.target ) )
then
return rec( success:= true, result:= res );
else
return rec( success:= true );
fi;
end ) );
Add( Download_Methods, rec(
name:=
"via curl",
isAvailable:=
function()
local exec;
exec:= Filename( DirectoriesSystemPrograms(),
"curl" );
return exec <> fail and IsExecutableFile( exec );
end,
download:=
function( url, opt )
local res, outstream, exec, args, code;
res:=
"";
outstream:= OutputTextString( res, true );
exec:= Filename( DirectoriesSystemPrograms(),
"curl" );
args:= [
"--silent",
"-L",
"--fail" ];
if IsBound( opt.verifyCert ) and opt.verifyCert = false
then
Add( args,
"-k" );
fi;
Add( args,
"--output" );
if IsBound( opt.target ) and IsString( opt.target )
then
Add( args, opt.target );
else
Add( args,
"-" );
fi;
if IsBound( opt.maxTime ) and IsPosInt( opt.maxTime )
then
Add( args,
"--max-time" );
Add( args, opt.maxTime );
fi;
Add( args, url );
code:= Process( DirectoryCurrent(), exec, InputTextNone(), outstream, args );
CloseStream( outstream );
if code <>
0 then
return rec( success:= false,
error:= Concatenation(
"Process returned ", String( code ) ) );
elif not ( IsBound( opt.target ) and IsString( opt.target ) )
then
return rec( success:= true, result:= res );
else
return rec( success:= true );
fi;
end ) );
#############################################################################
##
#M Download( <url>[, <opt>] )
##
## Try to download the file described by the string <url>,
## and return a record with the components 'success' ('true' or 'false'),
## and 'result' (a string, only if 'success' is 'true'),
## and 'error' (a string, only if 'success' is 'false').
##
InstallMethod( Download,
[
"IsString" ],
url -> Download( url, rec() ) );
InstallMethod( Download,
[
"IsString",
"IsRecord" ],
function( url, opt )
local timeout, errors, r, res;
# Set the default for 'verifyCert' if necessary.
if not IsBound( opt.verifyCert ) and
UserPreference(
"utils",
"DownloadVerifyCertificate" ) = false
then
opt.verifyCert:= false;
fi;
# Set the default for 'maxTime' if necessary.
if not IsBound( opt.maxTime )
then
timeout:= UserPreference(
"utils",
"DownloadMaxTime" );
if IsPosInt( timeout )
then
opt.maxTime:= timeout;
fi;
fi;
# Run over the methods.
errors:= [];
for r in Download_Methods
do
if r.isAvailable()
then
Info( InfoUtils,
2,
"try Download method ", r.name );
res:= r.download( url, opt );
if res.success = true
then
return res;
fi;
Info( InfoUtils,
2,
"Download method ", r.name,
" failed with\n",
"#I ", res.error );
Add( errors, Concatenation( r.name,
": ", res.error ) );
else
Info( InfoUtils,
2,
"Download method ", r.name,
" is not available" );
fi;
od;
# No method was successful.
if Length( errors ) =
0 then
# No method was available, inform the user
# that it is recommended to load or install some download tool.
Info( InfoWarning,
1,
"No 'Download' method is available.\n",
"#I Please consider to install one of the tools, ",
"see '?Download'" );
return rec( success:= false, error:=
"no method was available" );
else
# At least one method was tried but without success.
if IsBound( opt.maxTime )
then
Add( errors,
Concatenation(
"(maxTime option was set to ",
String( opt.maxTime ),
")" ) );
fi;
return rec( success:= false,
error:= JoinStringsWithSeparator( errors,
"; " ) );
fi;
end );