138 lines
5.1 KiB
PowerShell
138 lines
5.1 KiB
PowerShell
|
|
[CmdletBinding()]
|
||
|
|
param(
|
||
|
|
[string]$Bucket = 'yuedong-wechatapplet',
|
||
|
|
[string]$Prefix = 'static/wwwweb',
|
||
|
|
[string]$PublicOrigin = 'https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com',
|
||
|
|
[string]$ConfigFile = (Join-Path $HOME '.ossutilconfig'),
|
||
|
|
[string]$OssutilPath = '',
|
||
|
|
[switch]$SkipRemoteVerify
|
||
|
|
)
|
||
|
|
|
||
|
|
Set-StrictMode -Version Latest
|
||
|
|
$ErrorActionPreference = 'Stop'
|
||
|
|
|
||
|
|
$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||
|
|
$DistDir = Join-Path $ProjectRoot 'dist'
|
||
|
|
$DefaultOssutil = Join-Path $ProjectRoot '.tools\ossutil-2.3.0-windows-amd64\ossutil.exe'
|
||
|
|
|
||
|
|
if (-not $OssutilPath) {
|
||
|
|
$OssutilPath = $DefaultOssutil
|
||
|
|
}
|
||
|
|
|
||
|
|
if (-not (Test-Path -LiteralPath $OssutilPath -PathType Leaf)) {
|
||
|
|
throw "ossutil not found: $OssutilPath"
|
||
|
|
}
|
||
|
|
|
||
|
|
if (-not (Test-Path -LiteralPath $ConfigFile -PathType Leaf)) {
|
||
|
|
throw "OSS config not found: $ConfigFile"
|
||
|
|
}
|
||
|
|
|
||
|
|
if (-not (Get-Command 'npm.cmd' -ErrorAction SilentlyContinue)) {
|
||
|
|
throw 'npm.cmd was not found. Install Node.js and reopen PowerShell.'
|
||
|
|
}
|
||
|
|
|
||
|
|
$Timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||
|
|
$CleanOrigin = $PublicOrigin.TrimEnd('/')
|
||
|
|
$CleanPrefix = $Prefix.Trim('/')
|
||
|
|
$PublicBase = "$CleanOrigin/$CleanPrefix/$Timestamp/"
|
||
|
|
$OssTarget = "oss://$Bucket/$CleanPrefix/$Timestamp/"
|
||
|
|
|
||
|
|
Push-Location $ProjectRoot
|
||
|
|
try {
|
||
|
|
Write-Host "[1/5] Building production files" -ForegroundColor Cyan
|
||
|
|
& npm.cmd run build -- "--base=$PublicBase"
|
||
|
|
if ($LASTEXITCODE -ne 0) {
|
||
|
|
throw "Vite build failed with exit code $LASTEXITCODE"
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Host "[2/5] Rewriting and validating OSS asset paths" -ForegroundColor Cyan
|
||
|
|
& npm.cmd run rewrite:oss -- $PublicBase
|
||
|
|
if ($LASTEXITCODE -ne 0) {
|
||
|
|
throw "OSS path validation failed with exit code $LASTEXITCODE"
|
||
|
|
}
|
||
|
|
|
||
|
|
$IndexPath = Join-Path $DistDir 'index.html'
|
||
|
|
$IndexContent = [System.IO.File]::ReadAllText($IndexPath)
|
||
|
|
$DuplicatePrefix = $PublicBase.TrimEnd('/') + $PublicBase
|
||
|
|
|
||
|
|
if ($IndexContent.Contains($DuplicatePrefix)) {
|
||
|
|
throw 'The generated index.html contains a duplicated OSS prefix.'
|
||
|
|
}
|
||
|
|
|
||
|
|
if (-not $IndexContent.Contains($PublicBase)) {
|
||
|
|
throw 'The generated index.html does not reference the new OSS directory.'
|
||
|
|
}
|
||
|
|
|
||
|
|
$Files = Get-ChildItem -LiteralPath $DistDir -Recurse -File
|
||
|
|
$UploadFiles = @($Files | Where-Object { $_.Name -ne 'index.html' })
|
||
|
|
$UploadBytes = ($UploadFiles | Measure-Object Length -Sum).Sum
|
||
|
|
|
||
|
|
Write-Host "[3/5] Uploading $($UploadFiles.Count) static files" -ForegroundColor Cyan
|
||
|
|
Write-Host " Target: $OssTarget"
|
||
|
|
& $OssutilPath -c $ConfigFile cp "$DistDir\" $OssTarget -r --exclude 'index.html' -f -q --no-progress
|
||
|
|
if ($LASTEXITCODE -ne 0) {
|
||
|
|
throw "OSS upload failed with exit code $LASTEXITCODE"
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Host '[4/5] Confirming index.html was excluded' -ForegroundColor Cyan
|
||
|
|
$RemoteIndexUrl = "${PublicBase}index.html?verify=$Timestamp"
|
||
|
|
try {
|
||
|
|
$RemoteIndexResponse = Invoke-WebRequest -Uri $RemoteIndexUrl -Method Head -UseBasicParsing
|
||
|
|
if ($RemoteIndexResponse.StatusCode -eq 200) {
|
||
|
|
throw 'index.html exists in the new OSS directory, but it must remain on the web server.'
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
if (-not $_.Exception.Response) {
|
||
|
|
throw
|
||
|
|
}
|
||
|
|
|
||
|
|
$RemoteIndexStatus = [int]$_.Exception.Response.StatusCode
|
||
|
|
if ($RemoteIndexStatus -ne 404) {
|
||
|
|
throw "Unable to confirm that index.html was excluded. HTTP status: $RemoteIndexStatus"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (-not $SkipRemoteVerify) {
|
||
|
|
Write-Host '[5/5] Verifying uploaded JS, CSS, and image URLs' -ForegroundColor Cyan
|
||
|
|
$ScriptMatch = [regex]::Match($IndexContent, '<script[^>]+src="([^"]+)"')
|
||
|
|
$StyleMatch = [regex]::Match($IndexContent, '<link[^>]+rel="stylesheet"[^>]+href="([^"]+)"')
|
||
|
|
|
||
|
|
if (-not $ScriptMatch.Success -or -not $StyleMatch.Success) {
|
||
|
|
throw 'Could not locate generated JS or CSS URLs in index.html.'
|
||
|
|
}
|
||
|
|
|
||
|
|
$CacheBuster = "verify=$Timestamp"
|
||
|
|
$ScriptUrl = $ScriptMatch.Groups[1].Value
|
||
|
|
$StyleUrl = $StyleMatch.Groups[1].Value
|
||
|
|
$ScriptResponse = Invoke-WebRequest -Uri "${ScriptUrl}?$CacheBuster" -UseBasicParsing
|
||
|
|
$StyleResponse = Invoke-WebRequest -Uri "${StyleUrl}?$CacheBuster" -Method Head -UseBasicParsing
|
||
|
|
|
||
|
|
if ($ScriptResponse.StatusCode -ne 200 -or $StyleResponse.StatusCode -ne 200) {
|
||
|
|
throw 'The uploaded JS or CSS file is not publicly accessible.'
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($ScriptResponse.Content.Contains($DuplicatePrefix) -or
|
||
|
|
$ScriptResponse.Content -match 'https://[^\s"''`]+https://') {
|
||
|
|
throw 'The uploaded JS contains a duplicated URL.'
|
||
|
|
}
|
||
|
|
|
||
|
|
$SampleImage = "${PublicBase}site/assets/logo.png"
|
||
|
|
$ImageResponse = Invoke-WebRequest -Uri "${SampleImage}?$CacheBuster" -Method Head -UseBasicParsing
|
||
|
|
if ($ImageResponse.StatusCode -ne 200) {
|
||
|
|
throw 'The uploaded sample image is not publicly accessible.'
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
Write-Host '[5/5] Remote verification skipped by parameter' -ForegroundColor Yellow
|
||
|
|
}
|
||
|
|
|
||
|
|
$SizeMiB = [math]::Round($UploadBytes / 1MB, 2)
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host 'Deployment completed successfully.' -ForegroundColor Green
|
||
|
|
Write-Host "OSS directory : $OssTarget"
|
||
|
|
Write-Host "Public base : $PublicBase"
|
||
|
|
Write-Host "Uploaded : $($UploadFiles.Count) files / $SizeMiB MiB"
|
||
|
|
Write-Host "Server index : $IndexPath"
|
||
|
|
} finally {
|
||
|
|
Pop-Location
|
||
|
|
}
|