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
|
||||||
71
installer/dupfinder-start-stop.ps1
Normal file
71
installer/dupfinder-start-stop.ps1
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Start, stop, restart DupFinder, or open it in the browser.
|
||||||
|
.PARAMETER Action
|
||||||
|
start | stop | restart | open (default: open)
|
||||||
|
.EXAMPLE
|
||||||
|
.\dupfinder-start-stop.ps1 -Action open
|
||||||
|
.\dupfinder-start-stop.ps1 -Action stop
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[ValidateSet("start","stop","restart","open")]
|
||||||
|
[string]$Action = "open"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ConfigFile = "C:\ProgramData\DupFinder\dupfinder.conf"
|
||||||
|
|
||||||
|
if (-not (Test-Path $ConfigFile)) {
|
||||||
|
Write-Error "DupFinder is not installed. Run install.ps1 first."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read config
|
||||||
|
$conf = @{}
|
||||||
|
Get-Content $ConfigFile | ForEach-Object {
|
||||||
|
if ($_ -match '^(.+?)=(.+)$') { $conf[$Matches[1]] = $Matches[2] }
|
||||||
|
}
|
||||||
|
|
||||||
|
$ComposeDir = $conf["COMPOSE_DIR"]
|
||||||
|
$AppPort = $conf["APP_PORT"]
|
||||||
|
$ComposeYml = "$ComposeDir\docker-compose.yml"
|
||||||
|
$OverrideYml = "$ComposeDir\docker-compose.override.yml"
|
||||||
|
$Url = "http://localhost:$AppPort"
|
||||||
|
|
||||||
|
function Invoke-Compose([string]$cmd) {
|
||||||
|
& docker compose -f $ComposeYml -f $OverrideYml $cmd.Split(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($Action) {
|
||||||
|
"start" {
|
||||||
|
Write-Host "Starting DupFinder..."
|
||||||
|
Invoke-Compose "up -d --pull never"
|
||||||
|
}
|
||||||
|
"stop" {
|
||||||
|
Write-Host "Stopping DupFinder..."
|
||||||
|
Invoke-Compose "stop"
|
||||||
|
}
|
||||||
|
"restart" {
|
||||||
|
Write-Host "Restarting DupFinder..."
|
||||||
|
Invoke-Compose "restart"
|
||||||
|
}
|
||||||
|
"open" {
|
||||||
|
# Ensure container is running
|
||||||
|
$running = docker ps --filter "name=dup-finder" --format "{{.Names}}" 2>$null
|
||||||
|
if (-not $running) {
|
||||||
|
Write-Host "Starting DupFinder..."
|
||||||
|
Invoke-Compose "up -d --pull never"
|
||||||
|
}
|
||||||
|
# Poll until responsive (up to 15s)
|
||||||
|
$tries = 0
|
||||||
|
while ($tries -lt 15) {
|
||||||
|
try {
|
||||||
|
$r = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 1 -ErrorAction Stop
|
||||||
|
break
|
||||||
|
} catch { }
|
||||||
|
Start-Sleep 1
|
||||||
|
$tries++
|
||||||
|
}
|
||||||
|
Start-Process $Url
|
||||||
|
}
|
||||||
|
}
|
||||||
276
installer/install.ps1
Normal file
276
installer/install.ps1
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Installs DupFinder on this workstation.
|
||||||
|
.DESCRIPTION
|
||||||
|
- Verifies Docker Desktop is installed and running
|
||||||
|
- Loads the pre-built Docker image (or builds from source as fallback)
|
||||||
|
- Prompts for photos library path and data storage path
|
||||||
|
- Writes a docker-compose.override.yml
|
||||||
|
- Starts the container
|
||||||
|
- Creates a desktop shortcut
|
||||||
|
.PARAMETER ForceReload
|
||||||
|
Re-load the Docker image even if it's already present locally.
|
||||||
|
.EXAMPLE
|
||||||
|
PowerShell -ExecutionPolicy Bypass -File install.ps1
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[switch]$ForceReload
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$ScriptDir = $PSScriptRoot
|
||||||
|
$AppDir = "C:\ProgramData\DupFinder"
|
||||||
|
$ConfigFile = "$AppDir\dupfinder.conf"
|
||||||
|
$OverrideYml = "$AppDir\docker-compose.override.yml"
|
||||||
|
$ComposeYml = "$AppDir\docker-compose.yml"
|
||||||
|
$ImageName = "dupfinder:latest"
|
||||||
|
$TarPath = "$ScriptDir\image\dupfinder.tar"
|
||||||
|
$SourcePath = "$ScriptDir\source"
|
||||||
|
$AppPort = 8765
|
||||||
|
|
||||||
|
function Write-Step([string]$msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan }
|
||||||
|
function Write-OK([string]$msg) { Write-Host " OK $msg" -ForegroundColor Green }
|
||||||
|
function Write-Warn([string]$msg) { Write-Host " !! $msg" -ForegroundColor Yellow }
|
||||||
|
function Write-Fail([string]$msg) { Write-Host "`n[FAIL] $msg" -ForegroundColor Red }
|
||||||
|
function Pause-Continue {
|
||||||
|
Write-Host "`nPress Enter to continue..." -NoNewline
|
||||||
|
$null = Read-Host
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 1. Admin check ────────────────────────────────────────────────────────────
|
||||||
|
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||||
|
Write-Fail "This script must be run as Administrator."
|
||||||
|
Write-Host "Right-click install.ps1 and choose 'Run as administrator', or use:"
|
||||||
|
Write-Host " PowerShell -ExecutionPolicy Bypass -File `"$PSCommandPath`""
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " ====================================" -ForegroundColor Magenta
|
||||||
|
Write-Host " DupFinder Installer" -ForegroundColor Magenta
|
||||||
|
Write-Host " ====================================" -ForegroundColor Magenta
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# ── 2. WSL2 check ─────────────────────────────────────────────────────────────
|
||||||
|
Write-Step "Checking WSL2..."
|
||||||
|
$wslOut = & wsl --status 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Warn "WSL2 is not installed or needs updating."
|
||||||
|
Write-Host " Installing WSL2 (requires internet + possible reboot)..."
|
||||||
|
& wsl --install --no-distribution 2>&1 | Out-Null
|
||||||
|
Write-Warn "A reboot may be required. After rebooting, re-run this installer."
|
||||||
|
Pause-Continue
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
Write-OK "WSL2 is present"
|
||||||
|
|
||||||
|
# ── 3. Docker detection ───────────────────────────────────────────────────────
|
||||||
|
Write-Step "Checking Docker Desktop..."
|
||||||
|
|
||||||
|
$dockerExe = $null
|
||||||
|
$dockerCmd = Get-Command docker -ErrorAction SilentlyContinue
|
||||||
|
if ($dockerCmd) {
|
||||||
|
$dockerExe = $dockerCmd.Source
|
||||||
|
} else {
|
||||||
|
# Check known install locations
|
||||||
|
$candidates = @(
|
||||||
|
"$env:ProgramFiles\Docker\Docker\resources\bin\docker.exe",
|
||||||
|
"$env:LOCALAPPDATA\Programs\Docker\Docker\resources\bin\docker.exe"
|
||||||
|
)
|
||||||
|
foreach ($c in $candidates) {
|
||||||
|
if (Test-Path $c) { $dockerExe = $c; break }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $dockerExe) {
|
||||||
|
Write-Warn "Docker Desktop is not installed."
|
||||||
|
|
||||||
|
$bundledInstaller = "$ScriptDir\assets\DockerDesktopInstaller.exe"
|
||||||
|
if (Test-Path $bundledInstaller) {
|
||||||
|
Write-Host " Found bundled Docker Desktop installer. Installing..."
|
||||||
|
Start-Process -Wait $bundledInstaller -ArgumentList "install --quiet --accept-license"
|
||||||
|
Write-Warn "Docker Desktop was installed. A reboot may be required."
|
||||||
|
Write-Host " After rebooting, re-run this installer."
|
||||||
|
Pause-Continue
|
||||||
|
exit 0
|
||||||
|
} else {
|
||||||
|
Write-Host " Opening Docker Desktop download page in your browser..."
|
||||||
|
Start-Process "https://www.docker.com/products/docker-desktop/"
|
||||||
|
Write-Host " Install Docker Desktop, then re-run this script."
|
||||||
|
Pause-Continue
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-OK "Docker executable found: $dockerExe"
|
||||||
|
|
||||||
|
# ── 4. Ensure Docker daemon is running ────────────────────────────────────────
|
||||||
|
Write-Step "Waiting for Docker daemon..."
|
||||||
|
$maxWait = 60
|
||||||
|
$waited = 0
|
||||||
|
while ($waited -lt $maxWait) {
|
||||||
|
$info = docker info 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { break }
|
||||||
|
if ($waited -eq 0) {
|
||||||
|
# Try to start Docker Desktop
|
||||||
|
$desktopExe = "$env:ProgramFiles\Docker\Docker\Docker Desktop.exe"
|
||||||
|
if (Test-Path $desktopExe) {
|
||||||
|
Write-Host " Starting Docker Desktop..."
|
||||||
|
Start-Process $desktopExe
|
||||||
|
}
|
||||||
|
Write-Host " Waiting for Docker to become ready (up to ${maxWait}s)..."
|
||||||
|
Write-Host " If a Docker Desktop setup window appeared, please complete it."
|
||||||
|
}
|
||||||
|
Start-Sleep 3
|
||||||
|
$waited += 3
|
||||||
|
Write-Host " ... ${waited}s" -NoNewline
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
docker info 2>&1 | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Fail "Docker did not start within ${maxWait}s. Please start Docker Desktop manually and re-run."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-OK "Docker daemon is running"
|
||||||
|
|
||||||
|
# ── 5. docker compose V2 check ────────────────────────────────────────────────
|
||||||
|
docker compose version 2>&1 | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Fail "docker compose (V2 plugin) not found. Please update Docker Desktop to 4.0+ and re-run."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-OK "docker compose V2 available"
|
||||||
|
|
||||||
|
# ── 6. Load / build image ─────────────────────────────────────────────────────
|
||||||
|
Write-Step "Preparing DupFinder Docker image..."
|
||||||
|
$existingImage = (docker images $ImageName --format "{{.ID}}" 2>$null)
|
||||||
|
|
||||||
|
if ($existingImage -and -not $ForceReload) {
|
||||||
|
Write-OK "Image already loaded (use -ForceReload to replace it)"
|
||||||
|
} elseif (Test-Path $TarPath) {
|
||||||
|
Write-Host " Loading image from $TarPath ..."
|
||||||
|
docker load -i $TarPath
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Fail "docker load failed."; exit 1 }
|
||||||
|
Write-OK "Image loaded from tar"
|
||||||
|
} elseif (Test-Path "$SourcePath\Dockerfile") {
|
||||||
|
Write-Warn "No pre-built image found. Building from source (requires internet)..."
|
||||||
|
docker build -t $ImageName $SourcePath
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Fail "docker build failed."; exit 1 }
|
||||||
|
Write-OK "Image built from source"
|
||||||
|
} else {
|
||||||
|
Write-Fail "No image tar and no source Dockerfile found. Bundle may be incomplete."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 7. Collect paths from user ────────────────────────────────────────────────
|
||||||
|
Write-Step "Configuration"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Load existing config as defaults if re-running
|
||||||
|
$defaultPhotos = "C:\Photos"
|
||||||
|
$defaultData = "C:\ProgramData\DupFinder\data"
|
||||||
|
if (Test-Path $ConfigFile) {
|
||||||
|
Get-Content $ConfigFile | ForEach-Object {
|
||||||
|
if ($_ -match '^PHOTOS_PATH=(.+)$') { $defaultPhotos = $Matches[1] }
|
||||||
|
if ($_ -match '^DATA_PATH=(.+)$') { $defaultData = $Matches[1] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Photos path
|
||||||
|
do {
|
||||||
|
Write-Host " Photos library path (read-only mount):"
|
||||||
|
Write-Host " Default: $defaultPhotos"
|
||||||
|
$input = (Read-Host " Path").Trim().Trim('"')
|
||||||
|
if ([string]::IsNullOrWhiteSpace($input)) { $input = $defaultPhotos }
|
||||||
|
$PhotosPath = $input
|
||||||
|
if (-not (Test-Path $PhotosPath -PathType Container)) {
|
||||||
|
Write-Warn "Path not found: $PhotosPath (try again)"
|
||||||
|
}
|
||||||
|
} while (-not (Test-Path $PhotosPath -PathType Container))
|
||||||
|
Write-OK "Photos: $PhotosPath"
|
||||||
|
|
||||||
|
# Data path
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Database / data storage path:"
|
||||||
|
Write-Host " Default: $defaultData"
|
||||||
|
$input = (Read-Host " Path").Trim().Trim('"')
|
||||||
|
if ([string]::IsNullOrWhiteSpace($input)) { $input = $defaultData }
|
||||||
|
$DataPath = $input
|
||||||
|
New-Item -ItemType Directory -Force -Path $DataPath | Out-Null
|
||||||
|
Write-OK "Data: $DataPath"
|
||||||
|
|
||||||
|
# ── 8. Port conflict check ────────────────────────────────────────────────────
|
||||||
|
$portInUse = Test-NetConnection localhost -Port $AppPort `
|
||||||
|
-InformationLevel Quiet -WarningAction SilentlyContinue 2>$null
|
||||||
|
if ($portInUse) {
|
||||||
|
Write-Warn "Port $AppPort is already in use. DupFinder may already be running."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 9. Write config and compose override ─────────────────────────────────────
|
||||||
|
Write-Step "Writing configuration..."
|
||||||
|
New-Item -ItemType Directory -Force -Path $AppDir | Out-Null
|
||||||
|
|
||||||
|
# Docker requires forward slashes
|
||||||
|
$PhotosDocker = $PhotosPath -replace '\\', '/' -replace '//', '/'
|
||||||
|
$DataDocker = $DataPath -replace '\\', '/' -replace '//', '/'
|
||||||
|
|
||||||
|
@"
|
||||||
|
PHOTOS_PATH=$PhotosPath
|
||||||
|
DATA_PATH=$DataPath
|
||||||
|
APP_PORT=$AppPort
|
||||||
|
COMPOSE_DIR=$AppDir
|
||||||
|
"@ | Set-Content $ConfigFile -Encoding UTF8
|
||||||
|
|
||||||
|
@"
|
||||||
|
services:
|
||||||
|
dup-finder:
|
||||||
|
ports:
|
||||||
|
- "${AppPort}:8000"
|
||||||
|
volumes:
|
||||||
|
- "$PhotosDocker:/photos:ro"
|
||||||
|
- "$DataDocker:/data"
|
||||||
|
"@ | Set-Content $OverrideYml -Encoding UTF8
|
||||||
|
|
||||||
|
# Copy base compose file
|
||||||
|
Copy-Item "$ScriptDir\docker-compose.yml" $ComposeYml -Force
|
||||||
|
|
||||||
|
# Copy start-stop helper
|
||||||
|
Copy-Item "$ScriptDir\dupfinder-start-stop.ps1" "$AppDir\dupfinder-start-stop.ps1" -Force
|
||||||
|
|
||||||
|
Write-OK "Config written to $AppDir"
|
||||||
|
|
||||||
|
# ── 10. Start container ───────────────────────────────────────────────────────
|
||||||
|
Write-Step "Starting DupFinder container..."
|
||||||
|
docker compose -f $ComposeYml -f $OverrideYml up -d --pull never
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Fail "docker compose up failed."; exit 1 }
|
||||||
|
Write-OK "Container started"
|
||||||
|
|
||||||
|
# ── 11. Create desktop shortcut ───────────────────────────────────────────────
|
||||||
|
Write-Step "Creating desktop shortcut..."
|
||||||
|
$ShortcutPath = "$env:PUBLIC\Desktop\DupFinder.lnk"
|
||||||
|
$WshShell = New-Object -ComObject WScript.Shell
|
||||||
|
$Shortcut = $WshShell.CreateShortcut($ShortcutPath)
|
||||||
|
$Shortcut.TargetPath = "powershell.exe"
|
||||||
|
$Shortcut.Arguments = "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$AppDir\dupfinder-start-stop.ps1`" -Action open"
|
||||||
|
$Shortcut.Description = "Open DupFinder Duplicate Photo Scanner"
|
||||||
|
$Shortcut.WindowStyle = 7 # Minimized — hides the PS window
|
||||||
|
$Shortcut.Save()
|
||||||
|
Write-OK "Shortcut created: $ShortcutPath"
|
||||||
|
|
||||||
|
# ── 12. Done ──────────────────────────────────────────────────────────────────
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " ============================================" -ForegroundColor Green
|
||||||
|
Write-Host " DupFinder installed successfully!" -ForegroundColor Green
|
||||||
|
Write-Host " Open: http://localhost:$AppPort" -ForegroundColor Green
|
||||||
|
Write-Host " Or double-click DupFinder on your desktop." -ForegroundColor Green
|
||||||
|
Write-Host " ============================================" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Open browser
|
||||||
|
$open = Read-Host "Open DupFinder in browser now? (Y/n)"
|
||||||
|
if ($open -ne 'n' -and $open -ne 'N') {
|
||||||
|
Start-Process "http://localhost:$AppPort"
|
||||||
|
}
|
||||||
76
installer/uninstall.ps1
Normal file
76
installer/uninstall.ps1
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Uninstalls DupFinder from this workstation.
|
||||||
|
#>
|
||||||
|
|
||||||
|
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||||
|
Write-Error "This script must be run as Administrator."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$ConfigFile = "C:\ProgramData\DupFinder\dupfinder.conf"
|
||||||
|
$AppDir = "C:\ProgramData\DupFinder"
|
||||||
|
$ShortcutPath = "$env:PUBLIC\Desktop\DupFinder.lnk"
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " DupFinder Uninstaller" -ForegroundColor Magenta
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
if (-not (Test-Path $ConfigFile)) {
|
||||||
|
Write-Warning "DupFinder config not found. It may not be installed, or was already removed."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read config
|
||||||
|
$conf = @{}
|
||||||
|
Get-Content $ConfigFile | ForEach-Object {
|
||||||
|
if ($_ -match '^(.+?)=(.+)$') { $conf[$Matches[1]] = $Matches[2] }
|
||||||
|
}
|
||||||
|
$ComposeDir = $conf["COMPOSE_DIR"]
|
||||||
|
$DataPath = $conf["DATA_PATH"]
|
||||||
|
$ComposeYml = "$ComposeDir\docker-compose.yml"
|
||||||
|
$OverrideYml = "$ComposeDir\docker-compose.override.yml"
|
||||||
|
|
||||||
|
# ── Stop and remove container ─────────────────────────────────────────────────
|
||||||
|
Write-Host "Stopping and removing container..."
|
||||||
|
docker compose -f $ComposeYml -f $OverrideYml down 2>$null
|
||||||
|
Write-Host " Done."
|
||||||
|
|
||||||
|
# ── Remove Docker image? ──────────────────────────────────────────────────────
|
||||||
|
$rmImage = Read-Host "Remove the DupFinder Docker image? Frees ~300-600 MB (Y/n)"
|
||||||
|
if ($rmImage -ne 'n' -and $rmImage -ne 'N') {
|
||||||
|
docker rmi dupfinder:latest 2>$null
|
||||||
|
Write-Host " Image removed."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Remove data directory? ────────────────────────────────────────────────────
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Data directory: $DataPath"
|
||||||
|
Write-Host "This contains the scan database and all decisions."
|
||||||
|
$rmData = Read-Host "Remove data directory? This CANNOT be undone. (y/N)"
|
||||||
|
if ($rmData -eq 'y' -or $rmData -eq 'Y') {
|
||||||
|
if (Test-Path $DataPath) {
|
||||||
|
Remove-Item $DataPath -Recurse -Force
|
||||||
|
Write-Host " Data directory removed."
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Host " Data directory kept at: $DataPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Remove shortcut ───────────────────────────────────────────────────────────
|
||||||
|
if (Test-Path $ShortcutPath) {
|
||||||
|
Remove-Item $ShortcutPath -Force
|
||||||
|
Write-Host "Desktop shortcut removed."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Remove app directory ──────────────────────────────────────────────────────
|
||||||
|
if (Test-Path $AppDir) {
|
||||||
|
Remove-Item $AppDir -Recurse -Force
|
||||||
|
Write-Host "App directory removed: $AppDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " DupFinder has been uninstalled." -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
Reference in New Issue
Block a user