1#!/usr/bin/env pwsh2# nz.ps1: push a directory (or one html file) to your nz worker, print its url.3#4# nz push <dir|file> [--ttl 14d|never] [--password PW] [--new]5# nz update <id> [dir|file] [--ttl 14d|never] [--password PW | --no-password]6#7# The same tool as ./nz, for Windows PowerShell 5.1+ and pwsh 7. It uses tar.exe8# and curl.exe, which ship with Windows 10 and later, and shares $NZ_HOME9# (~/.config/nz: config, sites) with ./nz. Flags also work as -ttl, -password.10$ErrorActionPreference = 'Stop'1112function Die($msg) { [Console]::Error.WriteLine("nz: $msg"); exit 1 }13function Usage {14 [Console]::Error.WriteLine("nz push <dir|file> [--ttl 14d|never] [--password PW] [--new]")15 [Console]::Error.WriteLine("nz update <id> [dir|file] [--ttl 14d|never] [--password PW | --no-password]")16 exit 217}1819if ($args.Count -lt 1 -or $args[0] -notin 'push', 'update') { Usage }20$cmd = $args[0]21$headers = @(); $new = $false; $pos = @()22for ($i = 1; $i -lt $args.Count; $i++) {23 switch -regex ([string]$args[$i]) {24 '^--?ttl$' { if (++$i -ge $args.Count) { Usage }; $headers += "nz-ttl: $($args[$i])"; break }25 '^--?password$' { if (++$i -ge $args.Count) { Usage }; $headers += "nz-password: $($args[$i])"; break }26 '^--?no-password$' { $headers += 'nz-password;'; break }27 '^--?new$' { $new = $true; break }28 '^-' { Usage }29 default { $pos += [string]$args[$i] }30 }31}3233# config: NZ_SERVER=... and NZ_TOKEN=... lines, the same file ./nz sources34$nzHome = if ($env:NZ_HOME) { $env:NZ_HOME } else { Join-Path $HOME '.config/nz' }35$crumbs = Join-Path $nzHome 'sites'36$server = $env:NZ_SERVER; $token = $env:NZ_TOKEN37$config = Join-Path $nzHome 'config'38if (Test-Path -LiteralPath $config) {39 foreach ($line in Get-Content -LiteralPath $config) {40 if ($line -match '^\s*(NZ_SERVER|NZ_TOKEN)=(.*)$') {41 $v = $Matches[2].Trim().Trim('"', "'")42 if ($Matches[1] -eq 'NZ_SERVER') { $server = $v } else { $token = $v }43 }44 }45}46if (-not $server -or -not $token) { Die "set NZ_SERVER and NZ_TOKEN in $config" }47# curl.exe, not curl: in Windows PowerShell `curl` means Invoke-WebRequest48$curl = (Get-Command curl.exe, curl -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1).Source49if (-not $curl) { Die 'curl not found' }5051# files under a directory, as forward-slash paths, minus dotfiles and node_modules52function Get-Files($root, $rel) {53 $dir = if ($rel) { Join-Path $root $rel } else { $root }54 foreach ($e in Get-ChildItem -LiteralPath $dir -Force) {55 if ($e.Name.StartsWith('.') -or $e.Name -eq 'node_modules') { continue }56 $r = if ($rel) { "$rel/$($e.Name)" } else { $e.Name }57 if ($e.PSIsContainer) { Get-Files $root $r } else { $r }58 }59}6061# ustar of a directory, or of one file as index.html, into $tar62function Pack($path) {63 if (Test-Path -LiteralPath $path -PathType Container) {64 $root = Get-Src $path65 [string[]]$files = @(Get-Files $root '')66 [Array]::Sort($files, [StringComparer]::Ordinal)67 [IO.File]::WriteAllText($list, ($files -join "`n") + "`n") # utf-8, no BOM, LF68 $env:COPYFILE_DISABLE = '1' # macOS tar: no ._ files69 & tar --format=ustar -cf $tar -C $root -T $list70 if ($LASTEXITCODE) { Die 'tar failed' }71 } elseif (Test-Path -LiteralPath $path -PathType Leaf) {72 $one = Join-Path $tmp 'one'73 New-Item -ItemType Directory $one | Out-Null74 Copy-Item -LiteralPath $path (Join-Path $one 'index.html')75 Pack $one76 } else {77 Die "no such file or directory: $path"78 }79}8081# absolute path, the key for the breadcrumb file82function Get-Src($path) { (Resolve-Path -LiteralPath $path).ProviderPath.TrimEnd('\', '/') }8384function Get-Crumb($src) {85 $id = ''86 if (Test-Path -LiteralPath $crumbs) {87 foreach ($l in [IO.File]::ReadAllLines($crumbs)) {88 $p = $l -split "`t", 289 if ($p.Count -eq 2 -and $p[1] -eq $src) { $id = $p[0] }90 }91 }92 $id93}9495# send <method> <path> [-Body]: returns the http status, response body in $out96function Send($method, $path, [switch]$Body) {97 $data = @()98 if ($Body) { $data = @('-H', 'content-type: application/x-tar', '--data-binary', "@$tar") }99 $code = & $curl -sS -o $out -w '%{http_code}' -X $method -H "@$hfile" @data "$server$path"100 if ($LASTEXITCODE) { exit 1 }101 $code102}103104# url to stdout, details to stderr, and remember which path it came from105function Finish($code, $src) {106 $lines = @(Get-Content -LiteralPath $out -Encoding UTF8)107 if ($code -notmatch '^2') { Die ($lines -join "`n") }108 $lines | Select-Object -Skip 1 | ForEach-Object { [Console]::Error.WriteLine($_) }109 if ($src) {110 New-Item -ItemType Directory -Force $nzHome | Out-Null111 $id = ($lines[0] -replace '^.*//', '') -replace '\..*$', ''112 [IO.File]::AppendAllText($crumbs, "$id`t$src`n")113 }114 $lines[0]115}116117$tmp = Join-Path ([IO.Path]::GetTempPath()) "nz-$([guid]::NewGuid())"118New-Item -ItemType Directory $tmp | Out-Null119$tar = Join-Path $tmp 'site.tar'; $list = Join-Path $tmp 'files'120$out = Join-Path $tmp 'out'; $hfile = Join-Path $tmp 'headers'121try {122 # headers live in a file so the token and password stay out of the process list123 [IO.File]::WriteAllText($hfile, ((@("authorization: Bearer $token") + $headers) -join "`n") + "`n")124 if ($cmd -eq 'push') {125 if ($pos.Count -ne 1) { Usage }126 Pack $pos[0]127 $src = Get-Src $pos[0]128 $id = if ($new) { '' } else { Get-Crumb $src }129 if ($id) {130 $code = Send PUT "/$id" -Body131 if ($code -eq '404') { [Console]::Error.WriteLine("nz: $id has expired, pushing a new site"); $id = '' }132 }133 if (-not $id) { $code = Send POST '/' -Body }134 Finish $code $src135 } else {136 if ($pos.Count -lt 1 -or $pos.Count -gt 2 -or $new) { Usage }137 if ($pos.Count -eq 2) {138 Pack $pos[1]139 Finish (Send PUT "/$($pos[0])" -Body) (Get-Src $pos[1])140 } else {141 Finish (Send PUT "/$($pos[0])") ''142 }143 }144} finally {145 Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue146}