From e7d6e5f5bdf8d61d9dcabb2685ee41a7a72fa9e5 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:13:59 +0530 Subject: [PATCH] feat: add Windows installation script and update README for Linux/MacOS users --- README.md | 8 + docker/scripts/install.ps1 | 335 ++++++++++++++++++++++++++++ docker/scripts/migrate-database.ps1 | 332 +++++++++++++++++++++++++++ 3 files changed, 675 insertions(+) create mode 100644 docker/scripts/install.ps1 create mode 100644 docker/scripts/migrate-database.ps1 diff --git a/README.md b/README.md index 7641aa202..d27ca0273 100644 --- a/README.md +++ b/README.md @@ -86,10 +86,18 @@ Run SurfSense on your own infrastructure for full data control and privacy. > [!NOTE] > Windows users: install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) first and run the command below in the Ubuntu terminal. +#### For Linux/MacOS users: + ```bash curl -fsSL https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.sh | bash ``` +#### For Windows users: + +```bash +irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex +``` + The install script sets up [Watchtower](https://github.com/nicholas-fedor/watchtower) automatically for daily auto-updates. To skip it, add the `--no-watchtower` flag. For Docker Compose, manual installation, and other deployment options, see the [docs](https://www.surfsense.com/docs/). diff --git a/docker/scripts/install.ps1 b/docker/scripts/install.ps1 new file mode 100644 index 000000000..2be8e2132 --- /dev/null +++ b/docker/scripts/install.ps1 @@ -0,0 +1,335 @@ +# ============================================================================= +# SurfSense — One-line Install Script (Windows / PowerShell) +# +# +# Usage: irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex +# +# To pass flags, save and run locally: +# .\install.ps1 -NoWatchtower +# .\install.ps1 -WatchtowerInterval 3600 +# +# Handles two cases automatically: +# 1. Fresh install — no prior SurfSense data detected +# 2. Migration from the legacy all-in-one container (surfsense-data volume) +# Downloads and runs migrate-database.sh --yes, then restores the dump +# into the new PostgreSQL 17 stack. The user runs one command for both. +# ============================================================================= + +param( + [switch]$NoWatchtower, + [int]$WatchtowerInterval = 86400 +) + +$ErrorActionPreference = 'Stop' + +# ── Configuration ─────────────────────────────────────────────────────────── + +$RepoRaw = "https://raw.githubusercontent.com/MODSetter/SurfSense/dev" +$InstallDir = ".\surfsense" +$OldVolume = "surfsense-data" +$DumpFile = ".\surfsense_migration_backup.sql" +$KeyFile = ".\surfsense_migration_secret.key" +$MigrationDoneFile = "$InstallDir\.migration_done" +$MigrationMode = $false +$SetupWatchtower = -not $NoWatchtower +$WatchtowerContainer = "watchtower" + +# ── Output helpers ────────────────────────────────────────────────────────── + +function Write-Info { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Cyan -NoNewline; Write-Host $Msg } +function Write-Ok { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Green -NoNewline; Write-Host $Msg } +function Write-Warn { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Yellow -NoNewline; Write-Host $Msg } +function Write-Step { param([string]$Msg) Write-Host "`n-- $Msg" -ForegroundColor Cyan } +function Write-Err { param([string]$Msg) Write-Host "[SurfSense] ERROR: $Msg" -ForegroundColor Red; exit 1 } + +# ── Pre-flight checks ────────────────────────────────────────────────────── + +Write-Step "Checking prerequisites" + +if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { + Write-Err "Docker is not installed. Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/" +} +Write-Ok "Docker found." + +docker info *>$null +if ($LASTEXITCODE -ne 0) { + Write-Err "Docker daemon is not running. Please start Docker Desktop and try again." +} +Write-Ok "Docker daemon is running." + +docker compose version *>$null +if ($LASTEXITCODE -ne 0) { + Write-Err "Docker Compose is not available. It should be bundled with Docker Desktop." +} +Write-Ok "Docker Compose found." + +# ── Wait-for-postgres helper ──────────────────────────────────────────────── + +function Wait-ForPostgres { + param([string]$DbUser) + $maxAttempts = 45 + $attempt = 0 + + Write-Info "Waiting for PostgreSQL to accept connections..." + do { + $attempt++ + if ($attempt -ge $maxAttempts) { + Write-Err "PostgreSQL did not become ready after $($maxAttempts * 2) seconds.`nCheck logs: cd $InstallDir; docker compose logs db" + } + Start-Sleep -Seconds 2 + Push-Location $InstallDir + docker compose exec -T db pg_isready -U $DbUser -q 2>$null + $ready = $LASTEXITCODE -eq 0 + Pop-Location + } while (-not $ready) + + Write-Ok "PostgreSQL is ready." +} + +# ── Download files ────────────────────────────────────────────────────────── + +Write-Step "Downloading SurfSense files" +Write-Info "Installation directory: $InstallDir" + +New-Item -ItemType Directory -Path "$InstallDir\scripts" -Force | Out-Null + +$Files = @( + @{ Src = "docker/docker-compose.yml"; Dest = "docker-compose.yml" } + @{ Src = "docker/.env.example"; Dest = ".env.example" } + @{ Src = "docker/postgresql.conf"; Dest = "postgresql.conf" } + @{ Src = "docker/scripts/init-electric-user.sh"; Dest = "scripts/init-electric-user.sh" } + @{ Src = "docker/scripts/migrate-database.ps1"; Dest = "scripts/migrate-database.ps1" } +) + +foreach ($f in $Files) { + $destPath = Join-Path $InstallDir $f.Dest + Write-Info "Downloading $($f.Dest)..." + try { + Invoke-WebRequest -Uri "$RepoRaw/$($f.Src)" -OutFile $destPath -UseBasicParsing + } catch { + Write-Err "Failed to download $($f.Dest). Check your internet connection and try again." + } +} + +Write-Ok "All files downloaded to $InstallDir/" + +# ── Legacy all-in-one detection ───────────────────────────────────────────── + +$volumeList = docker volume ls --format '{{.Name}}' 2>$null +if (($volumeList -split "`n") -contains $OldVolume -and -not (Test-Path $MigrationDoneFile)) { + $MigrationMode = $true + + if (Test-Path $DumpFile) { + Write-Step "Migration mode - using existing dump (skipping extraction)" + Write-Info "Found existing dump: $DumpFile" + Write-Info "Skipping data extraction - proceeding directly to restore." + Write-Info "To force a fresh extraction, remove the dump first: Remove-Item $DumpFile" + } else { + Write-Step "Migration mode - legacy all-in-one container detected" + Write-Warn "Volume '$OldVolume' found. Your data will be migrated automatically." + Write-Warn "PostgreSQL is being upgraded from version 14 to 17." + Write-Warn "Your original data will NOT be deleted." + Write-Host "" + Write-Info "Running data extraction (migrate-database.ps1 -Yes)..." + Write-Info "Full extraction log: ./surfsense-migration.log" + Write-Host "" + + $migrateScript = Join-Path $InstallDir "scripts/migrate-database.ps1" + & $migrateScript -Yes + if ($LASTEXITCODE -ne 0) { + Write-Err "Data extraction failed. See ./surfsense-migration.log for details.`nYou can also run migrate-database.ps1 manually with custom flags." + } + + Write-Host "" + Write-Ok "Data extraction complete. Proceeding with installation and restore." + } +} + +# ── Set up .env ───────────────────────────────────────────────────────────── + +Write-Step "Configuring environment" + +$envPath = Join-Path $InstallDir ".env" +$envExamplePath = Join-Path $InstallDir ".env.example" + +if (-not (Test-Path $envPath)) { + Copy-Item $envExamplePath $envPath + + if ($MigrationMode -and (Test-Path $KeyFile)) { + $SecretKey = (Get-Content $KeyFile -Raw).Trim() + Write-Ok "Using SECRET_KEY recovered from legacy container." + } else { + $bytes = New-Object byte[] 32 + $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new() + $rng.GetBytes($bytes) + $rng.Dispose() + $SecretKey = [Convert]::ToBase64String($bytes) + Write-Ok "Generated new random SECRET_KEY." + } + + $content = Get-Content $envPath -Raw + $content = $content -replace 'SECRET_KEY=replace_me_with_a_random_string', "SECRET_KEY=$SecretKey" + Set-Content -Path $envPath -Value $content -NoNewline + + Write-Info "Created $envPath" +} else { + Write-Warn ".env already exists - keeping your existing configuration." +} + +# ── Start containers ──────────────────────────────────────────────────────── + +if ($MigrationMode) { + $envContent = Get-Content $envPath + $DbUser = ($envContent | Select-String '^DB_USER=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1 + $DbPass = ($envContent | Select-String '^DB_PASSWORD=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1 + $DbName = ($envContent | Select-String '^DB_NAME=' | ForEach-Object { ($_ -split '=',2)[1].Trim('"') }) | Select-Object -First 1 + if (-not $DbUser) { $DbUser = "surfsense" } + if (-not $DbPass) { $DbPass = "surfsense" } + if (-not $DbName) { $DbName = "surfsense" } + + Write-Step "Starting PostgreSQL 17" + Push-Location $InstallDir + docker compose up -d db + Pop-Location + Wait-ForPostgres -DbUser $DbUser + + Write-Step "Restoring database" + if (-not (Test-Path $DumpFile)) { + Write-Err "Dump file '$DumpFile' not found. The migration script may have failed." + } + Write-Info "Restoring dump into PostgreSQL 17 - this may take a while for large databases..." + + $restoreErrFile = Join-Path $env:TEMP "surfsense_restore_err.log" + Push-Location $InstallDir + Get-Content $DumpFile | docker compose exec -T -e "PGPASSWORD=$DbPass" db psql -U $DbUser -d $DbName 2>$restoreErrFile | Out-Null + Pop-Location + + $fatalErrors = @() + if (Test-Path $restoreErrFile) { + $fatalErrors = Get-Content $restoreErrFile | + Where-Object { $_ -match '^ERROR:' } | + Where-Object { $_ -notmatch 'already exists' } | + Where-Object { $_ -notmatch 'multiple primary keys' } + } + + if ($fatalErrors.Count -gt 0) { + Write-Warn "Restore completed with errors (may be harmless pg_dump header noise):" + $fatalErrors | ForEach-Object { Write-Host $_ } + Write-Warn "If SurfSense behaves incorrectly, inspect manually." + } else { + Write-Ok "Database restored with no fatal errors." + } + + # Smoke test + Push-Location $InstallDir + $tableCount = (docker compose exec -T -e "PGPASSWORD=$DbPass" db psql -U $DbUser -d $DbName -t -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>$null).Trim() + Pop-Location + + if (-not $tableCount -or $tableCount -eq "0") { + Write-Warn "Smoke test: no tables found after restore." + Write-Warn "The restore may have failed silently. Check: cd $InstallDir; docker compose logs db" + } else { + Write-Ok "Smoke test passed: $tableCount table(s) restored successfully." + New-Item -Path $MigrationDoneFile -ItemType File -Force | Out-Null + } + + Write-Step "Starting all SurfSense services" + Push-Location $InstallDir + docker compose up -d + Pop-Location + Write-Ok "All services started." + + Remove-Item $KeyFile -ErrorAction SilentlyContinue + +} else { + Write-Step "Starting SurfSense" + Push-Location $InstallDir + docker compose up -d + Pop-Location + Write-Ok "All services started." +} + +# ── Watchtower (auto-update) ──────────────────────────────────────────────── + +if ($SetupWatchtower) { + $wtHours = [math]::Floor($WatchtowerInterval / 3600) + Write-Step "Setting up Watchtower (auto-updates every ${wtHours}h)" + + $wtState = docker inspect -f '{{.State.Running}}' $WatchtowerContainer 2>$null + if ($LASTEXITCODE -ne 0) { $wtState = "missing" } + + if ($wtState -eq "true") { + Write-Ok "Watchtower is already running - skipping." + } else { + if ($wtState -ne "missing") { + Write-Info "Removing stopped Watchtower container..." + docker rm -f $WatchtowerContainer *>$null + } + docker run -d ` + --name $WatchtowerContainer ` + --restart unless-stopped ` + -v /var/run/docker.sock:/var/run/docker.sock ` + nickfedor/watchtower ` + --label-enable ` + --interval $WatchtowerInterval *>$null + + if ($LASTEXITCODE -eq 0) { + Write-Ok "Watchtower started - labeled SurfSense containers will auto-update." + } else { + Write-Warn "Could not start Watchtower. You can set it up manually or use: docker compose pull; docker compose up -d" + } + } +} else { + Write-Info "Skipping Watchtower setup (-NoWatchtower flag)." +} + +# ── Done ──────────────────────────────────────────────────────────────────── + +Write-Host "" +Write-Host @" + + + .d8888b. .d888 .d8888b. +d88P Y88b d88P" d88P Y88b +Y88b. 888 Y88b. + "Y888b. 888 888 888d888 888888 "Y888b. .d88b. 88888b. .d8888b .d88b. + "Y88b. 888 888 888P" 888 "Y88b. d8P Y8b 888 "88b 88K d8P Y8b + "888 888 888 888 888 "888 88888888 888 888 "Y8888b. 88888888 +Y88b d88P Y88b 888 888 888 Y88b d88P Y8b. 888 888 X88 Y8b. + "Y8888P" "Y88888 888 888 "Y8888P" "Y8888 888 888 88888P' "Y8888 + + +"@ -ForegroundColor White + +$versionDisplay = if ($env:SURFSENSE_VERSION -and $env:SURFSENSE_VERSION -ne "latest") { "v$env:SURFSENSE_VERSION" } else { "latest" } +Write-Host " Your personal AI-powered search engine [$versionDisplay]" -ForegroundColor Yellow +Write-Host ("=" * 62) -ForegroundColor Cyan +Write-Host "" + +Write-Info " Frontend: http://localhost:3000" +Write-Info " Backend: http://localhost:8000" +Write-Info " API Docs: http://localhost:8000/docs" +Write-Info "" +Write-Info " Config: $InstallDir\.env" +Write-Info " Logs: cd $InstallDir; docker compose logs -f" +Write-Info " Stop: cd $InstallDir; docker compose down" +Write-Info " Update: cd $InstallDir; docker compose pull; docker compose up -d" +Write-Info "" + +if ($SetupWatchtower) { + Write-Info " Watchtower: auto-updates every ${wtHours}h (stop: docker rm -f $WatchtowerContainer)" +} else { + Write-Warn " Watchtower skipped. For auto-updates, re-run without -NoWatchtower." +} +Write-Info "" + +if ($MigrationMode) { + Write-Warn " Migration complete! Open frontend and verify your data." + Write-Warn " Once verified, clean up the legacy volume and migration files:" + Write-Warn " docker volume rm $OldVolume" + Write-Warn " Remove-Item $DumpFile" + Write-Warn " Remove-Item $MigrationDoneFile" +} else { + Write-Warn " First startup may take a few minutes while images are pulled." + Write-Warn " Edit $InstallDir\.env to configure API keys, OAuth, etc." +} diff --git a/docker/scripts/migrate-database.ps1 b/docker/scripts/migrate-database.ps1 new file mode 100644 index 000000000..c616a3f77 --- /dev/null +++ b/docker/scripts/migrate-database.ps1 @@ -0,0 +1,332 @@ +# ============================================================================= +# SurfSense — Database Migration Script (Windows / PowerShell) +# +# Extracts data from the legacy all-in-one surfsense-data volume (PostgreSQL 14) +# and saves it as a SQL dump + SECRET_KEY file ready for install.ps1 to restore. +# +# Usage: +# .\migrate-database.ps1 [options] +# +# Options: +# -DbUser USER Old PostgreSQL username (default: surfsense) +# -DbPassword PASS Old PostgreSQL password (default: surfsense) +# -DbName NAME Old PostgreSQL database (default: surfsense) +# -Yes Skip all confirmation prompts +# +# Prerequisites: +# - Docker Desktop installed and running +# - The legacy surfsense-data volume must exist +# - ~500 MB free disk space for the dump file +# +# What this script does: +# 1. Stops any container using surfsense-data (to prevent corruption) +# 2. Starts a temporary PG14 container against the old volume +# 3. Dumps the database to .\surfsense_migration_backup.sql +# 4. Recovers the SECRET_KEY to .\surfsense_migration_secret.key +# 5. Exits — leaving installation to install.ps1 +# +# What this script does NOT do: +# - Delete the original surfsense-data volume (do this manually after verifying) +# - Install the new SurfSense stack (install.ps1 handles that automatically) +# +# Note: +# install.ps1 downloads and runs this script automatically when it detects the +# legacy surfsense-data volume. You only need to run this script manually if +# you have custom database credentials (-DbUser / -DbPassword / -DbName) +# or if the automatic migration inside install.ps1 fails at the extraction step. +# ============================================================================= + +param( + [string]$DbUser = "surfsense", + [string]$DbPassword = "surfsense", + [string]$DbName = "surfsense", + [switch]$Yes +) + +$ErrorActionPreference = 'Stop' + +# ── Constants ──────────────────────────────────────────────────────────────── + +$OldVolume = "surfsense-data" +$TempContainer = "surfsense-pg14-migration" +$DumpFile = ".\surfsense_migration_backup.sql" +$KeyFile = ".\surfsense_migration_secret.key" +$PG14Image = "pgvector/pgvector:pg14" +$LogFile = ".\surfsense-migration.log" + +# ── Output helpers ─────────────────────────────────────────────────────────── + +function Write-Info { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Cyan -NoNewline; Write-Host $Msg } +function Write-Ok { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Green -NoNewline; Write-Host $Msg } +function Write-Warn { param([string]$Msg) Write-Host "[SurfSense] " -ForegroundColor Yellow -NoNewline; Write-Host $Msg } +function Write-Step { param([string]$Step, [string]$Msg) Write-Host "`n-- Step ${Step}: $Msg" -ForegroundColor Cyan } +function Write-Err { param([string]$Msg) Write-Host "[SurfSense] ERROR: $Msg" -ForegroundColor Red; exit 1 } + +function Log { param([string]$Msg) Add-Content -Path $LogFile -Value $Msg } + +function Confirm-Action { + param([string]$Prompt) + if ($Yes) { return } + $reply = Read-Host "[SurfSense] $Prompt [y/N]" + if ($reply -notmatch '^[Yy]$') { + Write-Warn "Aborted." + exit 0 + } +} + +# ── Cleanup helper ─────────────────────────────────────────────────────────── + +function Remove-TempContainer { + $containers = docker ps -a --format '{{.Names}}' 2>$null + if ($containers -and ($containers -split "`n") -contains $TempContainer) { + Write-Info "Cleaning up temporary container '$TempContainer'..." + docker stop $TempContainer *>$null + docker rm $TempContainer *>$null + } +} + +# Register cleanup on script exit +Register-EngineEvent PowerShell.Exiting -Action { + $containers = docker ps -a --format '{{.Names}}' 2>$null + if ($containers -and ($containers -split "`n") -contains "surfsense-pg14-migration") { + docker stop "surfsense-pg14-migration" *>$null + docker rm "surfsense-pg14-migration" *>$null + } +} | Out-Null + +# ── Wait-for-postgres helper ──────────────────────────────────────────────── + +function Wait-ForPostgres { + param( + [string]$Container, + [string]$User, + [string]$Label = "PostgreSQL" + ) + $maxAttempts = 45 + $attempt = 0 + + Write-Info "Waiting for $Label to accept connections..." + do { + $attempt++ + if ($attempt -ge $maxAttempts) { + Write-Err "$Label did not become ready after $($maxAttempts * 2) seconds. Check: docker logs $Container" + } + Start-Sleep -Seconds 2 + docker exec $Container pg_isready -U $User -q 2>$null + } while ($LASTEXITCODE -ne 0) + + Write-Ok "$Label is ready." +} + +Write-Info "Migrating data from legacy database (PostgreSQL 14 -> 17)" +"Migration started at $(Get-Date)" | Out-File $LogFile + +# ── Step 0: Pre-flight checks ─────────────────────────────────────────────── + +Write-Step "0" "Pre-flight checks" + +if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { + Write-Err "Docker is not installed. Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/" +} + +docker info *>$null +if ($LASTEXITCODE -ne 0) { + Write-Err "Docker daemon is not running. Please start Docker Desktop and try again." +} + +$volumeList = docker volume ls --format '{{.Name}}' 2>$null +if (-not (($volumeList -split "`n") -contains $OldVolume)) { + Write-Err "Legacy volume '$OldVolume' not found. Are you sure you ran the old all-in-one SurfSense container?" +} +Write-Ok "Found legacy volume: $OldVolume" + +$oldContainer = (docker ps --filter "volume=$OldVolume" --format '{{.Names}}' 2>$null | Select-Object -First 1) +if ($oldContainer) { + Write-Warn "Container '$oldContainer' is running and using the '$OldVolume' volume." + Write-Warn "It must be stopped before migration to prevent data file corruption." + Confirm-Action "Stop '$oldContainer' now and proceed with data extraction?" + docker stop $oldContainer *>$null + if ($LASTEXITCODE -ne 0) { + Write-Err "Failed to stop '$oldContainer'. Try: docker stop $oldContainer" + } + Write-Ok "Container '$oldContainer' stopped." +} + +if (Test-Path $DumpFile) { + Write-Warn "Dump file '$DumpFile' already exists." + Write-Warn "If a previous extraction succeeded, just run install.ps1 now." + Write-Warn "To re-extract, remove the file first: Remove-Item $DumpFile" + Write-Err "Aborting to avoid overwriting an existing dump." +} + +$staleContainers = docker ps -a --format '{{.Names}}' 2>$null +if ($staleContainers -and ($staleContainers -split "`n") -contains $TempContainer) { + Write-Warn "Stale migration container '$TempContainer' found - removing it." + docker stop $TempContainer *>$null + docker rm $TempContainer *>$null +} + +$drive = (Get-Item .).PSDrive +$freeMB = [math]::Floor($drive.Free / 1MB) +if ($freeMB -lt 500) { + Write-Warn "Low disk space: $freeMB MB free. At least 500 MB recommended for the dump." + Confirm-Action "Continue anyway?" +} else { + Write-Ok "Disk space: $freeMB MB free." +} + +Write-Ok "All pre-flight checks passed." + +# ── Confirmation prompt ────────────────────────────────────────────────────── + +Write-Host "" +Write-Host "Extraction plan:" -ForegroundColor White +Write-Host " Source volume : " -NoNewline; Write-Host "$OldVolume" -ForegroundColor Yellow -NoNewline; Write-Host " (PG14 data at /data/postgres)" +Write-Host " Old credentials : user=" -NoNewline; Write-Host "$DbUser" -ForegroundColor Yellow -NoNewline; Write-Host " db=" -NoNewline; Write-Host "$DbName" -ForegroundColor Yellow +Write-Host " Dump saved to : " -NoNewline; Write-Host "$DumpFile" -ForegroundColor Yellow +Write-Host " SECRET_KEY to : " -NoNewline; Write-Host "$KeyFile" -ForegroundColor Yellow +Write-Host " Log file : " -NoNewline; Write-Host "$LogFile" -ForegroundColor Yellow +Write-Host "" +Confirm-Action "Start data extraction? (Your original data will not be deleted or modified.)" + +# ── Step 1: Start temporary PostgreSQL 14 container ────────────────────────── + +Write-Step "1" "Starting temporary PostgreSQL 14 container" + +Write-Info "Pulling $PG14Image..." +docker pull $PG14Image *>$null +if ($LASTEXITCODE -ne 0) { + Write-Warn "Could not pull $PG14Image - using cached image if available." +} + +$dataUid = docker run --rm -v "${OldVolume}:/data" alpine stat -c '%u' /data/postgres 2>$null +if (-not $dataUid -or $dataUid -eq "0") { + Write-Warn "Could not detect data directory UID - falling back to default (may chown files)." + $userFlag = @() +} else { + Write-Info "Data directory owned by UID $dataUid - starting temp container as that user." + $userFlag = @("--user", $dataUid) +} + +$dockerRunArgs = @( + "run", "-d", + "--name", $TempContainer, + "-v", "${OldVolume}:/data", + "-e", "PGDATA=/data/postgres", + "-e", "POSTGRES_USER=$DbUser", + "-e", "POSTGRES_PASSWORD=$DbPassword", + "-e", "POSTGRES_DB=$DbName" +) + $userFlag + @($PG14Image) + +docker @dockerRunArgs *>$null +if ($LASTEXITCODE -ne 0) { + Write-Err "Failed to start temporary PostgreSQL 14 container." +} + +Write-Ok "Temporary container '$TempContainer' started." +Wait-ForPostgres -Container $TempContainer -User $DbUser -Label "PostgreSQL 14" + +# ── Step 2: Dump the database ──────────────────────────────────────────────── + +Write-Step "2" "Dumping PostgreSQL 14 database" + +Write-Info "Running pg_dump - this may take a while for large databases..." + +$pgDumpErrFile = Join-Path $env:TEMP "surfsense_pgdump_err.log" +docker exec -e "PGPASSWORD=$DbPassword" $TempContainer pg_dump -U $DbUser --no-password $DbName > $DumpFile 2>$pgDumpErrFile +if ($LASTEXITCODE -ne 0) { + if (Test-Path $pgDumpErrFile) { Get-Content $pgDumpErrFile | Write-Host -ForegroundColor Red } + Remove-TempContainer + Write-Err "pg_dump failed. See above for details." +} + +if (-not (Test-Path $DumpFile) -or (Get-Item $DumpFile).Length -eq 0) { + Remove-TempContainer + Write-Err "Dump file '$DumpFile' is empty. Something went wrong with pg_dump." +} + +$dumpContent = Get-Content $DumpFile -TotalCount 5 -Raw +if ($dumpContent -notmatch "PostgreSQL database dump") { + Remove-TempContainer + Write-Err "Dump file does not contain a valid PostgreSQL dump header - the file may be corrupt." +} + +$dumpLines = (Get-Content $DumpFile | Measure-Object -Line).Lines +if ($dumpLines -lt 10) { + Remove-TempContainer + Write-Err "Dump has only $dumpLines lines - suspiciously small. Aborting." +} + +$dumpSize = "{0:N1} MB" -f ((Get-Item $DumpFile).Length / 1MB) +Write-Ok "Dump complete: $dumpSize ($dumpLines lines) -> $DumpFile" + +Write-Info "Stopping temporary PostgreSQL 14 container..." +docker stop $TempContainer *>$null +docker rm $TempContainer *>$null +Write-Ok "Temporary container removed." + +# ── Step 3: Recover SECRET_KEY ─────────────────────────────────────────────── + +Write-Step "3" "Recovering SECRET_KEY" + +$recoveredKey = "" + +$keyCheck = docker run --rm -v "${OldVolume}:/data" alpine sh -c 'test -f /data/.secret_key && cat /data/.secret_key' 2>$null +if ($LASTEXITCODE -eq 0 -and $keyCheck) { + $recoveredKey = $keyCheck.Trim() + Write-Ok "Recovered SECRET_KEY from '$OldVolume'." +} else { + Write-Warn "No SECRET_KEY file found at /data/.secret_key in '$OldVolume'." + Write-Warn "This means the all-in-one container was launched with SECRET_KEY set as an explicit env var." + + if ($Yes) { + $bytes = New-Object byte[] 32 + $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new() + $rng.GetBytes($bytes) + $rng.Dispose() + $recoveredKey = [Convert]::ToBase64String($bytes) + Write-Warn "Non-interactive mode: generated a new SECRET_KEY automatically." + Write-Warn "All active browser sessions will be logged out after migration." + Write-Warn "To restore your original key, update SECRET_KEY in .\surfsense\.env afterwards." + } else { + Write-Warn "Enter the SECRET_KEY from your old container's environment" + $recoveredKey = Read-Host "[SurfSense] (press Enter to generate a new one - existing sessions will be invalidated)" + if (-not $recoveredKey) { + $bytes = New-Object byte[] 32 + $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::new() + $rng.GetBytes($bytes) + $rng.Dispose() + $recoveredKey = [Convert]::ToBase64String($bytes) + Write-Warn "Generated a new SECRET_KEY. All active browser sessions will be logged out after migration." + } + } +} + +Set-Content -Path $KeyFile -Value $recoveredKey -NoNewline +Write-Ok "SECRET_KEY saved to $KeyFile" + +# ── Done ───────────────────────────────────────────────────────────────────── + +Write-Host "" +Write-Host ("=" * 62) -ForegroundColor Green +Write-Host " Data extraction complete!" -ForegroundColor Green +Write-Host ("=" * 62) -ForegroundColor Green +Write-Host "" + +Write-Ok "Dump file : $DumpFile ($dumpSize)" +Write-Ok "Secret key: $KeyFile" +Write-Host "" +Write-Info "Next step - run install.ps1 from this same directory:" +Write-Host "" +Write-Host " irm https://raw.githubusercontent.com/MODSetter/SurfSense/main/docker/scripts/install.ps1 | iex" -ForegroundColor Cyan +Write-Host "" +Write-Info "install.ps1 will detect the dump, restore your data into PostgreSQL 17," +Write-Info "and start the full SurfSense stack automatically." +Write-Host "" +Write-Warn "Keep both files until you have verified the migration:" +Write-Warn " $DumpFile" +Write-Warn " $KeyFile" +Write-Warn "Full log saved to: $LogFile" +Write-Host "" + +Log "Migration extraction completed successfully at $(Get-Date)"