To uavhengige feil hindret oppstart på Windows:
1. app/notes var committet som en git-symlink (mode 120000). På
Windows uten symlink-støtte materialiseres den som en stray
tekstfil («../notes»), så appen ikke fant notatene. Start.bat
sin reparasjon brukte rmdir/mkdir som begge feiler når notes
er en fil.
→ Avregistrert symlinken fra git og gitignorert den. app/notes
opprettes nå per plattform av launcherne (symlink på unix,
kopi på Windows). Start.bat normaliserer robust: sletter en
stray fil, lager mappe og kopierer *.md. Start.sh håndterer
også fil-tilfellet.
2. server.ps1 krasjet umiddelbart pga. Add-Type System.Web (ikke
tilgjengelig i PowerShell 7), og cmd-vinduet forsvant uten pause.
→ Fjernet System.Web (bruker System.Uri::UnescapeDataString),
la til HttpListener-støttesjekk og tydelige feilmeldinger.
Start.bat pauser nå når serveren stopper, så feil kan leses.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
137 lines
4.8 KiB
PowerShell
137 lines
4.8 KiB
PowerShell
# =====================================================================
|
|
# SMF2290 Pensum — server.ps1
|
|
# Lett statisk HTTP-server som fallback når Python/Node ikke er installert.
|
|
# Krever bare PowerShell 5+ (innebygd i Windows 10/11).
|
|
# =====================================================================
|
|
|
|
param(
|
|
[int]$Port = 8765,
|
|
[string]$Root = "."
|
|
)
|
|
|
|
# Krasj ikke stille: vis full feil og stopp pent.
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
# Merk: vi bruker [System.Uri]::UnescapeDataString for URL-dekoding i stedet
|
|
# for System.Web.HttpUtility. System.Web er ikke tilgjengelig i PowerShell 7
|
|
# (.NET Core) og kan faile pa enkelte oppsett — det var en kilde til at
|
|
# vinduet "krasjet umiddelbart". System.Uri finnes i bade PS 5.1 og PS 7.
|
|
|
|
try {
|
|
$RootAbs = (Resolve-Path -LiteralPath $Root -ErrorAction Stop).Path
|
|
} catch {
|
|
Write-Host "Fant ikke app-mappen: $Root" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
if (-not ([System.Net.HttpListener]::IsSupported)) {
|
|
Write-Host "HttpListener stottes ikke pa denne maskinen." -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
$listener = New-Object System.Net.HttpListener
|
|
$listener.Prefixes.Add("http://127.0.0.1:$Port/")
|
|
|
|
try {
|
|
$listener.Start()
|
|
} catch {
|
|
Write-Host ""
|
|
Write-Host " Klarte ikke starte server pa port ${Port}:" -ForegroundColor Red
|
|
Write-Host " $($_.Exception.Message)" -ForegroundColor Red
|
|
Write-Host " Tips: porten kan vaere opptatt, eller en brannmur/policy blokkerer." -ForegroundColor DarkGray
|
|
exit 1
|
|
}
|
|
|
|
$mimeMap = @{
|
|
'.html' = 'text/html; charset=utf-8'
|
|
'.htm' = 'text/html; charset=utf-8'
|
|
'.css' = 'text/css; charset=utf-8'
|
|
'.js' = 'application/javascript; charset=utf-8'
|
|
'.mjs' = 'application/javascript; charset=utf-8'
|
|
'.json' = 'application/json; charset=utf-8'
|
|
'.md' = 'text/markdown; charset=utf-8'
|
|
'.txt' = 'text/plain; charset=utf-8'
|
|
'.svg' = 'image/svg+xml; charset=utf-8'
|
|
'.png' = 'image/png'
|
|
'.jpg' = 'image/jpeg'
|
|
'.jpeg' = 'image/jpeg'
|
|
'.gif' = 'image/gif'
|
|
'.ico' = 'image/x-icon'
|
|
'.webp' = 'image/webp'
|
|
'.woff' = 'font/woff'
|
|
'.woff2'= 'font/woff2'
|
|
'.ttf' = 'font/ttf'
|
|
'.otf' = 'font/otf'
|
|
'.pdf' = 'application/pdf'
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host " Server kjorer pa http://localhost:$Port" -ForegroundColor Green
|
|
Write-Host " Trykk Ctrl+C for a stoppe" -ForegroundColor DarkGray
|
|
Write-Host ""
|
|
|
|
try {
|
|
while ($listener.IsListening) {
|
|
$context = $listener.GetContext()
|
|
$req = $context.Request
|
|
$res = $context.Response
|
|
|
|
try {
|
|
$path = [System.Uri]::UnescapeDataString($req.Url.LocalPath)
|
|
if ([string]::IsNullOrEmpty($path) -or $path -eq '/') { $path = '/index.html' }
|
|
|
|
# Strip leading slash and normalize
|
|
$relative = $path.TrimStart('/').Replace('/', [System.IO.Path]::DirectorySeparatorChar)
|
|
$filePath = Join-Path $RootAbs $relative
|
|
|
|
# Resolve to prevent directory traversal
|
|
try {
|
|
$resolved = [System.IO.Path]::GetFullPath($filePath)
|
|
} catch {
|
|
$resolved = $filePath
|
|
}
|
|
|
|
if (-not $resolved.StartsWith($RootAbs, [StringComparison]::OrdinalIgnoreCase)) {
|
|
$res.StatusCode = 403
|
|
$body = [System.Text.Encoding]::UTF8.GetBytes("403 Forbidden")
|
|
$res.OutputStream.Write($body, 0, $body.Length)
|
|
} elseif (Test-Path -LiteralPath $resolved -PathType Container) {
|
|
# Directory: try index.html
|
|
$idx = Join-Path $resolved 'index.html'
|
|
if (Test-Path -LiteralPath $idx -PathType Leaf) {
|
|
$bytes = [System.IO.File]::ReadAllBytes($idx)
|
|
$res.ContentType = 'text/html; charset=utf-8'
|
|
$res.ContentLength64 = $bytes.Length
|
|
$res.OutputStream.Write($bytes, 0, $bytes.Length)
|
|
} else {
|
|
$res.StatusCode = 404
|
|
$body = [System.Text.Encoding]::UTF8.GetBytes("404 Not Found")
|
|
$res.OutputStream.Write($body, 0, $body.Length)
|
|
}
|
|
} elseif (Test-Path -LiteralPath $resolved -PathType Leaf) {
|
|
$bytes = [System.IO.File]::ReadAllBytes($resolved)
|
|
$ext = [System.IO.Path]::GetExtension($resolved).ToLower()
|
|
$mime = if ($mimeMap.ContainsKey($ext)) { $mimeMap[$ext] } else { 'application/octet-stream' }
|
|
$res.ContentType = $mime
|
|
$res.ContentLength64 = $bytes.Length
|
|
$res.OutputStream.Write($bytes, 0, $bytes.Length)
|
|
} else {
|
|
$res.StatusCode = 404
|
|
$body = [System.Text.Encoding]::UTF8.GetBytes("404 Not Found: $path")
|
|
$res.OutputStream.Write($body, 0, $body.Length)
|
|
}
|
|
} catch {
|
|
try {
|
|
$res.StatusCode = 500
|
|
$body = [System.Text.Encoding]::UTF8.GetBytes("500 Internal Server Error: " + $_.Exception.Message)
|
|
$res.OutputStream.Write($body, 0, $body.Length)
|
|
} catch {}
|
|
} finally {
|
|
try { $res.Close() } catch {}
|
|
}
|
|
}
|
|
} finally {
|
|
try { $listener.Stop() } catch {}
|
|
try { $listener.Close() } catch {}
|
|
}
|