Add portable flash-drive installer
- build-release.ps1: builds Docker image, saves to tar, bundles everything into dist\ ready to copy to a flash drive - installer/install.ps1: checks WSL2, Docker Desktop, loads image (or builds from source as fallback), prompts for photo/data paths, writes docker-compose.override.yml, starts container, creates desktop shortcut - installer/uninstall.ps1: stops container, optionally removes image and data, removes shortcut and app directory - installer/dupfinder-start-stop.ps1: start/stop/restart/open helper copied to target machine during install; desktop shortcut uses -Action open which polls until the app is responsive before launching browser Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
153
build-release.ps1
Normal file
153
build-release.ps1
Normal file
@@ -0,0 +1,153 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds the DupFinder flash-drive installer bundle.
|
||||
.DESCRIPTION
|
||||
1. Builds the Docker image
|
||||
2. Saves it to dist\image\dupfinder.tar
|
||||
3. Copies all installer scripts and source into dist\
|
||||
Run this from the repo root before copying dist\ to a flash drive.
|
||||
.EXAMPLE
|
||||
.\build-release.ps1
|
||||
.\build-release.ps1 -SkipBuild # Skip docker build (reuse existing image)
|
||||
#>
|
||||
param(
|
||||
[switch]$SkipBuild,
|
||||
[string]$ImageName = "dupfinder",
|
||||
[string]$ImageTag = "latest"
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$RepoRoot = $PSScriptRoot
|
||||
$DistDir = Join-Path $RepoRoot "dist"
|
||||
$ImageFull = "${ImageName}:${ImageTag}"
|
||||
|
||||
function Write-Step([string]$msg) {
|
||||
Write-Host "`n==> $msg" -ForegroundColor Cyan
|
||||
}
|
||||
function Write-OK([string]$msg) {
|
||||
Write-Host " [OK] $msg" -ForegroundColor Green
|
||||
}
|
||||
function Write-Fail([string]$msg) {
|
||||
Write-Host " [!!] $msg" -ForegroundColor Red
|
||||
}
|
||||
|
||||
# ── Check Docker is running ───────────────────────────────────────────────────
|
||||
Write-Step "Checking Docker..."
|
||||
docker info 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Fail "Docker is not running. Start Docker Desktop and try again."
|
||||
exit 1
|
||||
}
|
||||
Write-OK "Docker is running"
|
||||
|
||||
# ── Build image ───────────────────────────────────────────────────────────────
|
||||
if (-not $SkipBuild) {
|
||||
Write-Step "Building Docker image ($ImageFull)..."
|
||||
docker build -t $ImageFull --progress=plain $RepoRoot
|
||||
if ($LASTEXITCODE -ne 0) { Write-Fail "Docker build failed."; exit 1 }
|
||||
Write-OK "Image built: $ImageFull"
|
||||
} else {
|
||||
Write-Step "Skipping build (-SkipBuild). Checking image exists..."
|
||||
$exists = docker images $ImageFull --format "{{.ID}}" 2>$null
|
||||
if (-not $exists) {
|
||||
Write-Fail "Image $ImageFull not found locally. Remove -SkipBuild to build it."
|
||||
exit 1
|
||||
}
|
||||
Write-OK "Image found: $ImageFull"
|
||||
}
|
||||
|
||||
# ── Clean dist\ ──────────────────────────────────────────────────────────────
|
||||
Write-Step "Preparing dist\ directory..."
|
||||
if (Test-Path $DistDir) {
|
||||
Remove-Item $DistDir -Recurse -Force
|
||||
}
|
||||
New-Item -ItemType Directory -Path $DistDir | Out-Null
|
||||
New-Item -ItemType Directory -Path "$DistDir\image" | Out-Null
|
||||
New-Item -ItemType Directory -Path "$DistDir\source" | Out-Null
|
||||
New-Item -ItemType Directory -Path "$DistDir\assets" | Out-Null
|
||||
Write-OK "dist\ ready"
|
||||
|
||||
# ── Save Docker image ─────────────────────────────────────────────────────────
|
||||
Write-Step "Saving Docker image to dist\image\dupfinder.tar (this may take a minute)..."
|
||||
docker save -o "$DistDir\image\dupfinder.tar" $ImageFull
|
||||
if ($LASTEXITCODE -ne 0) { Write-Fail "docker save failed."; exit 1 }
|
||||
$tarSize = [math]::Round((Get-Item "$DistDir\image\dupfinder.tar").Length / 1MB, 1)
|
||||
Write-OK "Image saved (${tarSize} MB)"
|
||||
|
||||
# ── Copy installer scripts ────────────────────────────────────────────────────
|
||||
Write-Step "Copying installer scripts..."
|
||||
Copy-Item "$RepoRoot\installer\install.ps1" "$DistDir\install.ps1"
|
||||
Copy-Item "$RepoRoot\installer\uninstall.ps1" "$DistDir\uninstall.ps1"
|
||||
Copy-Item "$RepoRoot\installer\dupfinder-start-stop.ps1" "$DistDir\dupfinder-start-stop.ps1"
|
||||
Copy-Item "$RepoRoot\docker-compose.yml" "$DistDir\docker-compose.yml"
|
||||
|
||||
# install.bat launcher (no-click PS1 execution for non-technical users)
|
||||
@'
|
||||
@echo off
|
||||
echo Starting DupFinder installer...
|
||||
PowerShell -ExecutionPolicy Bypass -File "%~dp0install.ps1"
|
||||
pause
|
||||
'@ | Set-Content "$DistDir\INSTALL.bat" -Encoding ASCII
|
||||
|
||||
Write-OK "Scripts copied"
|
||||
|
||||
# ── Copy source (fallback build) ──────────────────────────────────────────────
|
||||
Write-Step "Copying source files (offline build fallback)..."
|
||||
$excludeDirs = @('dist', '__pycache__', 'data', '.git', '.claude', 'installer')
|
||||
$excludeFiles = @('*.db', '*.db-shm', '*.db-wal', '*.pyc', '*.pyo')
|
||||
|
||||
Get-ChildItem $RepoRoot -Recurse | Where-Object {
|
||||
$item = $_
|
||||
$skip = $false
|
||||
foreach ($d in $excludeDirs) { if ($item.FullName -match [regex]::Escape($d)) { $skip = $true } }
|
||||
foreach ($f in $excludeFiles) { if ($item.Name -like $f) { $skip = $true } }
|
||||
-not $skip
|
||||
} | ForEach-Object {
|
||||
$rel = $_.FullName.Substring($RepoRoot.Length + 1)
|
||||
$dst = Join-Path "$DistDir\source" $rel
|
||||
if ($_.PSIsContainer) {
|
||||
New-Item -ItemType Directory -Path $dst -Force | Out-Null
|
||||
} else {
|
||||
$dstDir = Split-Path $dst -Parent
|
||||
if (-not (Test-Path $dstDir)) { New-Item -ItemType Directory -Path $dstDir -Force | Out-Null }
|
||||
Copy-Item $_.FullName $dst -Force
|
||||
}
|
||||
}
|
||||
Write-OK "Source copied"
|
||||
|
||||
# ── README for flash drive ────────────────────────────────────────────────────
|
||||
@"
|
||||
DupFinder Installer
|
||||
===================
|
||||
|
||||
Requirements:
|
||||
- Windows 10/11 (64-bit)
|
||||
- Docker Desktop for Windows (if not installed, the installer will guide you)
|
||||
|
||||
To install:
|
||||
1. Right-click INSTALL.bat -> "Run as administrator"
|
||||
OR
|
||||
Open PowerShell as Administrator and run:
|
||||
PowerShell -ExecutionPolicy Bypass -File install.ps1
|
||||
|
||||
2. Follow the prompts (photos path, data path)
|
||||
|
||||
3. A "DupFinder" shortcut will appear on the desktop when done.
|
||||
|
||||
To uninstall:
|
||||
Run uninstall.ps1 as Administrator.
|
||||
|
||||
Built: $(Get-Date -Format 'yyyy-MM-dd HH:mm')
|
||||
Image: $ImageFull
|
||||
"@ | Set-Content "$DistDir\README.txt" -Encoding UTF8
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
$totalMB = [math]::Round((Get-ChildItem $DistDir -Recurse | Measure-Object -Property Length -Sum).Sum / 1MB, 1)
|
||||
Write-Host ""
|
||||
Write-Host "============================================" -ForegroundColor Green
|
||||
Write-Host " Build complete! dist\ is ${totalMB} MB total" -ForegroundColor Green
|
||||
Write-Host " Copy the dist\ folder to your flash drive." -ForegroundColor Green
|
||||
Write-Host "============================================" -ForegroundColor Green
|
||||
Reference in New Issue
Block a user