Files
blance e384719ecb Add per-practice logins, a standalone login test, and pre-commit checks
The ChatGPT transcript showed each CAMBS practice has its own SFTP
account (YSMD is YAM62682 on io.myallylogin.com:14147), not one shared
entity login - so the wizard needed a way to set those without hand-
writing nine practice.json files. Option 5 does that, and the settings
screen now lists which login each practice will actually use.

test-login.bat tests one account on its own, with the details typed in
rather than read from a config, which isolates the account from whatever
a script is doing to the credentials. It reports password length,
stray whitespace, and $ / backtick characters - the last being the usual
reason a login works in the WinSCP window but fails from a script, since
"$pass" interpolates in PowerShell while '$pass' does not. On failure it
prints the authentication methods the server offered.

check.sh runs what has actually caught bugs here: non-ASCII characters,
"$name?" (? is legal in an unbraced variable name, so it renders empty -
this bit twice), parse errors, 5.1 syntax compatibility, and JSON validity.
2026-08-30 20:28:32 -05:00

774 lines
30 KiB
PowerShell

# Interactive setup - asks plain questions and writes entity.json so nobody has
# to hand-edit JSON. Called by setup-entity.bat. Safe to re-run any time.
$scriptRoot = Split-Path -Parent $PSCommandPath
$entitiesDir = Join-Path $scriptRoot "Entities"
$winscpDll = Join-Path $scriptRoot "WinSCP\WinSCPnet.dll"
if (-not (Test-Path $entitiesDir)) { New-Item -ItemType Directory -Path $entitiesDir | Out-Null }
# -- Asking questions ---------------------------------------------------------
function Read-Answer($question, $default, [switch]$AllowBlank) {
while ($true) {
if ($default) {
Write-Host "$question " -NoNewline -ForegroundColor White
Write-Host "[$default]" -NoNewline -ForegroundColor DarkGray
Write-Host ": " -NoNewline -ForegroundColor White
} else {
Write-Host "${question}: " -NoNewline -ForegroundColor White
}
$answer = Read-Host
if ([string]::IsNullOrWhiteSpace($answer)) {
if ($default) { return $default }
if ($AllowBlank) { return $null }
Write-Host " (this one is required)" -ForegroundColor Yellow
continue
}
return $answer.Trim()
}
}
function Read-YesNo($question, $default) {
$hint = if ($default -eq "y") { "Y/n" } else { "y/N" }
while ($true) {
Write-Host "$question " -NoNewline -ForegroundColor White
Write-Host "($hint)" -NoNewline -ForegroundColor DarkGray
Write-Host ": " -NoNewline -ForegroundColor White
$answer = Read-Host
if ([string]::IsNullOrWhiteSpace($answer)) { $answer = $default }
switch ($answer.Trim().ToLower()) {
"y" { return $true }
"yes" { return $true }
"n" { return $false }
"no" { return $false }
}
Write-Host " (please answer y or n)" -ForegroundColor Yellow
}
}
function Read-Choice($question, $options, $default) {
Write-Host ""
Write-Host $question -ForegroundColor White
for ($i = 0; $i -lt $options.Count; $i++) {
Write-Host (" {0}. {1}" -f ($i + 1), $options[$i])
}
while ($true) {
Write-Host "Choose 1-$($options.Count) " -NoNewline -ForegroundColor White
Write-Host "[$default]" -NoNewline -ForegroundColor DarkGray
Write-Host ": " -NoNewline -ForegroundColor White
$answer = Read-Host
if ([string]::IsNullOrWhiteSpace($answer)) { $answer = [string]$default }
$n = 0
if ([int]::TryParse(([string]$answer).Trim(), [ref]$n) -and $n -ge 1 -and $n -le $options.Count) {
return $n
}
Write-Host " (type a number between 1 and $($options.Count))" -ForegroundColor Yellow
}
}
function Write-Header($text) {
Write-Host ""
Write-Host ("=" * 64) -ForegroundColor Cyan
Write-Host " $text" -ForegroundColor Cyan
Write-Host ("=" * 64) -ForegroundColor Cyan
}
function Write-Section($text) {
Write-Host ""
Write-Host "--- $text " -NoNewline -ForegroundColor Cyan
Write-Host ("-" * [Math]::Max(1, (58 - $text.Length))) -ForegroundColor Cyan
}
# -- Config file read/write ---------------------------------------------------
function Get-EntityNames {
if (-not (Test-Path $entitiesDir)) { return @() }
return @(Get-ChildItem -Path $entitiesDir -Directory |
Where-Object { $_.Name -ne "EXAMPLE" } |
ForEach-Object { $_.Name })
}
function Save-EntityConfig($entityName, $config) {
$dir = Join-Path $entitiesDir $entityName
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null }
$file = Join-Path $dir "entity.json"
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $file -Encoding UTF8
return $file
}
function Get-EntityConfig($entityName) {
$file = Join-Path (Join-Path $entitiesDir $entityName) "entity.json"
if (-not (Test-Path $file)) { return $null }
return Get-Content $file -Raw | ConvertFrom-Json
}
function Copy-RunBat($entityName) {
$source = Join-Path $scriptRoot "run.bat"
$target = Join-Path (Join-Path $entitiesDir $entityName) "run.bat"
if ((Test-Path $source) -and -not (Test-Path $target)) {
Copy-Item -Path $source -Destination $target
}
}
# -- Asking about one server --------------------------------------------------
# Returns a hashtable of connection settings, or $null if the user backs out.
function Read-ServerDetails($label, $existing) {
$defaultHost = $null
$defaultUser = $null
$defaultProto = 1
if ($existing -and $existing.host) {
$defaultHost = (([string]$existing.host) -replace '^\w+://', '').Split('/')[0].Trim()
$defaultUser = $existing.username
switch (Get-ConnProtocol $existing "sftp") {
"ftps" { $defaultProto = 2 }
"ftp" { $defaultProto = 3 }
}
}
Write-Host ""
Write-Host "Server address for $label" -ForegroundColor White
Write-Host " Just the name - no sftp:// or ftp:// in front." -ForegroundColor DarkGray
Write-Host " Example: sft.polluxsystems.com" -ForegroundColor DarkGray
$serverHost = Read-Answer "Server address" $defaultHost
# Strip a pasted scheme/path so it can't break the connection later.
$serverHost = ($serverHost -replace '^\w+://', '').Split('/')[0].Trim()
$protoChoice = Read-Choice "How does $serverHost receive files?" @(
"SFTP - secure, port 22 (most common)",
"FTPS - secure FTP, port 21",
"FTP - plain, no encryption, port 21"
) $defaultProto
$protocol = @("sftp", "ftps", "ftp")[$protoChoice - 1]
Write-Host ""
$username = Read-Answer "Username" $defaultUser
$password = $null
if ($existing -and $existing.password) {
Write-Host ""
Write-Host "A password is already saved for this server." -ForegroundColor DarkGray
if (Read-YesNo "Keep the saved password?" "y") {
$password = $existing.password
}
}
if (-not $password) {
$password = Read-Answer "Password" $null
}
return @{
host = $serverHost
protocol = $protocol
username = $username
password = $password
}
}
function Read-RemotePath($what, $default) {
Write-Host ""
Write-Host "Where do $what go on the server?" -ForegroundColor White
Write-Host " Type {practice} where the practice name belongs and it gets" -ForegroundColor DarkGray
Write-Host " filled in automatically for each one - so one line covers all." -ForegroundColor DarkGray
Write-Host " Example: /home/clients/CAMBS/{practice}" -ForegroundColor DarkGray
return Read-Answer "Folder path" $default
}
# -- New / edit entity --------------------------------------------------------
function New-Entity {
Write-Header "Set up a new entity"
Write-Host ""
Write-Host "The entity name is the folder name you use for this group of" -ForegroundColor DarkGray
Write-Host "practices - for example CAMBS, RMI, INLAND." -ForegroundColor DarkGray
$entityName = $null
while (-not $entityName) {
$candidate = Read-Answer "Entity name" $null
if ($candidate.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()) -ge 0) {
Write-Host " A folder name cannot contain \ / : * ? "" < > | - try again." -ForegroundColor Yellow
continue
}
$entityName = $candidate
}
$entityDir = Join-Path $entitiesDir $entityName
if (Test-Path (Join-Path $entityDir "entity.json")) {
Write-Host ""
Write-Host "$entityName is already set up." -ForegroundColor Yellow
if (-not (Read-YesNo "Start over and replace its settings?" "n")) { return }
}
Write-Section "The daily upload"
Write-Host "These are the dated folders (like 20260826) full of scanned images." -ForegroundColor DarkGray
$daily = Read-ServerDetails "the daily files" $null
$tiffPath = Read-RemotePath "the daily image files" $null
Write-Host ""
$pdfPath = $null
Write-Host "Some entities get PT STMT files mixed into the same dated folder," -ForegroundColor DarkGray
Write-Host "and those go to a different folder on the server." -ForegroundColor DarkGray
if (Read-YesNo "Do PT STMT files show up in the dated folders for ${entityName}?" "n") {
$pdfPath = Read-RemotePath "the PT STMT files" $null
}
Write-Section "Patient files"
Write-Host "These come from Patient\Export\PT STMT_<date>\ - the folders the" -ForegroundColor DarkGray
Write-Host "rename script creates. Many entities do not use this at all." -ForegroundColor DarkGray
$patient = $null
$patientPath = $null
if (Read-YesNo "Does $entityName send patient files too?" "n") {
$samePlace = Read-Choice "Which server do the patient files go to?" @(
"The same server as the daily files ($($daily.host))",
"A different server"
) 1
if ($samePlace -eq 1) {
$patient = @{
host = $daily.host
protocol = $daily.protocol
username = $daily.username
password = $daily.password
}
} else {
$patient = Read-ServerDetails "the patient files" $null
}
$patientPath = Read-RemotePath "the patient .txt/.tif files" $null
}
$config = New-ConfigObject $daily $tiffPath $pdfPath $patient $patientPath
$file = Save-EntityConfig $entityName $config
Copy-RunBat $entityName
Write-Host ""
Write-Host "Saved $file" -ForegroundColor Green
Add-Practices $entityName
Write-Host ""
if (Read-YesNo "Test the connection now?" "y") { Test-Entity $entityName }
}
function New-ConfigObject($daily, $tiffPath, $pdfPath, $patient, $patientPath) {
$workflow = if ($patient) { "insurance+patient" } else { "insurance" }
$ftps = [ordered]@{
host = $daily.host
protocol = $daily.protocol
port = $null
username = $daily.username
password = $daily.password
tiff_path = $tiffPath
pdf_path = $pdfPath
}
$sftp = [ordered]@{
host = $null
protocol = $null
port = $null
username = $null
password = $null
patient_path = $null
}
if ($patient) {
$sftp.host = $patient.host
$sftp.protocol = $patient.protocol
$sftp.username = $patient.username
$sftp.password = $patient.password
$sftp.patient_path = $patientPath
}
return [PSCustomObject][ordered]@{
"_note" = "Written by setup-entity.bat - run it again to change anything. The 'ftps' section is the daily upload (it can be SFTP too - see protocol); the 'sftp' section is the patient files."
workflow = $workflow
ftps = [PSCustomObject]$ftps
sftp = [PSCustomObject]$sftp
}
}
function Add-Practices($entityName) {
$export1 = Join-Path (Join-Path $entitiesDir $entityName) "Export1"
if (-not (Test-Path $export1)) { New-Item -ItemType Directory -Path $export1 | Out-Null }
$existing = @(Get-ChildItem -Path $export1 -Directory | ForEach-Object { $_.Name })
Write-Section "Practices under $entityName"
if ($existing.Count -gt 0) {
Write-Host "Already there: $($existing -join ', ')" -ForegroundColor Gray
Write-Host ""
if (-not (Read-YesNo "Add more practice folders?" "n")) { return }
} else {
Write-Host "Type each practice name and press Enter." -ForegroundColor DarkGray
Write-Host "Press Enter on an empty line when you are done." -ForegroundColor DarkGray
}
while ($true) {
$name = Read-Answer "Practice name (blank to finish)" $null -AllowBlank
if (-not $name) { break }
if ($name.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()) -ge 0) {
Write-Host " A folder name cannot contain \ / : * ? "" < > | - try again." -ForegroundColor Yellow
continue
}
$dir = Join-Path $export1 $name
if (Test-Path $dir) {
Write-Host " $name already exists - skipped." -ForegroundColor Gray
continue
}
New-Item -ItemType Directory -Path $dir | Out-Null
New-Item -ItemType Directory -Path (Join-Path $dir "Archive sent to FTP") | Out-Null
New-Item -ItemType Directory -Path (Join-Path $dir "Patient\Export\Archive Sent to FTP") -Force | Out-Null
Write-Host " Created $name" -ForegroundColor Green
}
}
function Get-PracticeNames($entityName) {
$export1 = Join-Path (Join-Path $entitiesDir $entityName) "Export1"
if (-not (Test-Path $export1)) { return @() }
return @(Get-ChildItem -Path $export1 -Directory | ForEach-Object { $_.Name })
}
# Same rules engine.ps1 uses: practice.json replaces or adds to entity.json for
# that one practice, so what gets tested here is what will actually be sent.
function Get-MergedPracticeConfig($entityName, $practiceName, $entityConfig) {
$file = Join-Path (Join-Path (Join-Path (Join-Path $entitiesDir $entityName) "Export1") $practiceName) "practice.json"
$merged = $entityConfig | ConvertTo-Json -Depth 10 | ConvertFrom-Json
if (-not (Test-Path $file)) { return $merged }
$override = Get-Content $file -Raw | ConvertFrom-Json
foreach ($section in $override.PSObject.Properties) {
if ($section.Name -like "_*") { continue }
if ($null -eq $merged.PSObject.Properties[$section.Name]) {
$merged | Add-Member -NotePropertyName $section.Name -NotePropertyValue $section.Value
} elseif ($section.Value -is [System.Management.Automation.PSCustomObject]) {
foreach ($prop in $section.Value.PSObject.Properties) {
$merged.$($section.Name) | Add-Member -NotePropertyName $prop.Name -NotePropertyValue $prop.Value -Force
}
} else {
Set-Field $merged $section.Name $section.Value
}
}
return $merged
}
function Set-PracticeLogin($entityName, $entityConfig) {
$practices = Get-PracticeNames $entityName
if ($practices.Count -eq 0) {
Write-Host ""
Write-Host "No practice folders yet - add some first." -ForegroundColor Yellow
return
}
$choice = Read-Choice "Which practice?" $practices 1
$practiceName = $practices[$choice - 1]
$dir = Join-Path (Join-Path (Join-Path $entitiesDir $entityName) "Export1") $practiceName
$file = Join-Path $dir "practice.json"
$existing = $null
if (Test-Path $file) { $existing = Get-Content $file -Raw | ConvertFrom-Json }
Write-Section "$practiceName"
Write-Host "Anything you set here is used for $practiceName only." -ForegroundColor DarkGray
Write-Host "Anything you leave alone still comes from $entityName." -ForegroundColor DarkGray
$ftps = [ordered]@{}
if ($existing -and $existing.ftps) {
foreach ($p in $existing.ftps.PSObject.Properties) { $ftps[$p.Name] = $p.Value }
}
Write-Host ""
if (Read-YesNo "Does $practiceName have its own username and password?" "y") {
Write-Host ""
$ftps["username"] = Read-Answer "Username for $practiceName" $ftps["username"]
$keep = $false
if ($ftps["password"]) {
Write-Host ""
Write-Host "A password is already saved for $practiceName." -ForegroundColor DarkGray
$keep = Read-YesNo "Keep it?" "y"
}
if (-not $keep) { $ftps["password"] = Read-Answer "Password for $practiceName" $null }
}
Write-Host ""
if (Read-YesNo "Does $practiceName use a different server than the rest of ${entityName}?" "n") {
$srv = Read-ServerDetails "$practiceName" $existing.ftps
$ftps["host"] = $srv.host
$ftps["protocol"] = $srv.protocol
$ftps["username"] = $srv.username
$ftps["password"] = $srv.password
}
Write-Host ""
if (Read-YesNo "Does $practiceName use a different folder on the server?" "n") {
$ftps["tiff_path"] = Read-RemotePath "$practiceName's daily image files" $ftps["tiff_path"]
}
if ($ftps.Count -eq 0) {
if (Test-Path $file) {
Remove-Item $file -Force
Write-Host ""
Write-Host "Nothing set - removed $practiceName's practice.json." -ForegroundColor Gray
} else {
Write-Host ""
Write-Host "Nothing set for $practiceName." -ForegroundColor Gray
}
return
}
$out = [ordered]@{
"_note" = "Settings for $practiceName only. Anything not listed comes from ..\..\entity.json."
ftps = [PSCustomObject]$ftps
}
([PSCustomObject]$out) | ConvertTo-Json -Depth 10 | Set-Content -Path $file -Encoding UTF8
Write-Host ""
Write-Host "Saved $file" -ForegroundColor Green
}
function Show-PracticeLogins($entityName, $entityConfig) {
$practices = Get-PracticeNames $entityName
if ($practices.Count -eq 0) { return }
Write-Host ""
Write-Host " Practices" -ForegroundColor White
foreach ($p in $practices) {
$merged = Get-MergedPracticeConfig $entityName $p $entityConfig
$own = Test-Path (Join-Path (Join-Path (Join-Path (Join-Path $entitiesDir $entityName) "Export1") $p) "practice.json")
$tag = if ($own) { "own settings" } else { "uses $entityName's" }
Write-Host (" {0,-12} {1,-18} login: {2}" -f $p, $tag, (Show-Value $merged.ftps.username))
}
}
function Edit-Entity($entityName) {
$config = Get-EntityConfig $entityName
if (-not $config) {
Write-Host "No entity.json found for $entityName." -ForegroundColor Red
return
}
# A hand-written entity.json may be missing whole sections.
foreach ($section in @("ftps", "sftp")) {
if (-not $config.$section) {
Set-Field $config $section ([PSCustomObject]@{})
}
}
while ($true) {
Write-Header "$entityName - current settings"
Show-Settings $config
Show-PracticeLogins $entityName $config
$choice = Read-Choice "What do you want to change?" @(
"The daily-upload server (address, protocol, login)",
"Where the daily image files go on the server",
"Where PT STMT files from the dated folder go",
"Patient files (server and folder)",
"Give one practice its own login or folder",
"Add practice folders",
"Nothing - save and go back"
) 7
switch ($choice) {
1 {
$d = Read-ServerDetails "the daily files" $config.ftps
Set-Field $config.ftps "host" $d.host
Set-Field $config.ftps "protocol" $d.protocol
Set-Field $config.ftps "username" $d.username
Set-Field $config.ftps "password" $d.password
}
2 {
Set-Field $config.ftps "tiff_path" (Read-RemotePath "the daily image files" $config.ftps.tiff_path)
}
3 {
Write-Host ""
if (Read-YesNo "Do PT STMT files show up in the dated folders?" "n") {
Set-Field $config.ftps "pdf_path" (Read-RemotePath "the PT STMT files" $config.ftps.pdf_path)
} else {
Set-Field $config.ftps "pdf_path" $null
Write-Host " PT STMT files from the dated folder will be ignored." -ForegroundColor Gray
}
}
4 {
Write-Host ""
if (Read-YesNo "Does $entityName send patient files?" "n") {
$same = Read-Choice "Which server do the patient files go to?" @(
"The same server as the daily files ($($config.ftps.host))",
"A different server"
) 1
if ($same -eq 1) {
Set-Field $config.sftp "host" $config.ftps.host
Set-Field $config.sftp "protocol" $config.ftps.protocol
Set-Field $config.sftp "username" $config.ftps.username
Set-Field $config.sftp "password" $config.ftps.password
} else {
$p = Read-ServerDetails "the patient files" $config.sftp
Set-Field $config.sftp "host" $p.host
Set-Field $config.sftp "protocol" $p.protocol
Set-Field $config.sftp "username" $p.username
Set-Field $config.sftp "password" $p.password
}
Set-Field $config.sftp "patient_path" (Read-RemotePath "the patient .txt/.tif files" $config.sftp.patient_path)
Set-Field $config "workflow" "insurance+patient"
} else {
Set-Field $config "workflow" "insurance"
Write-Host " Patient files turned off for $entityName." -ForegroundColor Gray
}
}
5 { Set-PracticeLogin $entityName $config }
6 { Add-Practices $entityName }
7 {
$file = Save-EntityConfig $entityName $config
Copy-RunBat $entityName
Write-Host ""
Write-Host "Saved $file" -ForegroundColor Green
return
}
}
Save-EntityConfig $entityName $config | Out-Null
}
}
function Set-Field($object, $name, $value) {
$object | Add-Member -NotePropertyName $name -NotePropertyValue $value -Force
}
function Show-Settings($config) {
Write-Host ""
Write-Host " Daily upload" -ForegroundColor White
Write-Host " Server : $(Show-Value $config.ftps.host) over $(Show-Protocol $config.ftps 'ftp')"
Write-Host " Login : $(Show-Value $config.ftps.username) / $(Show-Password $config.ftps.password)"
Write-Host " Images to : $(Show-Value $config.ftps.tiff_path)"
Write-Host " PT STMT to: $(Show-Value $config.ftps.pdf_path)"
Write-Host ""
Write-Host " Patient files" -ForegroundColor White
if ($config.workflow -eq "insurance+patient") {
Write-Host " Server : $(Show-Value $config.sftp.host) over $(Show-Protocol $config.sftp 'sftp')"
Write-Host " Login : $(Show-Value $config.sftp.username) / $(Show-Password $config.sftp.password)"
Write-Host " Files to : $(Show-Value $config.sftp.patient_path)"
} else {
Write-Host " Not used for this entity." -ForegroundColor Gray
}
}
# Reports the protocol the upload will actually use, so a legacy config that
# only carries an "ftps://" prefix on the host doesn't read as unset.
function Show-Protocol($conn, $fallback) {
if ([string]::IsNullOrWhiteSpace([string]$conn.host)) { return "(not set)" }
$protocol = (Get-ConnProtocol $conn $fallback).ToUpper()
if ($conn.protocol) { return $protocol }
return "$protocol (assumed)"
}
function Show-Value($value) {
if ([string]::IsNullOrWhiteSpace([string]$value)) { return "(not set)" }
return [string]$value
}
function Show-Password($value) {
if ([string]::IsNullOrWhiteSpace([string]$value)) { return "(not set)" }
return "*" * ([string]$value).Length
}
# -- Connection test ----------------------------------------------------------
# Works out the protocol exactly the way engine.ps1 does, so a passing test
# here means the real run connects the same way.
function Get-ConnProtocol($conn, $fallback) {
if ($conn.protocol) {
$p = ([string]$conn.protocol).ToLower().Trim()
if ($p -in @("sftp", "ftps", "ftp")) { return $p }
}
if (([string]$conn.host) -imatch '^(\w+)://') {
$scheme = $Matches[1].ToLower()
if ($scheme -in @("sftp", "ftps", "ftp")) { return $scheme }
}
if ($conn.tls -eq $true) { return "ftps" }
return $fallback
}
function Get-SessionOptions($conn, $fallback) {
$opts = New-Object WinSCP.SessionOptions
# Older configs kept the protocol as an "sftp://" prefix on the host.
$opts.HostName = (([string]$conn.host) -replace '^\w+://', '').Split('/')[0].Trim()
$opts.UserName = $conn.username
$opts.Password = $conn.password
$protocol = Get-ConnProtocol $conn $fallback
if ($protocol -eq "sftp") {
$opts.Protocol = [WinSCP.Protocol]::Sftp
$opts.PortNumber = if ($conn.port) { [int]$conn.port } else { 22 }
$opts.GiveUpSecurityAndAcceptAnySshHostKey = $true
} else {
$opts.Protocol = [WinSCP.Protocol]::Ftp
$opts.PortNumber = if ($conn.port) { [int]$conn.port } else { 21 }
if ($protocol -eq "ftps") {
$opts.FtpSecure = [WinSCP.FtpSecure]::Explicit
$opts.GiveUpSecurityAndAcceptAnyTlsHostCertificate = $true
} else {
$opts.FtpSecure = [WinSCP.FtpSecure]::None
}
}
return $opts
}
function Test-ServerConnection($label, $conn, $pathTemplate, $practices, $fallback) {
Write-Host ""
Write-Host " $label" -ForegroundColor White
if ([string]::IsNullOrWhiteSpace([string]$conn.host)) {
Write-Host " Not set up - nothing to test." -ForegroundColor Gray
return
}
$opts = Get-SessionOptions $conn $fallback
$protocol = (Get-ConnProtocol $conn $fallback).ToUpper()
Write-Host " Connecting to $($opts.HostName) over $protocol as $($conn.username) ... " -NoNewline
$session = New-Object WinSCP.Session
try {
$session.Open($opts)
Write-Host "OK" -ForegroundColor Green
} catch {
Write-Host "FAILED" -ForegroundColor Red
Write-Host " $($_.Exception.Message)" -ForegroundColor Red
Write-Host ""
Write-Host " $(Get-ConnectionHint $_.Exception.Message)" -ForegroundColor Yellow
$session.Dispose()
return
}
if ([string]::IsNullOrWhiteSpace([string]$pathTemplate)) {
Write-Host " No folder path set yet - skipping the folder check." -ForegroundColor Gray
$session.Dispose()
return
}
try {
foreach ($practice in $practices) {
$remote = $pathTemplate -replace '\{practice\}', $practice
Write-Host " $remote ... " -NoNewline
if ($session.FileExists($remote)) {
Write-Host "found" -ForegroundColor Green
} else {
Write-Host "NOT THERE" -ForegroundColor Red
}
}
} catch {
Write-Host "could not check - $($_.Exception.Message)" -ForegroundColor Yellow
} finally {
$session.Dispose()
}
}
function Get-ConnectionHint($message) {
if ($message -imatch "cannot connect to an SFTP server using an FTP") {
return "This server wants SFTP. Run setup again and pick SFTP for this server."
}
if ($message -imatch "cannot connect to an FTP server using an SFTP|expected SSH") {
return "This server wants FTP or FTPS. Run setup again and pick FTPS for this server."
}
if ($message -imatch "authentication|password|login incorrect|530") {
return "The username or password was rejected. Run setup again and retype the login."
}
if ($message -imatch "host does not exist|no such host|could not resolve") {
return "That server address could not be found. Check it for a typo."
}
if ($message -imatch "timed out|timeout|refused") {
return "No answer from the server. Check the address, or ask IT whether the firewall allows it."
}
return "Write down this message - it says what the server objected to."
}
function Test-Entity($entityName) {
Write-Header "Testing $entityName"
if (-not (Test-Path $winscpDll)) {
Write-Host "WinSCP is not installed yet - run setup.bat first." -ForegroundColor Red
return
}
Get-ChildItem -Path (Join-Path $scriptRoot "WinSCP") -Recurse -File |
Unblock-File -ErrorAction SilentlyContinue
try {
Add-Type -Path $winscpDll
} catch {
Write-Host "Could not load WinSCP - $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Try deleting the WinSCP folder and running setup.bat again." -ForegroundColor Yellow
return
}
$config = Get-EntityConfig $entityName
if (-not $config) {
Write-Host "No entity.json found for $entityName." -ForegroundColor Red
return
}
$export1 = Join-Path (Join-Path $entitiesDir $entityName) "Export1"
$practices = @()
if (Test-Path $export1) {
$practices = @(Get-ChildItem -Path $export1 -Directory | ForEach-Object { $_.Name })
}
if ($practices.Count -eq 0) {
Write-Host "No practice folders yet - testing the login only." -ForegroundColor Gray
}
Test-ServerConnection "Daily upload" $config.ftps $config.ftps.tiff_path $practices "ftp"
if ($config.ftps.pdf_path) {
Test-ServerConnection "PT STMT folders" $config.ftps $config.ftps.pdf_path $practices "ftp"
}
if ($config.workflow -eq "insurance+patient") {
Test-ServerConnection "Patient files" $config.sftp $config.sftp.patient_path $practices "sftp"
}
Write-Host ""
Write-Host "A folder marked NOT THERE means the upload will fail for that" -ForegroundColor Yellow
Write-Host "practice. Either the path is wrong, or the partner has not made" -ForegroundColor Yellow
Write-Host "the folder yet - check the path first, then ask them." -ForegroundColor Yellow
}
# -- Main menu ----------------------------------------------------------------
function Select-Entity($purpose) {
$names = Get-EntityNames
if ($names.Count -eq 0) {
Write-Host ""
Write-Host "No entities set up yet - pick 'Set up a new entity' first." -ForegroundColor Yellow
return $null
}
$choice = Read-Choice "Which entity do you want to $purpose" $names 1
return $names[$choice - 1]
}
Write-Header "Melissa Uploader - Setup"
Write-Host ""
Write-Host "This asks a few questions and writes the settings files for you." -ForegroundColor Gray
Write-Host "You never have to open one by hand." -ForegroundColor Gray
while ($true) {
$names = Get-EntityNames
Write-Host ""
if ($names.Count -gt 0) {
Write-Host "Entities set up so far: $($names -join ', ')" -ForegroundColor Gray
} else {
Write-Host "No entities set up yet." -ForegroundColor Gray
}
$choice = Read-Choice "What do you want to do?" @(
"Set up a new entity",
"Change an entity's settings",
"Test an entity's connection and folders",
"Quit"
) 4
switch ($choice) {
1 { New-Entity }
2 { $name = Select-Entity "change"; if ($name) { Edit-Entity $name } }
3 { $name = Select-Entity "test"; if ($name) { Test-Entity $name } }
4 {
Write-Host ""
Write-Host "Done. Double-click run-all.bat when you are ready to upload." -ForegroundColor Green
Write-Host ""
exit 0
}
}
}