├── .gitignore ├── LICENSE ├── functions.ps1 ├── README.md └── scoop-backup.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | backups/** -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute 4 | this software, either in source code form or as a compiled binary, for any 5 | purpose, commercial or non-commercial, and by any means. 6 | 7 | In jurisdictions that recognize copyright laws, the author or authors of this 8 | software dedicate any and all copyright interest in the software to the public 9 | domain. We make this dedication for the benefit of the public at large and 10 | to the detriment of our heirs and 11 | 12 | successors. We intend this dedication to be an overt act of relinquishment 13 | in perpetuity of all present and future rights to this software under copyright 14 | law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 19 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH 21 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | For more information, please refer to 24 | -------------------------------------------------------------------------------- /functions.ps1: -------------------------------------------------------------------------------- 1 | # passed_one_of() but accepts our custom argument object 2 | function passed($argument) { 3 | return passed_one_of($argument.Aliases) 4 | } 5 | 6 | # checks for arguments that match one of the $aliases, strips out matches, and returns true if they were found 7 | function passed_one_of($aliases) { 8 | $mismatches = $arguments | where { $aliases -notcontains $_ } 9 | if($mismatches.Count -lt $arguments.Count) { 10 | $global:arguments = $mismatches 11 | return $true 12 | } 13 | return $false 14 | } 15 | 16 | # creates a hashtable object storing information about this argument 17 | function argument($description) { 18 | return @{ 19 | Description = $description; 20 | Aliases = $args 21 | } 22 | } 23 | 24 | # prints a message with the error formatting, but does not break 25 | function complain($message) { 26 | Write-Host -ForegroundColor $host.PrivateData.ErrorForegroundColor -BackgroundColor $host.PrivateData.ErrorBackgroundColor $message 27 | } 28 | 29 | # executes the first expression, but falls back to the second if it isn't defined 30 | function invoke($cmd, $fallback) { 31 | if(Get-Command $cmd -errorAction SilentlyContinue) { 32 | Invoke-Expression $cmd 33 | } else { 34 | Invoke-Expression $fallback 35 | } 36 | } 37 | 38 | # appends another line to our in-progress script file 39 | function append($line) { 40 | $global:cmd += ($line + "`n") 41 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scoop-backup (archived) 2 | 3 | > **Warning**: This package was originally created before the addition of `scoop import` 4 | > and `scoop export` to the main scoop executable. I suggest you use that functionality 5 | > instead of this package, although you can still install and use it using the instructions 6 | > below. 7 | 8 | Additional functionality for the [Scoop Package Manager](https://scoop.sh). 9 | 10 | Backup your entire current scoop installation with one command: 11 | 12 | ```powershell 13 | scoop-backup 14 | ``` 15 | 16 | This produces the compressed backup `scoop-backup\backups\backup-YYMMDD.ps1`, which can now be executed on any Windows computer to restore your entire Scoop installation, including all buckets and apps. 17 | 18 | scoop-backup can be installed via, you guessed it, scoop, via the [knox-scoop bucket](https://github.com/KNOXDEV/knox-scoop): 19 | 20 | ```powershell 21 | scoop bucket add knox-scoop https://github.com/KNOXDEV/knox-scoop 22 | scoop install scoop-backup 23 | ``` 24 | 25 | ## use cases 26 | * Backing up a computer's software for easy reinstallation 27 | * Deploying an identical scoop-based installation to many computers 28 | * Syncing configurations between two personal computers for an effortless, uniform experience 29 | 30 | ## options 31 | 32 | Save to a different folder: 33 | ```powershell 34 | scoop-backup .\path\to\folder\ 35 | ``` 36 | 37 | Save backup as a compressed batch file: 38 | ```powershell 39 | scoop-backup --compress 40 | # >> "output-folder\backup.bat" 41 | ``` 42 | 43 | ## ~~future plans~~ 44 | 45 | * ~~Implement a strategy for backing up the persistence directory. For many apps, this is trivial (ccleaner), but others pose a significant challenge (jetbrains-toolbox).~~ 46 | -------------------------------------------------------------------------------- /scoop-backup.ps1: -------------------------------------------------------------------------------- 1 | #requires -Version 3 2 | # include functions and argument processing 3 | $global:arguments = $args 4 | . "$psscriptroot\functions.ps1" 5 | 6 | $supported_arguments = @( 7 | (argument 'prints this help message' '-h' '--help' '/?' '-?'), 8 | (argument 'compresses the restoration script as an encoded batch file' '-c' '--compress') 9 | ) 10 | 11 | # parse various default path settings 12 | $compressed = passed($supported_arguments[1]) 13 | $default_filename = "backup-$(Get-Date -f yyMMdd)" 14 | $default_file = "$default_filename" + $(if($compressed) {".bat"} else {".ps1"}) 15 | $default_destination = "$psscriptroot\backups\$default_file" 16 | $destination = $default_destination 17 | 18 | # check for help arguments 19 | if(passed($supported_arguments[0])) { 20 | Write-Host "Usage: scoop-backup [flags] [destination_folder] `n" 21 | Write-Host "Default destination: $default_destination `n" 22 | 23 | $supported_arguments | ForEach-Object { 24 | Write-Host "$($_.Aliases) `t $($_.Description)" 25 | } 26 | break 27 | } 28 | 29 | # filter all paths from our arguments and set our output folder the last path found 30 | $global:arguments = $arguments | Where { 31 | if(Test-Path -Path $_ -PathType container) { 32 | $destination = "$_\$default_file" 33 | return $false 34 | } 35 | complain "the following path does not exist or is not a directory: $_" 36 | return $true 37 | } 38 | 39 | # complain about unrecognized arguments and abort if found 40 | if($arguments.Count -ne 0) { 41 | complain "unrecognized arguments: $arguments" 42 | complain "see: 'scoop-backup --help'" 43 | break 44 | } 45 | 46 | 47 | 48 | # import core libraries 49 | try { 50 | if(!$env:SCOOP_HOME) { $env:SCOOP_HOME = Resolve-Path (scoop prefix scoop) } 51 | $scooplib = "$env:SCOOP_HOME\lib" 52 | . "$scooplib\core.ps1" 53 | . "$scooplib\commands.ps1" 54 | . "$scooplib\help.ps1" 55 | . "$scooplib\manifest.ps1" 56 | . "$scooplib\buckets.ps1" 57 | . "$scooplib\versions.ps1" 58 | } catch { 59 | Write-Output "Failed to import Scoop libraries, not found on path" 60 | break 61 | } 62 | 63 | # creates initial restoration script content 64 | $global:cmd = "if (Get-Command -Name scoop -ErrorAction SilentlyContinue) {} else {iwr -useb get.scoop.sh | iex}`n" 65 | 66 | # if we need to install some buckets, we'll need to install git first 67 | $buckets = invoke "Get-LocalBucket" "buckets" 68 | if(($buckets | Measure-Object).Count -gt 0) { 69 | append "scoop install git" 70 | append "scoop update" 71 | 72 | # add each bucket installation on its own line 73 | $buckets | ForEach-Object { 74 | $repo_url = git config --file "$bucketsdir\$_\.git\config" remote.origin.url 75 | "$_ $repo_url" 76 | } | ForEach-Object { append "scoop bucket add $_" } 77 | } 78 | 79 | # next, we install apps 80 | $apps = installed_apps 81 | if(($apps | Measure-Object).Count -gt 0) { 82 | 83 | # installing each app on a new line is, unfortunately, more resilient 84 | $apps | ForEach-Object { 85 | $info = install_info $_ (Select-CurrentVersion -AppName $_ -Global:$false) $false 86 | if($info.url) { $info.url } else { "$($info.bucket)/$_" } 87 | } | ForEach-Object { append "scoop install $_" } 88 | } 89 | 90 | # next, we install global apps 91 | $globals = installed_apps $true 92 | if(($globals | Measure-Object).Count -gt 0) { 93 | append 'scoop install main/gsudo' 94 | 95 | # installing each app on a new line is, unfortunately, more resilient 96 | append ("gsudo powershell -Command {`nscoop install --global " + (($globals | ForEach-Object { 97 | $info = install_info $_ (Select-CurrentVersion -AppName $_ -Global:$true) $true 98 | if($info.url) { $($info.url) } else { "$($info.bucket)/$_" } 99 | }) -Join "`nscoop install --global ") + "`n}") 100 | } 101 | 102 | # finally, we install any scoop aliases 103 | # I've noticed that on old configs from scoop some of this operations will error, hense the try 104 | try { 105 | (get_config 'alias').PSObject.Properties.GetEnumerator() | ForEach-Object { 106 | $content = Get-Content (command_path $_.Name) 107 | $command = ($content | Select-Object -Skip 1).Trim() 108 | $summary = (summary $content).Trim() 109 | 110 | append "scoop alias add '$($_.name)' '$command' '$summary'" 111 | } 112 | } catch { 113 | Write-Output "Failed to enumerate aliases, not backed up" 114 | } 115 | 116 | # writing the final output 117 | New-Item $destination -Force | Out-Null 118 | if($compressed) { 119 | $cmd_bytes = [System.Text.Encoding]::Unicode.GetBytes($cmd) 120 | $cmd_encoded = '@echo off' + [environment]::NewLine ` 121 | + "powershell.exe -NoProfile -EncodedCommand " ` 122 | + [Convert]::ToBase64String($cmd_bytes) + [environment]::NewLine ` 123 | + 'pause' 124 | Add-Content -Path $destination -Value $cmd_encoded 125 | } else { 126 | Add-Content -Path $destination -Value $cmd 127 | } 128 | 129 | Write-Output "backed up to: $destination" 130 | --------------------------------------------------------------------------------