module fpm_downloader use fpm_error, only: error_t, fatal_error use fpm_filesystem, only: which, run use fpm_versioning, only: version_t use jonquil, only: json_object, json_value, json_error, json_load, cast_to_object use fpm_strings, only: string_t implicit none private public :: downloader_t !> This type could be entirely avoided but it is quite practical because it can be mocked for testing. type downloader_t contains procedure, nopass :: get_pkg_data, get_file, upload_form, unpack end type contains !> Perform an http get request, save output to file, and parse json. subroutine get_pkg_data(url, version, tmp_pkg_file, json, error) character(*), intent(in) :: url type(version_t), allocatable, intent(in) :: version character(*), intent(in) :: tmp_pkg_file type(json_object), intent(out) :: json type(error_t), allocatable, intent(out) :: error class(json_value), allocatable :: j_value type(json_object), pointer :: ptr type(json_error), allocatable :: j_error if (allocated(version)) then ! Request specific version. call get_file(url//'/'//version%s(), tmp_pkg_file, error) else ! Request latest version. call get_file(url, tmp_pkg_file, error) end if if (allocated(error)) return call json_load(j_value, tmp_pkg_file, error=j_error) if (allocated(j_error)) then allocate (error); call move_alloc(j_error%message, error%message); call json%destroy(); return end if ptr => cast_to_object(j_value) if (.not. associated(ptr)) then call fatal_error(error, "Error parsing JSON from '"//url//"'."); return end if json = ptr end !> Download a file from a url using either curl or wget. subroutine get_file(url, tmp_pkg_file, error) character(*), intent(in) :: url character(*), intent(in) :: tmp_pkg_file type(error_t), allocatable, intent(out) :: error integer :: stat if (which('curl') /= '') then print *, "Downloading '"//url//"' -> '"//tmp_pkg_file//"'" call execute_command_line('curl '//url//' -s -o '//tmp_pkg_file, exitstat=stat) else if (which('wget') /= '') then print *, "Downloading '"//url//"' -> '"//tmp_pkg_file//"'" call execute_command_line('wget '//url//' -q -O '//tmp_pkg_file, exitstat=stat) else call fatal_error(error, "Neither 'curl' nor 'wget' installed."); return end if if (stat /= 0) then call fatal_error(error, "Error downloading package from '"//url//"'."); return end if end !> Perform an http post request with form data. subroutine upload_form(endpoint, form_data, verbose, error) !> Endpoint to upload to. character(len=*), intent(in) :: endpoint !> Form data to upload. type(string_t), intent(in) :: form_data(:) !> Print additional information if true. logical, intent(in) :: verbose !> Error handling. type(error_t), allocatable, intent(out) :: error integer :: stat, i character(len=:), allocatable :: form_data_str form_data_str = '' do i = 1, size(form_data) form_data_str = form_data_str//"-F '"//form_data(i)%s//"' " end do if (which('curl') /= '') then print *, 'Uploading package ...' call run('curl -X POST -H "Content-Type: multipart/form-data" '// & & form_data_str//endpoint, exitstat=stat, echo=verbose) else call fatal_error(error, "'curl' not installed."); return end if if (stat /= 0) then call fatal_error(error, "Error uploading package to registry."); return end if end !> Unpack a tarball to a destination. subroutine unpack(tmp_pkg_file, destination, error) !> Path to tarball. character(*), intent(in) :: tmp_pkg_file !> Destination to unpack to. character(*), intent(in) :: destination !> Error handling. type(error_t), allocatable, intent(out) :: error integer :: stat if (which('tar') == '') then call fatal_error(error, "'tar' not installed."); return end if print *, "Unpacking '"//tmp_pkg_file//"' to '"//destination//"' ..." call execute_command_line('tar -zxf '//tmp_pkg_file//' -C '//destination, exitstat=stat) if (stat /= 0) then call fatal_error(error, "Error unpacking '"//tmp_pkg_file//"'."); return end if end end