Public/Backup.ps1
|
# PSSnips — Backup and restore operations. # Export-SnipCollection and Import-SnipCollection provide portable ZIP backups # of the local snippet collection, enabling migration between machines and # safe archiving before major changes. function Export-SnipCollection { <# .SYNOPSIS Exports the full local snippet collection to a portable ZIP archive. .DESCRIPTION Gathers all snippet files from the configured SnippetsDir, the index.json metadata file, and (optionally) config.json, then packages them into a ZIP archive using Compress-Archive. The archive layout mirrors the PSSnips data directory so that Import-SnipCollection can restore it correctly: snippets\ — all snippet files index.json — snippet metadata index config.json (optional) The default destination is ~/Desktop/PSSnips-backup-<yyyyMMdd-HHmmss>.zip. .PARAMETER Path Optional. Destination path for the ZIP file. When omitted, the archive is written to the current user's Desktop with a timestamped name. .PARAMETER IncludeConfig Optional switch. Also includes config.json in the backup. A warning is displayed because config.json may contain a GitHub personal access token. .EXAMPLE Export-SnipCollection Creates a timestamped backup ZIP on the Desktop. .EXAMPLE Export-SnipCollection -Path C:\Backups\my-snips.zip Creates the backup at the specified path. .EXAMPLE Export-SnipCollection -IncludeConfig Includes config.json in the archive (warns about potential token exposure). .INPUTS None. This function does not accept pipeline input. .OUTPUTS None. Writes a success message with the ZIP path and file count to the host. .NOTES Requires PowerShell 5.0+ for Compress-Archive (already satisfied by the module's #Requires -Version 7.0 declaration). The archive is created via a temporary staging directory that is removed automatically on completion or error. #> [CmdletBinding()] param( [string]$Path = '', [switch]$IncludeConfig ) script:InitEnv $cfg = script:LoadCfg # Resolve destination path if (-not $Path) { $ts = Get-Date -Format 'yyyyMMdd-HHmmss' $Path = Join-Path ([System.Environment]::GetFolderPath('Desktop')) "PSSnips-backup-$ts.zip" } if ($IncludeConfig) { script:Out-Warn "config.json may contain a GitHub personal access token in plain text." } # Collect source files $snipFiles = @(Get-ChildItem $cfg.SnippetsDir -File -ErrorAction SilentlyContinue) $fileCount = $snipFiles.Count + $(if (Test-Path $script:IdxFile) { 1 } else { 0 }) + $(if ($IncludeConfig -and (Test-Path $script:CfgFile)) { 1 } else { 0 }) if ($fileCount -eq 0) { script:Out-Warn "No files found to backup."; return } $stageDir = Join-Path $env:TEMP "pssnips_export_$([System.IO.Path]::GetRandomFileName())" try { $stageSnips = Join-Path $stageDir 'snippets' New-Item -ItemType Directory -Path $stageSnips -Force | Out-Null foreach ($f in $snipFiles) { Copy-Item $f.FullName (Join-Path $stageSnips $f.Name) -Force } if (Test-Path $script:IdxFile) { Copy-Item $script:IdxFile (Join-Path $stageDir 'index.json') -Force } if ($IncludeConfig -and (Test-Path $script:CfgFile)) { Copy-Item $script:CfgFile (Join-Path $stageDir 'config.json') -Force } Compress-Archive -Path (Join-Path $stageDir '*') -DestinationPath $Path -Force -ErrorAction Stop script:Out-OK "Backup created: $Path ($fileCount file(s))" } catch { Write-Error "Failed to create backup: $_" -ErrorAction Continue } finally { if (Test-Path $stageDir) { Remove-Item $stageDir -Recurse -Force -ErrorAction SilentlyContinue } } } function Import-SnipCollection { <# .SYNOPSIS Restores a snippet collection from a PSSnips backup ZIP archive. .DESCRIPTION Extracts a ZIP archive created by Export-SnipCollection and copies the snippet files and index into the configured SnippetsDir. Three modes: Default (no switches) If the snippets directory already contains files, warns and aborts to prevent accidental overwrites. Use -Merge or -Force to proceed. -Merge Adds backup snippets that do not already exist locally. Existing snippets are preserved unless -Force is also specified. -Force (without -Merge) Replaces all local snippets with the backup contents. -Merge -Force Imports all backup snippets, overwriting any local conflicts. .PARAMETER Path Mandatory. Path to the PSSnips backup ZIP file created by Export-SnipCollection. .PARAMETER Merge Optional switch. Merges backup snippets into the existing collection. New snippets from the backup are added; existing local snippets are kept unless -Force is also provided. .PARAMETER Force Optional switch. When used alone, replaces all snippets with the backup. When used with -Merge, existing local snippets are overwritten on conflict. .EXAMPLE Import-SnipCollection -Path C:\Backups\my-snips.zip Restores snippets from the backup. Aborts if snippets already exist. .EXAMPLE Import-SnipCollection -Path C:\Backups\my-snips.zip -Merge Adds new snippets from the backup; existing local snippets are unaffected. .EXAMPLE Import-SnipCollection -Path C:\Backups\my-snips.zip -Force Replaces all local snippets with the backup contents. .EXAMPLE Import-SnipCollection -Path C:\Backups\my-snips.zip -Merge -Force Imports all backup snippets, overwriting local snippets on any conflict. .INPUTS None. This function does not accept pipeline input. .OUTPUTS None. Writes a summary of imported snippets to the host. .NOTES The archive is extracted to a temporary directory which is removed automatically after the import completes or on error. Supports -WhatIf via SupportsShouldProcess on Force (non-Merge) mode. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, Position=0, HelpMessage='Path to the PSSnips backup ZIP')] [ValidateNotNullOrEmpty()] [string]$Path, [switch]$Merge, [switch]$Force ) script:InitEnv $cfg = script:LoadCfg if (-not (Test-Path $Path)) { Write-Error "ZIP file not found: $Path" -ErrorAction Continue; return } $extractDir = Join-Path $env:TEMP "pssnips_import_$([System.IO.Path]::GetRandomFileName())" try { New-Item -ItemType Directory -Path $extractDir -Force | Out-Null Expand-Archive -Path $Path -DestinationPath $extractDir -Force -ErrorAction Stop # Locate index.json — may be at root or nested one level deep $backupIdxPath = Join-Path $extractDir 'index.json' if (-not (Test-Path $backupIdxPath)) { $found = @(Get-ChildItem $extractDir -Filter 'index.json' -Recurse -ErrorAction SilentlyContinue) if ($found.Count -gt 0) { $backupIdxPath = $found[0].FullName } } $backupIdx = if ($backupIdxPath -and (Test-Path $backupIdxPath)) { $raw = Get-Content $backupIdxPath -Raw -Encoding UTF8 -ErrorAction SilentlyContinue if ($raw) { $raw | ConvertFrom-Json -AsHashtable } else { @{ snippets = @{} } } } else { @{ snippets = @{} } } if (-not $backupIdx.ContainsKey('snippets')) { $backupIdx['snippets'] = @{} } foreach ($k in @($backupIdx.snippets.Keys)) { if ($backupIdx.snippets[$k] -is [hashtable]) { $backupIdx.snippets[$k] = [SnippetMetadata]::FromHashtable($backupIdx.snippets[$k]) } } # Locate the backup snippets directory $backupSnipDir = Join-Path $extractDir 'snippets' if (-not (Test-Path $backupSnipDir)) { $backupSnipDir = $extractDir } # Guard: warn if existing snippets and no merge/force specified $existingSnips = @(Get-ChildItem $cfg.SnippetsDir -File -ErrorAction SilentlyContinue) if ($existingSnips.Count -gt 0 -and -not $Merge -and -not $Force) { script:Out-Warn "Snippets directory already has $($existingSnips.Count) file(s). Use -Merge or -Force." return } $importCount = 0 if ($Merge) { $localIdx = script:LoadIdx foreach ($snipName in @($backupIdx.snippets.Keys)) { # Skip conflict unless -Force if ($localIdx.snippets.ContainsKey($snipName) -and -not $Force) { continue } $lang = $backupIdx.snippets[$snipName].Language $srcFile = Join-Path $backupSnipDir "$snipName.$lang" if (-not (Test-Path $srcFile)) { $found = @(Get-ChildItem $backupSnipDir -Filter "$snipName.*" -ErrorAction SilentlyContinue) if ($found.Count -gt 0) { $srcFile = $found[0].FullName } else { $srcFile = '' } } if ($srcFile -and (Test-Path $srcFile)) { Copy-Item $srcFile (Join-Path $cfg.SnippetsDir (Split-Path $srcFile -Leaf)) -Force $localIdx.snippets[$snipName] = $backupIdx.snippets[$snipName] $importCount++ } } script:SaveIdx -Idx $localIdx } else { # Force mode: replace everything if ($PSCmdlet.ShouldProcess($cfg.SnippetsDir, 'Replace all snippets from backup')) { $backupFiles = @(Get-ChildItem $backupSnipDir -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -notmatch '^(index|config)\.json$' }) foreach ($f in $backupFiles) { Copy-Item $f.FullName (Join-Path $cfg.SnippetsDir $f.Name) -Force $importCount++ } script:SaveIdx -Idx $backupIdx } } script:Out-OK "$importCount snippet(s) imported from backup." } finally { if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue } } } |