Invoke-Uplift-ActionResourceDownload.ps1



# helpers

. "$PSScriptRoot/Invoke-Uplift-ActionResourceDownloadVS17.ps1"

function Get-LatestStatus($resourceContainer, $customFileName = $null) {

    Write-DebugMessage "[~] checking /latest status"
    Write-DebugMessage " - customFileName: $customFileName"

    $filePath     = $resourceContainer.LatestFilePath
    $checksumPath = $resourceContainer.LatestChecksumFilePath
    $metadataPath = $resourceContainer.LatestMetadataFilePath

    if($null -ne $customFileName) {
        $filePath     = Join-Path -Path $resourceContainer.LatestDirPath -ChildPath $customFileName
        $checksumPath = "$filePath.sha256"
    }

    $fileOk     = Test-Path  $filePath
    $checksumOk = Test-Path  $checksumPath
    $metadataOk = Test-Path  $metadataPath

    Write-DebugMessage "- fileOk : $fileOk $($filePath)"
    Write-DebugMessage "- checksumOk: $checksumOk $($checksumPath)"
    Write-DebugMessage "- metadataOk: $metadataOk $($metadataPath)"

    return ( $fileOk -and $checksumOk -and $metadataOk)
}

function Write-LatestMetadata($resourceContainer, $customFileName) {

    Write-DebugMessage "[+] saving metadata file"

    $fileName =  $resourceContainer.ResourceFileName
    if($null -ne $customFileName) {
        $fileName = $customFileName
    }

    $metadata     = New-Object PSObject -Property @{}
    $metadataPath =  $resourceContainer.LatestMetadataFilePath

    $metadata | Add-Member -Name 'file_name' `
        -Type NoteProperty -Force -Value  $fileName

    $metadata | Add-Member -Name 'metadata' `
        -Type NoteProperty -Force -Value  $resourceContainer.ResourceMetadata

    # checksum file always follows name convention:
    # "file-name.extention" + ".sha256"
    # we should not even expose it here

    # $metadata | Add-Member -Name 'checksum_file_name' `
    # -Type NoteProperty -Force -Value ($resourceContainer.ResourceFileName + ".sha256")

    $metadataJSON = $metadata | ConvertTo-Json -Depth 10

    Write-DebugMessage "metadata path: $metadataPath"
    Write-DebugMessage "metadata json: $metadataJSON"

    $metadataJSON | Out-File $metadataPath -Force
}

function New-ChecksumFile {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSShouldProcess", "", Scope="Function")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Scope="Function")]
    param(
        $originalFilePath,
        $checksumType = "SHA256"
    )

    $originalFileName     = (Split-Path $originalFilePath -Leaf)

    $originalFileChecksum = Get-FileHash -Algorithm $checksumType $originalFilePath
    $checksumFilePath     = ("$originalFilePath.$checksumType").ToLower()

    $checksumFileValue    = $originalFileChecksum.Hash.ToUpper() + " " + $originalFileName

    Write-DebugMessage "[+] saving checksum file"
    Write-DebugMessage " - path : $checksumFilePath"
    Write-DebugMessage " - value: $originalFileChecksum"
    Write-DebugMessage " - checksumFileValue: $checksumFileValue"

    $checksumFileValue | Out-File $checksumFilePath -Force | Out-Null
}

function Get-ResourceContainer($resource, $dstDir, $force = $false) {

    Write-DebugMessage "[~] creating resource container:"
    Write-DebugMessage " - resource: $resource"
    Write-DebugMessage " - dstDir : $dstDir"
    Write-DebugMessage " - force : $force"

    # always to lower
    # that simplifies further static web site hosting
    # and potential transfers to Amazon/Azure and other clouds
    $id  = $resource.id.ToLower()

    $resourceDir       = Join-Path -Path $dstDir -ChildPath $id
    $resourceFileName = (Get-ResourceFileName $resource)

    $container =  New-Object PsObject -Property @{
        Resource   = $resource
        ResourceMetadata  = $resource.metadata

        ResourceId   = $id
        ResourceType = Get-ResourceType $resource

        Force = $force

        LatestDirPath   = (Join-Path -Path $resourceDir -ChildPath "latest")
        LatestFilePath  = (Join-Path -Path $resourceDir -ChildPath "latest" -AdditionalChildPath $resourceFileName)
        LatestChecksumFilePath  = (Join-Path -Path $resourceDir -ChildPath "latest" -AdditionalChildPath ($resourceFileName + ".sha256")  )

        LatestMetadataFilePath  = (Join-Path -Path $resourceDir -ChildPath "latest" -AdditionalChildPath "__metadata.uplift.json")

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

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

        ResourceDirPath = $resourceDir
        DestinationPath = $dstDir

        ResourceFileName = (Get-ResourceFileName $resource)
    }

    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
    New-Folder  $container.DestinationPath

    return $container
}

function Invoke-StagingDownload($resourceContainer) {

    Invoke-ResourceFolderDownload $resourceContainer  `
        $resourceContainer.Resource.uri `
        $resourceContainer.StagingFilePath
}

function Invoke-CacheDownload($resourceContainer) {

    Invoke-ResourceFolderDownload $resourceContainer  `
        $resourceContainer.Resource.uri `
        $resourceContainer.CacheFilePath
}

function Invoke-ResourceFolderDownload($resourceContainer, $src, $dst) {
    Write-InfoMessage "[~] checking if file was already downloaded"

    if( (Confirm-FileValidity $resourceContainer $dst) -eq $True) {

        if($resourceContainer.Force -eq $True) {
            Write-InfoMessage "[~] -force, existing file is ok but will download again"
        } else {
            Write-InfoMessage "[~] using existing file: checksum is ok"
            New-ChecksumFile $dst

            return
        }
    }

    Write-InfoMessage  "[~] downloading file"
    Write-DebugMessage " -dst: $dst"

    # download
    $preferredTool = Get-CommandOptionValue @("-t", "-tool") $null $null
    Invoke-DownloadFile `
        $src `
        $dst `
        $preferredTool

    # validate download
    Write-InfoMessage "[~] validating file checksum"
    if( (Confirm-FileValidity $resourceContainer $dst) -eq $True) {
        Write-DebugMessage "[+] checksum is ok"
    } else {
        throw "[!] checksum validation failed for file: $dst"
    }

    # create checksum file
    New-ChecksumFile $dst
}

function Invoke-LatestDownload($resourceContainer) {
    # move all from staging to /latest
    # make metadata json file so that client can lookup information about files

    $stagingDirPath = $resourceContainer.StagingDirPath
    $latestDirPath  = $resourceContainer.LatestDirPath

    # clean up latest
    Write-DebugMessage "[~] cleaning latest dir: $latestDirPath"
    Invoke-CleanFolder $latestDirPath

    Write-InfoMessage  "[~] moving /download-staging to /latest"
    Write-DebugMessage " - src: $stagingDirPath/*"
    Write-DebugMessage " - dsr: $latestDirPath"

    Move-Item "$stagingDirPath/*"  `
              $latestDirPath `
              -Force

    Write-InfoMessage  "[~] writing metadata"
    Write-LatestMetadata $resourceContainer
}

function Invoke-DownloadResource($resource, $dstDir, $force = $False) {

    $jsonString = $resource | ConvertTo-Json -depth 5
    Write-DebugMessage "Downloading JSON data:`n$jsonString"

    $resourceContainer = Get-ResourceContainer $resource $dstDir $force
    Write-DebugMessage "container: $($resourceContainer | ConvertTo-JSON -depth 5 )"

    switch($resourceContainer.ResourceType) {
        "uplift/http-file"                 {  return Invoke-DownloadResourceHttpFile $resourceContainer  }
        "uplift/ms-visualstudio-2017-dist" {  return Invoke-DownloadResourceVS17Dist $resourceContainer  }
        default { throw "Unknown resource type: $($resource.ResourceType)"}
    }

    return 0
}

function Invoke-DownloadResourceHttpFile($resourceContainer) {

    Write-DebugMessage "Downloading http resource"

    $shouldDownload = $true

    # latest exists?
    $latestStatus = Get-LatestStatus $resourceContainer

    if($latestStatus -eq $True) {
        if($resourceContainer.Force -eq $True) {
            Write-WarnMessage "[~] -force is set, /latest is OK but will download again"
            $shouldDownload = $True
        } else {
            Write-InfoMessage "[+] /latest is OK, won't download"
            $shouldDownload = $False
        }
    } else {
        Write-DebugMessage "[~] /latest is NOT OK, will download it"
    }

    if($shouldDownload) {
        # download to staging
        # check if anything in cache?
        # check and create checksums
        Invoke-StagingDownload $resourceContainer

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

    Write-InfoMessage "[+] completed!"
}

function Invoke-ActionResourceDownload () {

    [System.ComponentModel.CategoryAttribute("ActionCommand")]
    [System.ComponentModel.DescriptionAttribute("Downloads resource file into local repository")]

    param(
        $commandOptions
    )

    $thirdOption = $commandOptions.Third

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

    Write-DebugMessage "cmd option: $thirdOption"
    $resources   = Get-AllResourcesMatch $thirdOption

    $resourceCount = $resources.Count
    $resourceIndex = 0

    if($resourceCount -eq 0) {
        throw "Cannot find any resource matching name: $thirdOption"
    } else {
        Write-InfoMessage "Found $resourceCount resource(s) matching id: $thirdOption"
    }

    $repoFolderPath = Get-LocalRepositoryPath $commandOptions
    $errors = @{}

    foreach($resource in $resources) {
        $resourceIndex = $resourceIndex  + 1
        $resourceId    = $resource.id

        try {
            Write-InfoMessage "[$resourceIndex/$resourceCount] resource: $resourceId"

            $result = Invoke-DownloadResource $resource $repoFolderPath (Get-ForceStatus)
            Write-DebugMessage " - result: $result"
        } catch {
            Write-ErrorMessage "Error while downloading resource: $resourceId"
            Write-ErrorMessage $_

            $errors[$resourceId] = $_
        }
    }

    return $errors.Count

    return 0
}