Invoke-Uplift-ActionResourceDownloadLocal.ps1



# resource file helpers
function Get-LocalResourceContainer($resourceId, $dstDir, $force = $false) {

    Write-DebugMessage "[~] creatng local resource container:"
    Write-DebugMessage " - resource: $resourceId"
    Write-DebugMessage " - dstDir : $dstDir"
    Write-DebugMessage " - force : $force"

    $id          = $resourceId.ToLower()
    $resourceDir = Join-Path -Path $dstDir -ChildPath $id

    $container =  New-Object PsObject -Property @{
        ResourceId   = $id

        ResourceUrl         = $null

        ResourceLatestUrl   = $null
        ResourceLatestMetadataUrl = $null

        ResourceLatestFileUrl = $null
        ResourceLatestChecksumFileUrl = $null

        Force = $force

        LatestDirPath   = (Join-Path -Path $resourceDir -ChildPath "latest")
        LatestFilePath  = $null

        StagingDirPath   = (Join-Path -Path $resourceDir -ChildPath "download-staging")
        StagingFilePath  = $null

        StagingChecksumFilePath = $null
        StagingMetadataFilePath = $null

        Metadata = $null

        CacheDirPath    = (Join-Path -Path $resourceDir -ChildPath "cache")

        LocalRepositoryPath = $dstDir
        ResourceDirPath     = $resourceDir

        ResourceFileName          = $null
        ResourceChecksumFileName  = $null
        ResourceFileExtension     = $null

        IsUnpackable = $false
        IsISO        = $false
        IsZIP        = $false
    }

    Write-DebugMessage "[~] container"
    Write-DebugMessage " - data: $container"

    Write-DebugMessage "[~] ensuring all dir paths"

    New-Folder  $container.StagingDirPath
    New-Folder  $container.LatestDirPath
    New-Folder  $container.CacheDirPath

    New-Folder  $container.ResourceDirPath

    return $container
}

function Get-LocalResourceStampFileName() {
    return "__metadata.uplift.json"
}

function Get-LocalResourceLatestStatus($localContainerPath) {
    $flagFile = Join-Path -Path $localContainerPath -ChildPath (Get-LocalResourceStampFileName)

    return Test-Path $flagFile
}

function Confirm-HttpUrl($value) {
    if( ($value.StartsWith("http://") -eq $False) -and ($value.StartsWith("https://") -eq $False) ) {
        throw "Value must be http/https URL: $value"
    }
}

function Confirm-LatestUrlsAvailability($resourceContainer) {
    Confirm-UrlAvailability $resourceContainer.ResourceLatestFileUrl
    Confirm-UrlAvailability $resourceContainer.ResourceLatestChecksumFileUrl
}

function Join-Uri ($a, $b)
{
    # extremely stupid uri join
    $result = $a.Trim("/").Trim("\") + "/" + $b.Trim("/").Trim("\")

    return $result
}

function Confirm-UrlAvailability($url) {

    Write-DebugMessage "Checking url: $url"

    $result = Invoke-WebRequest "$url" `
        -UseBasicParsing `
        -DisableKeepAlive `
        -Method HEAD

    if($result.StatusCode -eq 200) {
        Write-DebugMessage "[+] StatusCode: $($result.StatusCode) for url: $url"
    } else {
        throw "[!] StatusCode: $($result.StatusCode), expected 200!"
    }
}

function Confirm-HttpUrl($value) {

    $value = $value.ToLower()

    if( ($value.StartsWith("http://") -eq $False) -and ($value.StartsWith("https://") -eq $False) ) {
        throw "Value must be http/https URL: $value"
    }
}

function Write-LocalResourceMetadata($resourceContainer, $metadata) {

    $path = Join-Path -Path $resourceContainer.LatestDirPath -ChildPath (Get-LocalResourceStampFileName)

    $metadata | `
        ConvertTo-Json -Depth 10 | `
        Out-File $path -Force
}

function Invoke-LocalStagingDownload($resourceContainer) {

    # is staging ok already?
    Write-InfoMessage "[~] checking if /download-staging is OK"
    $result =  Confirm-LocalFileValidity $resourceContainer.StagingFilePath

    $preferredTool = Get-CommandOptionValue @("-t", "-tool") $null $null

    if($result -eq $False) {
        Write-InfoMessage "[~] downloading files..."

        Write-InfoMessage "[1/2] downloading: $($resourceContainer.ResourceChecksumFileName)"
        Invoke-DownloadFile `
            $resourceContainer.ResourceLatestChecksumFileUrl `
            $resourceContainer.StagingChecksumFilePath `
            $preferredTool

        Write-InfoMessage "[2/2] downloading: $($resourceContainer.ResourceFileName)"
        Invoke-DownloadFile `
            $resourceContainer.ResourceLatestFileUrl `
            $resourceContainer.StagingFilePath `
            $preferredTool

        Write-InfoMessage "[~] checksum validation: $($resourceContainer.StagingFilePath)"
        $result =  Confirm-LocalFileValidity $resourceContainer.StagingFilePath

        if($result  -eq $False) {
            throw "Incorrect checksum: $(resourceContainer.StagingFilePath)"
        } else {
            Write-InfoMessage "[+] checksum validation ok!"
        }
    } else {
        Write-InfoMessage "[+] /download-staging is OK"
    }
}

function Invoke-UnpackStagingToLatest($localFilePath, $localLatestFolder, $extension) {

    # cleaning up latest folder
    Invoke-CleanFolder $localLatestFolder

    $isISO = ($extension -ieq '.iso') -or ($extension -ieq '.img')
    $isZIP = $extension -ieq '.zip'

    Write-InfoMessage "[~] detected unpackable file, will unpack it to /latest..."

    Write-InfoMessage " - isISO: $isISO"
    Write-InfoMessage " - isZIP: $isZIP"

    if($isISO -eq $True) {
        if($IsWindows -eq $True) {
            # windows case only
            if (-not (test-path "$env:ProgramFiles\7-Zip\7z.exe")) {throw "$env:ProgramFiles\7-Zip\7z.exe needed"}
            set-alias sz "$env:ProgramFiles\7-Zip\7z.exe"

            Write-DebugMessage " - cmd: sz x -y $localFilePath -o$localLatestFolder"

            sz x -y "$localFilePath" "-o$localLatestFolder"
            Confirm-ExitCode $LASTEXITCODE "Failed to unpack: $localFilePath"

        } elseif($IsMacOS -eq $True) {

            Write-DebugMessage " - cmd: 7z x -y $localFilePath -o$localLatestFolder"

            7z x -y "$localFilePath" "-o$localLatestFolder"
            Confirm-ExitCode $LASTEXITCODE "Failed to unpack: $localFilePath"
        }
    } elseif($isZIP -eq $True) {
        if($IsWindows -eq $True) {
            # windows case only
            if (-not (test-path "$env:ProgramFiles\7-Zip\7z.exe")) {throw "$env:ProgramFiles\7-Zip\7z.exe needed"}
            set-alias sz "$env:ProgramFiles\7-Zip\7z.exe"

            Write-DebugMessage " - cmd: sz e $localFilePath -o$localLatestFolder"

            sz e -y "$localFilePath" "-o$localLatestFolder"
            Confirm-ExitCode $LASTEXITCODE "Failed to unpack: $localFilePath"

        } elseif($IsMacOS -eq $True) {

            Write-DebugMessage " - cmd: 7z e $localFilePath -o$localLatestFolder"
            7z e -y "$localFilePath" "-o$localLatestFolder"

            Confirm-ExitCode $LASTEXITCODE "Failed to unpack: $localFilePath"
        }
    } else {
        throw "Unsupported extention to unpack: $extension"
    }

    Write-InfoMessage "[+] completed unpacking!"
}

function Invoke-LocalLatestDownload($resourceContainer, $skipUnpack) {

    Write-InfoMessage "[~] preparing /latest"

    $localFilePath     = $resourceContainer.StagingFilePath

    $localStagingPath  = $resourceContainer.StagingDirPath
    $localLatestFolder = $resourceContainer.LatestDirPath

    Write-DebugMessage "[~] cleaning up /latest path"
    Invoke-CleanFolder $localLatestFolder

    $fileExtension = [System.IO.Path]::GetExtension($localFilePath).ToLower()
    Write-DebugMessage " - ext: $fileExtension"

    if($skipUnpack -eq $True) {
        Write-InfoMessage " - moving non-unpackable file to /latest (skipUnpack = $skipUnpack)"

        Write-DebugMessage " - src: $localStagingPath/*"
        Write-DebugMessage " - dst: $localLatestFolder"

        Move-Item "$localStagingPath/*" $localLatestFolder
    } else {
        Write-InfoMessage " - testing type of file before moving to /latest (skipUnpack = $skipUnpack)"

        switch($fileExtension) {
            '.iso' { Invoke-UnpackStagingToLatest $localFilePath $localLatestFolder $fileExtension }
            '.img' { Invoke-UnpackStagingToLatest $localFilePath $localLatestFolder $fileExtension }
            '.zip' { Invoke-UnpackStagingToLatest $localFilePath $localLatestFolder $fileExtension }

            default {
                Write-InfoMessage " - moving non-unpackable file to /latest"

                Write-DebugMessage " - src: $localStagingPath/*"
                Write-DebugMessage " - dst: $localLatestFolder"

                Move-Item "$localStagingPath/*" $localLatestFolder
            }
        }
    }
}

function Invoke-ActionResourceDownload-Local () {
    [System.ComponentModel.CategoryAttribute("ActionCommand")]
    [System.ComponentModel.DescriptionAttribute("Downloads resource from http server into local folder")]

    param(
        $commandOptions
    )

    $resourceId  = $commandOptions.Third

    Write-DebugMessage "cmd option: $resourceId"

    if( [String]::IsNullOrEmpty($resourceId) -eq $True ) {
        throw "A resource name is required. Try 7z-1805-x64 or 7z-1805-"
    }

    $serverUrl            = Get-CommandOptionValue @("-s", "-server")  $commandOptions "http://localhost:8080"
    $localRepositoryPath  = Get-CommandOptionValue @("-r", "-repository") $commandOptions "uplift-local-resources"
    $isForce              = Get-ForceStatus $commandOptions

    $skipUnpack           = Get-CommandOptionValue @("-skip-unpack", "-no-unpack") $commandOptions

    $resourceContainer = Get-LocalResourceContainer $resourceId $localRepositoryPath $isForce

    Write-InfoMessage "Downloading local resource: $resourceId"

    Confirm-HttpUrl $serverUrl

    $resourceContainer.ResourceUrl         = Join-Uri $serverUrl $resourceId
    $resourceContainer.ResourceLatestUrl   = Join-Uri $resourceContainer.ResourceUrl  "latest"
    $resourceContainer.ResourceLatestMetadataUrl  = Join-Uri $resourceContainer.ResourceUrl  "/latest/__metadata.uplift.json"

    Write-InfoMessage "[+] Remote resource:"
    Write-InfoMessage " - server : $serverUrl"
    Write-InfoMessage " - url : $($resourceContainer.ResourceUrl)"
    Write-InfoMessage " - meta : $($resourceContainer.ResourceLatestMetadataUrl )"

    Write-InfoMessage ""

    Write-InfoMessage "[+] Local resource:"
    Write-InfoMessage " - repo : $($resourceContainer.LocalRepositoryPath)"
    Write-InfoMessage " - resource : $($resourceContainer.ResourceDirPath)"
    Write-InfoMessage " - latestt : $($resourceContainer.LatestDirPath)"

    Write-InfoMessage ""

    Write-InfoMessage "[+] Download options:"
    Write-InfoMessage " - force : $isForce"
    Write-InfoMessage " - skip unpack : $skipUnpack"

    $isLatestOk = Get-LocalResourceLatestStatus $resourceContainer.LatestDirPath

    if($isLatestOk -eq $True) {
        if($isForce -eq $True) {
            Write-WarnMessage "[~] -force flag, latest is OK but will download again"
        } else {
            Write-InfoMessage "[+] latest is OK, won't download"
            return 0
        }
    } else {
        Write-WarnMessage "[~] latest is NOT OK, will download it"
    }

    $resourceMetadataUrl = $resourceContainer.ResourceLatestMetadataUrl
    $resourceLatestUrl   = $resourceContainer.ResourceLatestUrl

    # pinging metadata files, fetching file name
    Confirm-UrlAvailability $resourceMetadataUrl

    Write-InfoMessage "[~] fetching metadata: $resourceMetadataUrl"
    $metadataResult = Invoke-WebRequest $resourceMetadataUrl `
                        -UseBasicParsing

    Write-DebugMessage "[~] result: $metadataResult"

    $metadata =  $metadataResult | ConvertFrom-Json
    $resourceContainer.Metadata = $metadata

    $resourceFileName         = $metadata.file_name
    $resourceChecksumFileName =  ($resourceFileName + ".sha256")

    $resourceContainer.ResourceFileName         = $resourceFileName
    $resourceContainer.ResourceChecksumFileName = $resourceChecksumFileName

    $resourceContainer.ResourceLatestFileUrl         = Join-Uri $resourceLatestUrl $resourceFileName
    $resourceContainer.ResourceLatestChecksumFileUrl = Join-Uri $resourceLatestUrl $resourceChecksumFileName

    $resourceContainer.ResourceLatestFileUrl         = Join-Uri $resourceLatestUrl $resourceFileName

    $resourceContainer.StagingFilePath         = Join-Path -Path $resourceContainer.StagingDirPath -ChildPath $resourceFileName
    $resourceContainer.StagingChecksumFilePath = Join-Path -Path $resourceContainer.StagingDirPath -ChildPath $resourceChecksumFileName

    # alive?
    Confirm-LatestUrlsAvailability $resourceContainer

    # download to staging
    # check if anything in cache?
    # check and create checksums
    Invoke-LocalStagingDownload $resourceContainer

    # move staging to latest
    # fill cache, store needed files in cache
    Invoke-LocalLatestDownload $resourceContainer $skipUnpack

    Write-InfoMessage "[~] cleaning up /download-staging"
    Invoke-CleanFolder $resourceContainer.StagingDirPath

    # write local stamp
    # same-same as /"__metadata.uplift.json under latest URL
    Write-LocalResourceMetadata $resourceContainer $metadata

    Write-InfoMessage "[+] local resource path: $($resourceContainer.LatestDirPath)"
    Write-InfoMessage "[+] completed transfer for local resource: $resourceId"

    return 0
}