Ctrl K
ComunidadGuías…

scripts/validate-android-video-upload.ps1

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<#
.SYNOPSIS
Semi-automates Android video upload validation against local telesrv.

.DESCRIPTION
The script handles the stable parts of the Android upload regression loop:
preflight checks, optional video fixture generation/push, baseline snapshots,
server log scanning, PostgreSQL media/message assertions, upload_parts cleanup,
and local blob existence checks.

It intentionally does not drive the Android media picker. Send the prepared
video manually from Android/Alice, then run -Phase AfterSend.
#>
[CmdletBinding()]
param(
    [ValidateSet("Preflight", "Prepare", "BeforeSend", "AfterSend", "All")]
    [string]$Phase = "Preflight",

    [long]$SenderUserId = 1780269504,
    [long]$RecipientUserId = 1780269505,

    [string]$AndroidPackage = "org.telegram.messenger.beta",
    [string]$DeviceSerial,

    [string]$PostgresContainer = "telesrv-postgres",
    [string]$Database = "telesrv",
    [string]$DbUser = "telesrv",

    [string]$ServerLogPath,
    [string]$StatePath,
    [string]$FixtureDir,
    [string]$VideoFixture,
    [string]$BlobDir,
    [string]$RemoteMovieDir = "/sdcard/Movies/telesrv",

    [switch]$SkipAdb,
    [switch]$AllowMissingThumb
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$RepoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot ".."))
if (-not $ServerLogPath) {
    $latestLog = Get-ChildItem (Join-Path $RepoRoot "logs") -Filter "telesrv-*.err.log" -ErrorAction SilentlyContinue |
        Sort-Object LastWriteTime -Descending |
        Select-Object -First 1
    if ($latestLog) {
        $ServerLogPath = $latestLog.FullName
    } else {
        $ServerLogPath = Join-Path $RepoRoot "logs\telesrv.err.log"
    }
}
if (-not $StatePath) {
    $StatePath = Join-Path $RepoRoot "logs\android-video-upload-state.json"
}
if (-not $FixtureDir) {
    $FixtureDir = Join-Path $RepoRoot "logs\media-fixtures"
}
if (-not $BlobDir) {
    $BlobDir = Join-Path $RepoRoot "data\blobs"
}

$Failures = New-Object System.Collections.Generic.List[string]

function Write-Step {
    param([string]$Message)
    Write-Host ""
    Write-Host "== $Message =="
}

function Write-Ok {
    param([string]$Message)
    Write-Host "[ok] $Message"
}

function Write-Warn {
    param([string]$Message)
    Write-Host "[warn] $Message"
}

function Add-Failure {
    param([string]$Message)
    $script:Failures.Add($Message) | Out-Null
    Write-Host "[fail] $Message"
}

function Assert-Check {
    param(
        [bool]$Condition,
        [string]$Message
    )
    if ($Condition) {
        Write-Ok $Message
    } else {
        Add-Failure $Message
    }
}

function Invoke-External {
    param(
        [string]$FilePath,
        [string[]]$Arguments,
        [switch]$AllowFailure
    )
    $oldErrorActionPreference = $ErrorActionPreference
    $ErrorActionPreference = "Continue"
    try {
        $output = & $FilePath @Arguments 2>&1
        $exitCode = $LASTEXITCODE
    } finally {
        $ErrorActionPreference = $oldErrorActionPreference
    }
    $text = ($output | ForEach-Object { $_.ToString() }) -join "`n"
    if ($exitCode -ne 0 -and -not $AllowFailure) {
        throw "$FilePath $($Arguments -join ' ') failed with exit code ${exitCode}:`n$text"
    }
    [pscustomobject]@{
        ExitCode = $exitCode
        Output = $text
    }
}

function Assert-Command {
    param([string]$Name)
    $cmd = Get-Command $Name -ErrorAction SilentlyContinue
    Assert-Check ([bool]$cmd) "$Name is available"
    return [bool]$cmd
}

function Get-AdbArgs {
    param([string[]]$Arguments)
    if ($DeviceSerial) {
        return @("-s", $DeviceSerial) + $Arguments
    }
    return $Arguments
}

function Invoke-Adb {
    param([string[]]$Arguments, [switch]$AllowFailure)
    Invoke-External "adb" (Get-AdbArgs $Arguments) -AllowFailure:$AllowFailure
}

function Invoke-PsqlRows {
    param([string]$Sql)
    $args = @(
        "exec", $PostgresContainer,
        "psql", "-U", $DbUser, "-d", $Database,
        "-v", "ON_ERROR_STOP=1",
        "-At", "-F", "|",
        "-c", $Sql
    )
    $result = Invoke-External "docker" $args
    if ([string]::IsNullOrWhiteSpace($result.Output)) {
        return @()
    }
    return @($result.Output -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
}

function Get-LogLineCount {
    if (-not (Test-Path -LiteralPath $ServerLogPath)) {
        return 0
    }
    return @(Read-SharedLogLines).Count
}

function Get-LogLinesSince {
    param([int]$Skip)
    if (-not (Test-Path -LiteralPath $ServerLogPath)) {
        Add-Failure "server log exists at $ServerLogPath"
        return @()
    }
    return @(Read-SharedLogLines | Select-Object -Skip $Skip)
}

function Read-SharedLogLines {
    $stream = [System.IO.File]::Open($ServerLogPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
    try {
        $reader = New-Object System.IO.StreamReader($stream)
        try {
            $lines = New-Object System.Collections.Generic.List[string]
            while (-not $reader.EndOfStream) {
                $lines.Add($reader.ReadLine()) | Out-Null
            }
            return $lines
        } finally {
            $reader.Dispose()
        }
    } finally {
        $stream.Dispose()
    }
}

function Get-PrivateMessageMaxId {
    $rows = @(Invoke-PsqlRows @"
SELECT COALESCE(MAX(id), 0)
FROM private_messages
WHERE (sender_user_id = $SenderUserId AND recipient_user_id = $RecipientUserId)
   OR (sender_user_id = $RecipientUserId AND recipient_user_id = $SenderUserId);
"@)
    if ($rows.Count -eq 0) {
        return 0
    }
    return [long]$rows[0]
}

function Get-UploadPartUsage {
    $rows = @(Invoke-PsqlRows @"
SELECT COUNT(*), COALESCE(SUM(size), 0)
FROM upload_parts
WHERE owner_user_id = $SenderUserId;
"@)
    if ($rows.Count -eq 0) {
        return [pscustomobject]@{ Parts = 0; Bytes = 0L }
    }
    $parts = $rows[0] -split "\|"
    return [pscustomobject]@{
        Parts = [int]$parts[0]
        Bytes = [long]$parts[1]
    }
}

function Get-NewVideoMessages {
    param([long]$AfterMessageId)
    $rows = @(Invoke-PsqlRows @"
SELECT
  id,
  sender_user_id,
  recipient_user_id,
  COALESCE(media->>'kind', ''),
  COALESCE(media->>'video', 'false'),
  COALESCE(media->'document'->>'id', '0'),
  COALESCE(media->'document'->>'mime_type', ''),
  COALESCE(media->'document'->>'size', '0'),
  COALESCE(jsonb_array_length(COALESCE(media->'document'->'thumbs', '[]'::jsonb)), 0)
FROM private_messages
WHERE id > $AfterMessageId
  AND sender_user_id = $SenderUserId
  AND recipient_user_id = $RecipientUserId
ORDER BY id;
"@)
    $items = @()
    foreach ($row in $rows) {
        $parts = $row -split "\|", 9
        $items += [pscustomobject]@{
            MessageId = [long]$parts[0]
            SenderUserId = [long]$parts[1]
            RecipientUserId = [long]$parts[2]
            Kind = $parts[3]
            Video = $parts[4]
            DocumentId = [long]$parts[5]
            MimeType = $parts[6]
            Size = [long]$parts[7]
            ThumbCount = [int]$parts[8]
        }
    }
    return $items
}

function Get-DocumentRows {
    param([long[]]$DocumentIds)
    if ($DocumentIds.Count -eq 0) {
        return @()
    }
    $ids = ($DocumentIds | ForEach-Object { $_.ToString() }) -join ","
    $rows = @(Invoke-PsqlRows @"
SELECT id, mime_type, size, jsonb_array_length(COALESCE(thumbs, '[]'::jsonb))
FROM documents
WHERE id IN ($ids)
ORDER BY id;
"@)
    $items = @()
    foreach ($row in $rows) {
        $parts = $row -split "\|", 4
        $items += [pscustomobject]@{
            DocumentId = [long]$parts[0]
            MimeType = $parts[1]
            Size = [long]$parts[2]
            ThumbCount = [int]$parts[3]
        }
    }
    return $items
}

function Get-FileBlobRows {
    param([long[]]$DocumentIds)
    if ($DocumentIds.Count -eq 0) {
        return @()
    }
    $keys = @()
    foreach ($docId in $DocumentIds) {
        $keys += "doc:$docId"
        $keys += "doc:${docId}:m"
    }
    $quoted = ($keys | ForEach-Object { "'" + $_.Replace("'", "''") + "'" }) -join ","
    $rows = @(Invoke-PsqlRows @"
SELECT location_key, backend, object_key, size, mime_type
FROM file_blobs
WHERE location_key IN ($quoted)
ORDER BY location_key;
"@)
    $items = @()
    foreach ($row in $rows) {
        $parts = $row -split "\|", 5
        $items += [pscustomobject]@{
            LocationKey = $parts[0]
            Backend = $parts[1]
            ObjectKey = $parts[2]
            Size = [long]$parts[3]
            MimeType = $parts[4]
        }
    }
    return $items
}

function Wait-FileBlobRows {
    param(
        [long[]]$DocumentIds,
        [int]$TimeoutSeconds = 10
    )
    $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
    $last = @()
    while ($true) {
        $last = @(Get-FileBlobRows $DocumentIds)
        $hasBody = @($last | Where-Object { $_.LocationKey -like "doc:*" -and $_.LocationKey -notlike "*:m" -and $_.Size -gt 0 }).Count -ge 1
        $hasThumb = $AllowMissingThumb -or (@($last | Where-Object { $_.LocationKey -like "doc:*:m" -and $_.Size -gt 0 }).Count -ge 1)
        if ($hasBody -and $hasThumb) {
            return $last
        }
        if ((Get-Date) -ge $deadline) {
            return $last
        }
        Start-Sleep -Milliseconds 500
    }
}

function Get-EffectiveLogSkip {
    param([pscustomobject]$State)
    $stateLog = ""
    if ($State.PSObject.Properties.Name -contains "ServerLogPath") {
        $stateLog = [string]$State.ServerLogPath
    }
    if ($stateLog) {
        $stateFull = [System.IO.Path]::GetFullPath($stateLog)
        $currentFull = [System.IO.Path]::GetFullPath($ServerLogPath)
        if ($stateFull.Equals($currentFull, [System.StringComparison]::OrdinalIgnoreCase)) {
            return [int]$State.BaselineLogLineCount
        }
        Write-Warn "server log changed since baseline; scanning current log from the beginning"
        return 0
    }
    return [int]$State.BaselineLogLineCount
}

function Get-BlobFilePath {
    param([string]$ObjectKey)
    if ($ObjectKey.Length -lt 4) {
        return Join-Path $BlobDir $ObjectKey
    }
    return Join-Path (Join-Path (Join-Path $BlobDir $ObjectKey.Substring(0, 2)) $ObjectKey.Substring(2, 2)) $ObjectKey
}

function Save-State {
    param([pscustomobject]$State)
    $dir = Split-Path -Parent $StatePath
    if ($dir) {
        New-Item -ItemType Directory -Force -Path $dir | Out-Null
    }
    $State | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $StatePath -Encoding UTF8
    Write-Ok "state saved to $StatePath"
}

function Load-State {
    if (-not (Test-Path -LiteralPath $StatePath)) {
        throw "state file not found: $StatePath. Run -Phase BeforeSend first."
    }
    Get-Content -LiteralPath $StatePath -Raw | ConvertFrom-Json
}

function New-VideoFixture {
    param([string]$Path)
    $ffmpeg = Get-Command ffmpeg -ErrorAction SilentlyContinue
    if (-not $ffmpeg) {
        Add-Failure "ffmpeg is available or -VideoFixture is supplied"
        return
    }
    Invoke-External "ffmpeg" @(
        "-y",
        "-f", "lavfi",
        "-i", "testsrc=size=568x1280:rate=30",
        "-f", "lavfi",
        "-i", "sine=frequency=880:sample_rate=44100",
        "-t", "3",
        "-pix_fmt", "yuv420p",
        "-c:v", "libx264",
        "-c:a", "aac",
        "-shortest",
        $Path
    ) | Out-Null
}

function Ensure-VideoFixture {
    New-Item -ItemType Directory -Force -Path $FixtureDir | Out-Null
    if (-not $VideoFixture) {
        $stamp = Get-Date -Format "yyyyMMdd-HHmmss"
        $script:VideoFixture = Join-Path $FixtureDir "telesrv-android-video-upload-$stamp.mp4"
        New-VideoFixture $script:VideoFixture
    }
    Assert-Check ($VideoFixture -and (Test-Path -LiteralPath $VideoFixture)) "video fixture exists: $VideoFixture"
}

function Push-VideoFixture {
    if ($SkipAdb) {
        Write-Warn "adb push skipped"
        return
    }
    Invoke-Adb @("shell", "mkdir", "-p", $RemoteMovieDir) | Out-Null
    Invoke-Adb @("push", $VideoFixture, "$RemoteMovieDir/") | Out-Null
    $leaf = Split-Path -Leaf $VideoFixture
    Invoke-Adb @("shell", "am", "broadcast", "-a", "android.intent.action.MEDIA_SCANNER_SCAN_FILE", "-d", "file://$RemoteMovieDir/$leaf") | Out-Null
    Write-Ok "video pushed to Android: $RemoteMovieDir/$leaf"
}

function Run-Preflight {
    Write-Step "Preflight"
    if (-not $SkipAdb) {
        if (Assert-Command "adb") {
            $devices = Invoke-Adb @("devices")
            $deviceLines = @($devices.Output -split "`r?`n" | Where-Object { $_ -match "\tdevice$" })
            Assert-Check ($deviceLines.Count -ge 1) "adb has at least one connected device"
            Assert-Check (($deviceLines.Count -eq 1) -or [bool]$DeviceSerial) "adb selects a single device or -DeviceSerial is set"
            if (($deviceLines.Count -eq 1) -or [bool]$DeviceSerial) {
                $pkg = Invoke-Adb @("shell", "dumpsys", "package", $AndroidPackage)
                Assert-Check ($pkg.Output -match "versionName=") "Android package $AndroidPackage is installed"
                $model = (Invoke-Adb @("shell", "getprop", "ro.product.model")).Output.Trim()
                $sdk = (Invoke-Adb @("shell", "getprop", "ro.build.version.sdk")).Output.Trim()
                Write-Host "Android device: model=$model sdk=$sdk"
            }
        }
    } else {
        Write-Warn "adb checks skipped"
    }
    if (Assert-Command "docker") {
        Invoke-PsqlRows "SELECT 1;" | Out-Null
        Write-Ok "PostgreSQL is reachable through docker container $PostgresContainer"
    }
    Assert-Check (Test-Path -LiteralPath $ServerLogPath) "server log exists: $ServerLogPath"
    Write-Host "server log: $ServerLogPath"
}

function Run-Prepare {
    Write-Step "Prepare video fixture"
    Run-Preflight
    Ensure-VideoFixture
    Push-VideoFixture
    Write-Host "Manual step: send this video from Android/Alice to Bob:"
    Write-Host "  $VideoFixture"
}

function Run-BeforeSend {
    Write-Step "BeforeSend snapshot"
    $maxMessageId = Get-PrivateMessageMaxId
    $usage = Get-UploadPartUsage
    $lineCount = Get-LogLineCount
    Write-Host "private_messages max id before send: $maxMessageId"
    Write-Host "upload_parts before send: parts=$($usage.Parts) bytes=$($usage.Bytes)"
    $state = [pscustomobject]@{
        SenderUserId = $SenderUserId
        RecipientUserId = $RecipientUserId
        BaselinePrivateMessageId = $maxMessageId
        BaselineUploadParts = $usage.Parts
        BaselineUploadPartBytes = $usage.Bytes
        BaselineLogLineCount = $lineCount
        ServerLogPath = $ServerLogPath
        BlobDir = $BlobDir
        CreatedAt = (Get-Date -Format o)
        VideoFixture = $VideoFixture
    }
    Save-State $state
}

function Run-AfterSend {
    Write-Step "AfterSend assertions"
    $state = Load-State
    $messages = @(Get-NewVideoMessages ([long]$state.BaselinePrivateMessageId))
    foreach ($message in $messages) {
        Write-Host ("new private message id={0} kind={1} video={2} doc={3} mime={4} size={5} thumbs={6}" -f $message.MessageId, $message.Kind, $message.Video, $message.DocumentId, $message.MimeType, $message.Size, $message.ThumbCount)
    }
    $videos = @($messages | Where-Object {
        $_.Kind -eq "document" -and $_.Video -eq "true" -and $_.MimeType -eq "video/mp4" -and $_.DocumentId -gt 0
    })
    Assert-Check ($videos.Count -ge 1) "new private message includes uploaded video/mp4 document"

    $docIds = @($videos | Select-Object -ExpandProperty DocumentId -Unique)
    $documents = @(Get-DocumentRows $docIds)
    $blobs = @(Wait-FileBlobRows $docIds)
    foreach ($doc in $documents) {
        Write-Host ("document id={0} mime={1} size={2} thumbs={3}" -f $doc.DocumentId, $doc.MimeType, $doc.Size, $doc.ThumbCount)
    }
    foreach ($blob in $blobs) {
        Write-Host ("blob key={0} backend={1} object={2} size={3} mime={4}" -f $blob.LocationKey, $blob.Backend, $blob.ObjectKey, $blob.Size, $blob.MimeType)
    }

    Assert-Check ($documents.Count -ge $docIds.Count) "documents rows exist for uploaded video"
    Assert-Check (@($documents | Where-Object { $_.MimeType -eq "video/mp4" -and $_.Size -gt 0 }).Count -ge 1) "uploaded video document metadata is persisted"
    if (-not $AllowMissingThumb) {
        Assert-Check (@($documents | Where-Object { $_.ThumbCount -gt 0 }).Count -ge 1) "uploaded video document has thumbnail metadata"
    }
    Assert-Check (@($blobs | Where-Object { $_.LocationKey -like "doc:*" -and $_.LocationKey -notlike "*:m" -and $_.MimeType -eq "video/mp4" -and $_.Size -gt 0 }).Count -ge 1) "video body file_blobs row exists"
    if (-not $AllowMissingThumb) {
        Assert-Check (@($blobs | Where-Object { $_.LocationKey -like "doc:*:m" -and $_.Size -gt 0 }).Count -ge 1) "video thumbnail file_blobs row exists"
    }
    foreach ($blob in $blobs) {
        if ($blob.Backend -eq "localfs" -and $blob.ObjectKey) {
            $path = Get-BlobFilePath $blob.ObjectKey
            Assert-Check (Test-Path -LiteralPath $path) "localfs blob exists: $($blob.LocationKey)"
        }
    }

    $usage = Get-UploadPartUsage
    Write-Host "upload_parts after send: parts=$($usage.Parts) bytes=$($usage.Bytes)"
    if ([int]$state.BaselineUploadParts -eq 0) {
        Assert-Check ($usage.Parts -eq 0) "upload_parts cleaned after successful upload"
    } else {
        Assert-Check ($usage.Parts -le [int]$state.BaselineUploadParts) "upload_parts did not grow after successful upload"
    }

    $lines = @(Get-LogLinesSince (Get-EffectiveLogSkip $state))
    $savePartHits = @($lines | Where-Object {
        ($_ -like "*upload.saveFilePart*" -or $_ -like "*upload.saveBigFilePart*") -and $_ -like '*client_type": "android"*'
    })
    $sendMediaHits = @($lines | Where-Object {
        $_ -like "*messages.sendMedia*" -and $_ -like '*client_type": "android"*'
    })
    $bad = @($lines | Where-Object {
        $_ -cmatch "INTERNAL_SERVER_ERROR|rpc error|Unhandled RPC|NOT_IMPLEMENTED|bad_msg|panic|\tERROR\t"
    })
    Assert-Check ($savePartHits.Count -ge 1) "server log has Android upload.saveFilePart/saveBigFilePart"
    Assert-Check ($sendMediaHits.Count -ge 1) "server log has Android messages.sendMedia"
    Assert-Check ($bad.Count -eq 0) "server log has no upload-era internal errors or unhandled RPCs"

    if (-not $SkipAdb) {
        $logcat = Invoke-Adb @("logcat", "-d", "-t", "1200") -AllowFailure
        if ($logcat.ExitCode -eq 0) {
            $androidErrors = @($logcat.Output -split "`r?`n" | Where-Object {
                $_ -match "INTERNAL_SERVER_ERROR|rpc error 500|saveFilePart|saveBigFilePart|FileUploadOperation"
            })
            if ($androidErrors.Count -gt 0) {
                Write-Host "Recent Android upload log lines:"
                $androidErrors | Select-Object -Last 40 | ForEach-Object { Write-Host $_ }
            }
            $fatalAndroidErrors = @($androidErrors | Where-Object { $_ -match "INTERNAL_SERVER_ERROR|rpc error 500" })
            Assert-Check ($fatalAndroidErrors.Count -eq 0) "recent Android logcat has no upload 500"
        } else {
            Write-Warn "adb logcat scan failed: $($logcat.Output)"
        }
    }
}

function Finish-Run {
    if ($Failures.Count -gt 0) {
        Write-Host ""
        Write-Host "Validation failed:"
        foreach ($failure in $Failures) {
            Write-Host " - $failure"
        }
        exit 1
    }
    Write-Host ""
    Write-Host "Validation passed."
}

switch ($Phase) {
    "Preflight" { Run-Preflight }
    "Prepare" { Run-Prepare }
    "BeforeSend" { Run-BeforeSend }
    "AfterSend" { Run-AfterSend }
    "All" {
        Run-Prepare
        Run-BeforeSend
        Read-Host "Send the prepared video from Android/Alice to Bob, then press Enter"
        Run-AfterSend
    }
}

Finish-Run