param([string]$ConfigPath = "$PSScriptRoot\config.json") $ErrorActionPreference = 'Stop' $config = Get-Content -Raw $ConfigPath | ConvertFrom-Json $logDir = Join-Path $PSScriptRoot 'logs'; New-Item -ItemType Directory -Force -Path $logDir | Out-Null $logFile = Join-Path $logDir ('agent-' + (Get-Date -Format 'yyyy-MM-dd') + '.log') function Log($m){$line="$(Get-Date -Format o) $m"; Add-Content -Path $logFile -Value $line; Write-Host $line} function Json($ctx,$code,$obj){$body=$obj|ConvertTo-Json -Depth 6; $bytes=[Text.Encoding]::UTF8.GetBytes($body); $ctx.Response.StatusCode=$code; $ctx.Response.ContentType='application/json'; $ctx.Response.Headers['Access-Control-Allow-Origin']='null'; $ctx.Response.Headers['Access-Control-Allow-Headers']='Content-Type'; $ctx.Response.Headers['Access-Control-Allow-Methods']='GET,POST,OPTIONS'; $ctx.Response.OutputStream.Write($bytes,0,$bytes.Length); $ctx.Response.Close()} function AwsArgs($args){$all=@(); if($config.awsProfile){$all += @('--profile',$config.awsProfile)}; $all += $args; return $all} function RunAws([string[]]$args){$psi=New-Object Diagnostics.ProcessStartInfo; $psi.FileName=$config.awsExe; foreach($a in (AwsArgs $args)){$psi.ArgumentList.Add($a)}; $psi.RedirectStandardOutput=$true; $psi.RedirectStandardError=$true; $psi.UseShellExecute=$false; $p=[Diagnostics.Process]::Start($psi); $stdout=$p.StandardOutput.ReadToEnd(); $stderr=$p.StandardError.ReadToEnd(); $p.WaitForExit(); if($p.ExitCode -ne 0){throw "aws failed ($($p.ExitCode)): $stderr"}; return $stdout} $listener=[Net.HttpListener]::new(); $listener.Prefixes.Add('http://127.0.0.1:8765/'); $listener.Start(); Log 'CareTV Publish Agent v003 started on 127.0.0.1:8765' while($listener.IsListening){$ctx=$listener.GetContext(); try{if($ctx.Request.HttpMethod -eq 'OPTIONS'){Json $ctx 200 @{ok=$true}; continue}; $path=$ctx.Request.Url.AbsolutePath; if($path -eq '/api/status'){Json $ctx 200 @{ok=$true;version='v003';bucket=$config.bucket;distribution=$config.cloudFrontDistributionId;profile=$config.awsProfile;time=(Get-Date).ToString('o')}; continue}; if($path -eq '/api/ingest' -and $ctx.Request.HttpMethod -eq 'POST'){$inbox=[IO.Path]::GetFullPath([string]$config.inbox); $auth=[IO.Path]::GetFullPath([string]$config.authoritativeRoot); if(-not (Test-Path $inbox)){New-Item -ItemType Directory -Force -Path $inbox|Out-Null}; $day=Get-Date -Format 'yyyy-MM-dd'; $stamp=Get-Date -Format 'HHmmss'; $dest=Join-Path (Join-Path $auth $day) (Join-Path 'local-ingest' $stamp); New-Item -ItemType Directory -Force -Path $dest|Out-Null; $files=Get-ChildItem -LiteralPath $inbox -File; foreach($f in $files){Copy-Item -LiteralPath $f.FullName -Destination (Join-Path $dest $f.Name) -Force}; Json $ctx 200 @{ok=$true;count=$files.Count;destination=$dest;note='Source inbox files were copied, not deleted.'}; continue}; if($path -eq '/api/publish' -and $ctx.Request.HttpMethod -eq 'POST'){$reader=[IO.StreamReader]::new($ctx.Request.InputStream,$ctx.Request.ContentEncoding); $req=$reader.ReadToEnd()|ConvertFrom-Json; $source=[IO.Path]::GetFullPath([string]$req.sourceDir); if(-not (Test-Path $source -PathType Container)){throw 'Source directory does not exist'}; $prefix=([string]$req.prefix).TrimStart('/'); if($prefix -notlike 'beta/_daily-builds/*'){throw 'Remote prefix must stay under beta/_daily-builds/'}; $dest="s3://$($config.bucket)/$prefix"; Log "Sync $source -> $dest"; $sync=RunAws @('s3','sync',$source,$dest,'--only-show-errors'); $invalidatePath='/' + $prefix.TrimEnd('/') + '/*'; Log "Invalidate $invalidatePath"; $inv=RunAws @('cloudfront','create-invalidation','--distribution-id',$config.cloudFrontDistributionId,'--paths',$invalidatePath); Json $ctx 200 @{ok=$true;destination=$dest;invalidationPath=$invalidatePath;sync=$sync;cloudfront=($inv|ConvertFrom-Json)}; continue}; Json $ctx 404 @{ok=$false;error='Not found'}}catch{Log "ERROR $($_.Exception.Message)"; Json $ctx 500 @{ok=$false;error=$_.Exception.Message}}}