diff --git a/.gitignore b/.gitignore index fdc96d2a52..67ecc85d82 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ test/installer/tmp/* test/tmp/* *~ TestResults.xml +supporting/sqlite/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 00e336583e..8b6c2957d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [Unreleased](https://github.com/ScoopInstaller/Scoop/compare/master...develop) + +### Features + +- **scoop-search:** Use SQLite for caching apps to speed up local search ([#5851](https://github.com/ScoopInstaller/Scoop/issues/5851)) + ## [v0.4.0](https://github.com/ScoopInstaller/Scoop/compare/v0.3.1...v0.4.0) - 2024-04-18 ### Features diff --git a/lib/core.ps1 b/lib/core.ps1 index cf60f27e4c..f86f9956c9 100644 --- a/lib/core.ps1 +++ b/lib/core.ps1 @@ -216,6 +216,13 @@ function Complete-ConfigChange { } } } + + if ($Name -eq 'use_sqlite_cache' -and $Value -eq $true) { + . "$PSScriptRoot\..\lib\database.ps1" + . "$PSScriptRoot\..\lib\manifest.ps1" + info 'Initializing SQLite cache in progress... This may take a while, please wait.' + Set-ScoopDB + } } function setup_proxy() { diff --git a/lib/database.ps1 b/lib/database.ps1 new file mode 100644 index 0000000000..059e36d9aa --- /dev/null +++ b/lib/database.ps1 @@ -0,0 +1,359 @@ +# Description: Functions for interacting with the Scoop database cache + +<# +.SYNOPSIS + Get SQLite .NET driver +.DESCRIPTION + Download and extract the SQLite .NET driver from NuGet. +.PARAMETER Version + System.String + The version of the SQLite .NET driver to download. +.INPUTS + None +.OUTPUTS + System.Boolean + True if the SQLite .NET driver was successfully downloaded and extracted, otherwise false. +#> +function Get-SQLite { + param ( + [string]$Version = '1.0.118' + ) + # Install SQLite + try { + Write-Host "Downloading SQLite $Version..." -ForegroundColor DarkYellow + $sqlitePkgPath = "$env:TEMP\sqlite.nupkg" + $sqliteTempPath = "$env:TEMP\sqlite" + $sqlitePath = "$PSScriptRoot\..\supporting\sqlite" + Invoke-WebRequest -Uri "https://api.nuget.org/v3-flatcontainer/stub.system.data.sqlite.core.netframework/$version/stub.system.data.sqlite.core.netframework.$version.nupkg" -OutFile $sqlitePkgPath + Write-Host "Extracting SQLite $Version..." -ForegroundColor DarkYellow -NoNewline + Expand-Archive -Path $sqlitePkgPath -DestinationPath $sqliteTempPath -Force + New-Item -Path $sqlitePath -ItemType Directory -Force | Out-Null + Move-Item -Path "$sqliteTempPath\build\net45\*" -Destination $sqlitePath -Exclude '*.targets' -Force + Move-Item -Path "$sqliteTempPath\lib\net45\System.Data.SQLite.dll" -Destination $sqlitePath -Force + Remove-Item -Path $sqlitePkgPath, $sqliteTempPath -Recurse -Force + Write-Host ' Done' -ForegroundColor DarkYellow + return $true + } catch { + return $false + } +} + +<# +.SYNOPSIS + Close a SQLite database. +.DESCRIPTION + Close a SQLite database connection. +.PARAMETER InputObject + System.Data.SQLite.SQLiteConnection + The SQLite database connection to close. +.INPUTS + System.Data.SQLite.SQLiteConnection +.OUTPUTS + None +#> +function Close-ScoopDB { + [CmdletBinding()] + param ( + [Parameter(Mandatory, ValueFromPipeline)] + [System.Data.SQLite.SQLiteConnection] + $InputObject + ) + process { + $InputObject.Dispose() + } +} + +<# +.SYNOPSIS + Create a new SQLite database. +.DESCRIPTION + Create a new SQLite database connection and create the necessary tables. +.PARAMETER PassThru + System.Management.Automation.SwitchParameter + Return the SQLite database connection. +.INPUTS + None +.OUTPUTS + None + Default + + System.Data.SQLite.SQLiteConnection + The SQLite database connection if **PassThru** is used. +#> +function New-ScoopDB ([switch]$PassThru) { + # Load System.Data.SQLite + if (!('System.Data.SQLite.SQLiteConnection' -as [Type])) { + try { + if (!(Test-Path -Path "$PSScriptRoot\..\supporting\sqlite\System.Data.SQLite.dll")) { + Get-SQLite | Out-Null + } + Add-Type -Path "$PSScriptRoot\..\supporting\sqlite\System.Data.SQLite.dll" + } catch { + throw "Scoop's Database cache requires the ADO.NET driver:`n`thttp://system.data.sqlite.org/index.html/doc/trunk/www/downloads.wiki" + } + } + $dbPath = Join-Path $scoopdir 'scoop.db' + $db = New-Object -TypeName System.Data.SQLite.SQLiteConnection + $db.ConnectionString = "Data Source=$dbPath" + $db.ParseViaFramework = $true # Allow UNC path + $db.Open() + $tableCommand = $db.CreateCommand() + $tableCommand.CommandText = "CREATE TABLE IF NOT EXISTS 'app' ( + name TEXT NOT NULL COLLATE NOCASE, + description TEXT NOT NULL, + version TEXT NOT NULL, + bucket VARCHAR NOT NULL, + manifest JSON NOT NULL, + binary TEXT, + shortcut TEXT, + dependency TEXT, + suggest TEXT, + PRIMARY KEY (name, version, bucket) + )" + $tableCommand.ExecuteNonQuery() | Out-Null + $tableCommand.Dispose() + if ($PassThru) { + return $db + } else { + $db.Dispose() + } +} + +<# +.SYNOPSIS + Set Scoop database item(s). +.DESCRIPTION + Insert or replace Scoop database item(s) into the database. +.PARAMETER InputObject + System.Object[] + The database item(s) to insert or replace. +.INPUTS + System.Object[] +.OUTPUTS + None +#> +function Set-ScoopDBItem { + [CmdletBinding()] + param ( + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [psobject[]] + $InputObject + ) + + begin { + $db = New-ScoopDB -PassThru + $dbTrans = $db.BeginTransaction() + # TODO Support [hashtable]$InputObject + $colName = @($InputObject | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name) + $dbQuery = "INSERT OR REPLACE INTO app ($($colName -join ', ')) VALUES ($('@' + ($colName -join ', @')))" + $dbCommand = $db.CreateCommand() + $dbCommand.CommandText = $dbQuery + } + process { + foreach ($item in $InputObject) { + $item.PSObject.Properties | ForEach-Object { + $dbCommand.Parameters.AddWithValue("@$($_.Name)", $_.Value) | Out-Null + } + $dbCommand.ExecuteNonQuery() | Out-Null + } + } + end { + try { + $dbTrans.Commit() + } catch { + $dbTrans.Rollback() + throw $_ + } finally { + $db.Dispose() + } + } +} + +<# +.SYNOPSIS + Set Scoop app database item(s). +.DESCRIPTION + Insert or replace Scoop app(s) into the database. +.PARAMETER Path + System.String + The path to the bucket. +.PARAMETER CommitHash + System.String + The commit hash to compare with the HEAD. +.INPUTS + None +.OUTPUTS + None +#> +function Set-ScoopDB { + [CmdletBinding()] + param ( + [Parameter(Position = 0, ValueFromPipeline)] + [string[]] + $Path + ) + + begin { + $list = [System.Collections.Generic.List[PSCustomObject]]::new() + $arch = Get-DefaultArchitecture + } + process { + if ($Path.Count -eq 0) { + $bucketPath = Get-LocalBucket | ForEach-Object { Join-Path $bucketsdir $_ } + $Path = (Get-ChildItem $bucketPath -Filter '*.json' -Recurse).FullName + } + $Path | ForEach-Object { + $manifestRaw = [System.IO.File]::ReadAllText($_) + $manifest = $manifestRaw | ConvertFrom-Json -ErrorAction Continue + if ($null -ne $manifest.version) { + $list.Add([pscustomobject]@{ + name = $($_ -replace '.*[\\/]([^\\/]+)\.json$', '$1') + description = if ($manifest.description) { $manifest.description } else { '' } + version = $manifest.version + bucket = $($_ -replace '.*buckets[\\/]([^\\/]+)(?:[\\/].*)', '$1') + manifest = $manifestRaw + binary = $( + $result = @() + @(arch_specific 'bin' $manifest $arch) | ForEach-Object { + if ($_ -is [System.Array]) { + $result += "$($_[1]).$($_[0].Split('.')[-1])" + } else { + $result += $_ + } + } + $result -replace '.*?([^\\/]+)?(\.(exe|bat|cmd|ps1|jar|py))$', '$1' -join ' | ' + ) + shortcut = $( + $result = @() + @(arch_specific 'shortcuts' $manifest $arch) | ForEach-Object { + $result += $_[1] + } + $result -replace '.*?([^\\/]+$)', '$1' -join ' | ' + ) + dependency = $manifest.depends -join ' | ' + suggest = $( + $suggest_output = @() + $manifest.suggest.PSObject.Properties | ForEach-Object { + $suggest_output += $_.Value -join ' | ' + } + $suggest_output -join ' | ' + ) + }) + } + } + } + end { + if ($list.Count -ne 0) { + Set-ScoopDBItem $list + } + } +} + +<# +.SYNOPSIS + Select Scoop database item(s). +.DESCRIPTION + Select Scoop database item(s) from the database. + The pattern is matched against the name, binaries, and shortcuts columns for apps. +.PARAMETER Pattern + System.String + The pattern to search for. If is an empty string, all items will be returned. +.INPUTS + System.String +.OUTPUTS + System.Data.DataTable + The selected database item(s). +#> +function Select-ScoopDBItem { + [CmdletBinding()] + param ( + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [AllowEmptyString()] + [string] + $Pattern, + [Parameter(Mandatory, Position = 1)] + [string[]] + $From + ) + + begin { + $db = New-ScoopDB -PassThru + $dbAdapter = New-Object -TypeName System.Data.SQLite.SQLiteDataAdapter + $result = New-Object System.Data.DataTable + $dbQuery = "SELECT * FROM app WHERE $(($From -join ' LIKE @Pattern OR ') + ' LIKE @Pattern')" + $dbQuery = "SELECT * FROM ($($dbQuery + ' ORDER BY version DESC')) GROUP BY name, bucket" + $dbCommand = $db.CreateCommand() + $dbCommand.CommandText = $dbQuery + $dbCommand.CommandType = [System.Data.CommandType]::Text + } + process { + $dbCommand.Parameters.AddWithValue('@Pattern', $(if ($Pattern -eq '') { '%' } else { '%' + $Pattern + '%' })) | Out-Null + $dbAdapter.SelectCommand = $dbCommand + [void]$dbAdapter.Fill($result) + } + end { + $db.Dispose() + return $result + } +} + +<# +.SYNOPSIS + Get Scoop database item. +.DESCRIPTION + Get Scoop database item from the database. +.PARAMETER Name + System.String + The name of the item to get. +.PARAMETER Bucket + System.String + The bucket of the item to get. +.PARAMETER Version + System.String + The version of the item to get. If not provided, the latest version will be returned. +.INPUTS + System.String +.OUTPUTS + System.Data.DataTable + The selected database item. +#> +function Get-ScoopDBItem { + [CmdletBinding()] + param ( + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [string] + $Name, + [Parameter(Mandatory, Position = 1)] + [string] + $Bucket, + [Parameter(Position = 2)] + [string] + $Version + ) + + begin { + $db = New-ScoopDB -PassThru + $dbAdapter = New-Object -TypeName System.Data.SQLite.SQLiteDataAdapter + $result = New-Object System.Data.DataTable + $dbQuery = 'SELECT * FROM app WHERE name = @Name AND bucket = @Bucket' + if ($Version) { + $dbQuery += ' AND version = @Version' + } else { + $dbQuery += ' ORDER BY version DESC LIMIT 1' + } + $dbCommand = $db.CreateCommand() + $dbCommand.CommandText = $dbQuery + $dbCommand.CommandType = [System.Data.CommandType]::Text + } + process { + $dbCommand.Parameters.AddWithValue('@Name', $Name) | Out-Null + $dbCommand.Parameters.AddWithValue('@Bucket', $Bucket) | Out-Null + $dbCommand.Parameters.AddWithValue('@Version', $Version) | Out-Null + $dbAdapter.SelectCommand = $dbCommand + [void]$dbAdapter.Fill($result) + } + end { + $db.Dispose() + return $result + } +} diff --git a/lib/manifest.ps1 b/lib/manifest.ps1 index f66755c6f4..9ca618158b 100644 --- a/lib/manifest.ps1 +++ b/lib/manifest.ps1 @@ -23,7 +23,7 @@ function url_manifest($url) { } catch { throw } - if(!$str) { return $null } + if (!$str) { return $null } try { $str | ConvertFrom-Json -ErrorAction Stop } catch { @@ -137,16 +137,26 @@ function generate_user_manifest($app, $bucket, $version) { warn "Given version ($version) does not match manifest ($($manifest.version))" warn "Attempting to generate manifest for '$app' ($version)" + ensure (usermanifestsdir) | Out-Null + $manifest_path = "$(usermanifestsdir)\$app.json" + + if (get_config USE_SQLITE_CACHE) { + $cached_manifest = (Get-ScoopDBItem -Name $app -Bucket $bucket -Version $version).manifest + if ($cached_manifest) { + $cached_manifest | Out-UTF8File $manifest_path + return $manifest_path + } + } + if (!($manifest.autoupdate)) { abort "'$app' does not have autoupdate capability`r`ncouldn't find manifest for '$app@$version'" } - ensure (usermanifestsdir) | out-null try { - Invoke-AutoUpdate $app "$(Convert-Path (usermanifestsdir))\$app.json" $manifest $version $(@{ }) - return Convert-Path (usermanifest $app) + Invoke-AutoUpdate $app $manifest_path $manifest $version $(@{ }) + return $manifest_path } catch { - write-host -f darkred "Could not install $app@$version" + Write-Host -ForegroundColor DarkRed "Could not install $app@$version" } return $null @@ -156,5 +166,5 @@ function url($manifest, $arch) { arch_specific 'url' $manifest $arch } function installer($manifest, $arch) { arch_specific 'installer' $manifest $arch } function uninstaller($manifest, $arch) { arch_specific 'uninstaller' $manifest $arch } function hash($manifest, $arch) { arch_specific 'hash' $manifest $arch } -function extract_dir($manifest, $arch) { arch_specific 'extract_dir' $manifest $arch} -function extract_to($manifest, $arch) { arch_specific 'extract_to' $manifest $arch} +function extract_dir($manifest, $arch) { arch_specific 'extract_dir' $manifest $arch } +function extract_to($manifest, $arch) { arch_specific 'extract_to' $manifest $arch } diff --git a/libexec/scoop-config.ps1 b/libexec/scoop-config.ps1 index ff0bff3eee..6007bd6434 100644 --- a/libexec/scoop-config.ps1 +++ b/libexec/scoop-config.ps1 @@ -27,6 +27,9 @@ # use_lessmsi: $true|$false # Prefer lessmsi utility over native msiexec. # +# use_sqlite_cache: $true|$false +# Use SQLite database for caching. This is useful for speeding up 'scoop search' and 'scoop shim' commands. +# # no_junction: $true|$false # The 'current' version alias will not be used. Shims and shortcuts will point to specific version instead. # diff --git a/libexec/scoop-download.ps1 b/libexec/scoop-download.ps1 index ef43f8f41d..a4ba10d36e 100644 --- a/libexec/scoop-download.ps1 +++ b/libexec/scoop-download.ps1 @@ -24,6 +24,9 @@ . "$PSScriptRoot\..\lib\autoupdate.ps1" # 'generate_user_manifest' (indirectly) . "$PSScriptRoot\..\lib\manifest.ps1" # 'generate_user_manifest' 'Get-Manifest' . "$PSScriptRoot\..\lib\install.ps1" +if (get_config USE_SQLITE_CACHE) { + . "$PSScriptRoot\..\lib\database.ps1" +} $opt, $apps, $err = getopt $args 'fhua:' 'force', 'no-hash-check', 'no-update-scoop', 'arch=' if ($err) { error "scoop download: $err"; exit 1 } diff --git a/libexec/scoop-install.ps1 b/libexec/scoop-install.ps1 index fac03d71f4..1605efd2ac 100644 --- a/libexec/scoop-install.ps1 +++ b/libexec/scoop-install.ps1 @@ -32,6 +32,9 @@ . "$PSScriptRoot\..\lib\psmodules.ps1" . "$PSScriptRoot\..\lib\versions.ps1" . "$PSScriptRoot\..\lib\depends.ps1" +if (get_config USE_SQLITE_CACHE) { + . "$PSScriptRoot\..\lib\database.ps1" +} $opt, $apps, $err = getopt $args 'gikusa:' 'global', 'independent', 'no-cache', 'no-update-scoop', 'skip', 'arch=' if ($err) { "scoop install: $err"; exit 1 } diff --git a/libexec/scoop-search.ps1 b/libexec/scoop-search.ps1 index a9013afd02..be044513c1 100644 --- a/libexec/scoop-search.ps1 +++ b/libexec/scoop-search.ps1 @@ -3,6 +3,8 @@ # Help: Searches for apps that are available to install. # # If used with [query], shows app names that match the query. +# - With 'use_sqlite_cache' enabled, [query] is partially matched against app names, binaries, and shortcuts. +# - Without 'use_sqlite_cache', [query] can be a regular expression to match against app names and binaries. # Without [query], shows all the available apps. param($query) @@ -11,16 +13,10 @@ param($query) $list = [System.Collections.Generic.List[PSCustomObject]]::new() -try { - $query = New-Object Regex $query, 'IgnoreCase' -} catch { - abort "Invalid regular expression: $($_.Exception.InnerException.Message)" -} - $githubtoken = Get-GitHubToken $authheader = @{} if ($githubtoken) { - $authheader = @{'Authorization' = "token $githubtoken"} + $authheader = @{'Authorization' = "token $githubtoken" } } function bin_match($manifest, $query) { @@ -39,16 +35,16 @@ function bin_match($manifest, $query) { function bin_match_json($json, $query) { [System.Text.Json.JsonElement]$bin = [System.Text.Json.JsonElement]::new() - if (!$json.RootElement.TryGetProperty("bin", [ref] $bin)) { return $false } + if (!$json.RootElement.TryGetProperty('bin', [ref] $bin)) { return $false } $bins = @() - if($bin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($bin) -match $query) { + if ($bin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($bin) -match $query) { $bins += [System.IO.Path]::GetFileName($bin) } elseif ($bin.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) { - foreach($subbin in $bin.EnumerateArray()) { - if($subbin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($subbin) -match $query) { + foreach ($subbin in $bin.EnumerateArray()) { + if ($subbin.ValueKind -eq [System.Text.Json.JsonValueKind]::String -and [System.IO.Path]::GetFileNameWithoutExtension($subbin) -match $query) { $bins += [System.IO.Path]::GetFileName($subbin) } elseif ($subbin.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) { - if([System.IO.Path]::GetFileNameWithoutExtension($subbin[0]) -match $query) { + if ([System.IO.Path]::GetFileNameWithoutExtension($subbin[0]) -match $query) { $bins += [System.IO.Path]::GetFileName($subbin[0]) } elseif ($subbin.GetArrayLength() -ge 2 -and $subbin[1] -match $query) { $bins += $subbin[1] @@ -70,20 +66,20 @@ function search_bucket($bucket, $query) { if ($name -match $query) { $list.Add([PSCustomObject]@{ - Name = $name - Version = $json.RootElement.GetProperty("version") - Source = $bucket - Binaries = "" - }) + Name = $name + Version = $json.RootElement.GetProperty('version') + Source = $bucket + Binaries = '' + }) } else { $bin = bin_match_json $json $query if ($bin) { $list.Add([PSCustomObject]@{ - Name = $name - Version = $json.RootElement.GetProperty("version") - Source = $bucket - Binaries = $bin -join ' | ' - }) + Name = $name + Version = $json.RootElement.GetProperty('version') + Source = $bucket + Binaries = $bin -join ' | ' + }) } } } @@ -99,20 +95,20 @@ function search_bucket_legacy($bucket, $query) { if ($name -match $query) { $list.Add([PSCustomObject]@{ - Name = $name - Version = $manifest.Version - Source = $bucket - Binaries = "" - }) + Name = $name + Version = $manifest.Version + Source = $bucket + Binaries = '' + }) } else { $bin = bin_match $manifest $query if ($bin) { $list.Add([PSCustomObject]@{ - Name = $name - Version = $manifest.Version - Source = $bucket - Binaries = $bin -join ' | ' - }) + Name = $name + Version = $manifest.Version + Source = $bucket + Binaries = $bin -join ' | ' + }) } } } @@ -154,7 +150,7 @@ function search_remotes($query) { $names = $buckets | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty name $results = $names | Where-Object { !(Test-Path $(Find-BucketDirectory $_)) } | ForEach-Object { - @{ "bucket" = $_; "results" = (search_remote $_ $query) } + @{ 'bucket' = $_; 'results' = (search_remote $_ $query) } } | Where-Object { $_.results } if ($results.count -gt 0) { @@ -175,25 +171,45 @@ function search_remotes($query) { $remote_list } -$jsonTextAvailable = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-object { [System.IO.Path]::GetFileNameWithoutExtension($_.Location) -eq "System.Text.Json" } +if (get_config USE_SQLITE_CACHE) { + . "$PSScriptRoot\..\lib\database.ps1" + Select-ScoopDBItem $query -From @('name', 'binary', 'shortcut') | + Select-Object -Property name, version, bucket, binary | + ForEach-Object { + $list.Add([PSCustomObject]@{ + Name = $_.name + Version = $_.version + Source = $_.bucket + Binaries = $_.binary + }) + } +} else { + try { + $query = New-Object Regex $query, 'IgnoreCase' + } catch { + abort "Invalid regular expression: $($_.Exception.InnerException.Message)" + } -Get-LocalBucket | ForEach-Object { - if ($jsonTextAvailable) { - search_bucket $_ $query - } else { - search_bucket_legacy $_ $query + $jsonTextAvailable = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { [System.IO.Path]::GetFileNameWithoutExtension($_.Location) -eq 'System.Text.Json' } + + Get-LocalBucket | ForEach-Object { + if ($jsonTextAvailable) { + search_bucket $_ $query + } else { + search_bucket_legacy $_ $query + } } } if ($list.Count -gt 0) { - Write-Host "Results from local buckets..." + Write-Host 'Results from local buckets...' $list } if ($list.Count -eq 0 -and !(github_ratelimit_reached)) { $remote_results = search_remotes $query if (!$remote_results) { - warn "No matches found." + warn 'No matches found.' exit 1 } $remote_results diff --git a/libexec/scoop-update.ps1 b/libexec/scoop-update.ps1 index 354fb2e839..fdcf8707a3 100644 --- a/libexec/scoop-update.ps1 +++ b/libexec/scoop-update.ps1 @@ -24,6 +24,9 @@ . "$PSScriptRoot\..\lib\versions.ps1" . "$PSScriptRoot\..\lib\depends.ps1" . "$PSScriptRoot\..\lib\install.ps1" +if (get_config USE_SQLITE_CACHE) { + . "$PSScriptRoot\..\lib\database.ps1" +} $opt, $apps, $err = getopt $args 'gfiksqa' 'global', 'force', 'independent', 'no-cache', 'skip', 'quiet', 'all' if ($err) { "scoop update: $err"; exit 1 } @@ -38,21 +41,21 @@ $all = $opt.a -or $opt.all # load config $configRepo = get_config SCOOP_REPO if (!$configRepo) { - $configRepo = "https://github.com/ScoopInstaller/Scoop" + $configRepo = 'https://github.com/ScoopInstaller/Scoop' set_config SCOOP_REPO $configRepo | Out-Null } # Find current update channel from config $configBranch = get_config SCOOP_BRANCH if (!$configBranch) { - $configBranch = "master" + $configBranch = 'master' set_config SCOOP_BRANCH $configBranch | Out-Null } -if(($PSVersionTable.PSVersion.Major) -lt 5) { +if (($PSVersionTable.PSVersion.Major) -lt 5) { # check powershell version - Write-Output "PowerShell 5 or later is required to run Scoop." - Write-Output "Upgrade PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-windows" + Write-Output 'PowerShell 5 or later is required to run Scoop.' + Write-Output 'Upgrade PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-windows' break } $show_update_log = get_config SHOW_UPDATE_LOG $true @@ -63,7 +66,7 @@ function Sync-Scoop { [Switch]$Log ) # Test if Scoop Core is hold - if(Test-ScoopCoreOnHold) { + if (Test-ScoopCoreOnHold) { return } @@ -108,10 +111,10 @@ function Sync-Scoop { # Stash uncommitted changes if (Invoke-Git -Path $currentdir -ArgumentList @('diff', 'HEAD', '--name-only')) { if (get_config AUTOSTASH_ON_CONFLICT) { - warn "Uncommitted changes detected. Stashing..." + warn 'Uncommitted changes detected. Stashing...' Invoke-Git -Path $currentdir -ArgumentList @('stash', 'push', '-m', "WIP at $([System.DateTime]::Now.ToString('o'))", '-u', '-q') } else { - warn "Uncommitted changes detected. Update aborted." + warn 'Uncommitted changes detected. Update aborted.' return } } @@ -152,7 +155,7 @@ function Sync-Bucket { Param ( [Switch]$Log ) - Write-Host "Updating Buckets..." + Write-Host 'Updating Buckets...' if (!(Test-Path (Join-Path (Find-BucketDirectory 'main' -Root) '.git'))) { info "Converting 'main' bucket to git repo..." @@ -170,14 +173,15 @@ function Sync-Bucket { $buckets = Get-LocalBucket | ForEach-Object { $path = Find-BucketDirectory $_ -Root return @{ - name = $_ + name = $_ valid = Test-Path (Join-Path $path '.git') - path = $path + path = $path } } $buckets | Where-Object { !$_.valid } | ForEach-Object { Write-Host "'$($_.name)' is not a git repository. Skipped." } + $updatedFiles = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new()) if ($PSVersionTable.PSVersion.Major -ge 7) { # Parallel parameter is available since PowerShell 7 $buckets | Where-Object { $_.valid } | ForEach-Object -ThrottleLimit 5 -Parallel { @@ -191,6 +195,13 @@ function Sync-Bucket { if ($using:Log) { Invoke-GitLog -Path $bucketLoc -Name $name -CommitHash $previousCommit } + if (get_config USE_SQLITE_CACHE) { + Invoke-Git -Path $bucketLoc -ArgumentList @('diff', '--name-only', '--diff-filter=d', $previousCommit) | Where-Object { + $_ -match '^[^.].*\.json$' + } | ForEach-Object { + [void]($using:updatedFiles).Add($(Join-Path $bucketLoc $_)) + } + } } } else { $buckets | Where-Object { $_.valid } | ForEach-Object { @@ -202,8 +213,19 @@ function Sync-Bucket { if ($Log) { Invoke-GitLog -Path $bucketLoc -Name $name -CommitHash $previousCommit } + if (get_config USE_SQLITE_CACHE) { + Invoke-Git -Path $bucketLoc -ArgumentList @('diff', '--name-only', '--diff-filter=d', $previousCommit) | Where-Object { + $_ -match '^[^.].*\.json$' + } | ForEach-Object { + [void]($updatedFiles).Add($(Join-Path $bucketLoc $_)) + } + } } } + if ((get_config USE_SQLITE_CACHE) -and ($updatedFiles.Count -gt 0)) { + info 'Updating cache...' + Set-ScoopDB -Path $updatedFiles + } } function update($app, $global, $quiet = $false, $independent, $suggested, $use_cache = $true, $check_hash = $true) { @@ -251,7 +273,7 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c # region Workaround # Workaround for https://github.com/ScoopInstaller/Scoop/issues/2220 until install is refactored # Remove and replace whole region after proper fix - Write-Host "Downloading new version" + Write-Host 'Downloading new version' if (Test-Aria2Enabled) { Invoke-CachedAria2Download $app $version $manifest $architecture $cachedir $manifest.cookie $true $check_hash } else { @@ -269,12 +291,12 @@ function update($app, $global, $quiet = $false, $independent, $suggested, $use_c error $err if (Test-Path $source) { # rm cached file - Remove-Item -force $source + Remove-Item -Force $source } if ($url.Contains('sourceforge.net')) { Write-Host -f yellow 'SourceForge.net is known for causing hash validation fails. Please try again before opening a ticket.' } - abort $(new_issue_msg $app $bucket "hash check failed") + abort $(new_issue_msg $app $bucket 'hash check failed') } } } @@ -404,11 +426,11 @@ if (-not ($apps -or $all)) { } elseif ($outdated.Length -eq 0) { Write-Host -f Green "Latest versions for all apps are installed! For more information try 'scoop status'" } else { - Write-Host -f DarkCyan "Updating one outdated app:" + Write-Host -f DarkCyan 'Updating one outdated app:' } } - $suggested = @{}; + $suggested = @{} # $outdated is a list of ($app, $global) tuples $outdated | ForEach-Object { update @_ $quiet $independent $suggested $use_cache $check_hash } }