mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
Launcher (#4)
* 激进的更新 * 试试 * fix.可爱的我一直在修CI( * fix.启动器一定要能够启动 * feat.尝试弄了AOT的启动器。 * fix.修CI,好像是因为Linux那边有个问题,反正修就对了。 * fix.ci难修,为什么liunx跑不起来呢? * Update build.yml * Update LanMountainDesktop.csproj * changed.调整了启动逻辑,优化了更新页面。 * changed.优化了更新体验 * feat.依旧试增量更新这一块,看看velopack * fix.我们试验性地修复了启动器无法正常启动的问题,原因可能是这个画面没有启动,就GUI没显示。然后还把编译问题修了一下。 * fix.继续修ci,ci怎么天天炸 * changed.velopack,试试rust * fix.修ci,修融合桌面,修启动器 * fix.GitHub Action工作流怎么天天出问题 * feat.引入velopack,不好,是rust(至少内存很安全了。 * chore: migrate release pipeline to signed filemap and wire rainyun s3 * fix: make optional s3 upload step workflow-parse safe * fix: make delta pack generation robust for empty diffs and linux paths * chore: rotate launcher update public key for pdc signing * fix: restore stable launcher update public key * fix: sync launcher public key with update signing secret * fix: normalize PEM line endings in signing key validation * fix: rotate launcher public key to match ci signing secret * fix: compare signing keys by SPKI instead of PEM text * refactor update backend to host-managed PDC pipeline * fix release workflow env key collisions * relax publish-pdc precheck to require S3 only * set GH_TOKEN for PDCC installer step * ci: add local pdc mock fallback for release publish * ci: fix pdc mock process log redirection * ci: fallback pdcc signing key to update private key * ci: ensure pdcc signing passphrase env is always set * ci: create pdcc publish root before invoking client * ci: set pdcc version variable from release version * ci: decouple pdcc installer version from publish config version * ci: package pdcc subchannels with generated filemap and changelog * ci: make local pdc mock diff return empty for fast fallback * ci: fix pdcc variable mapping and pdc signing prechecks * Update App.axaml.cs * ci: wire aws cli credentials for rainyun s3 * ci: pin pdcc client version separately from app version * ci: harden local pdc mock transport handling * ci: publish pdcc subchannels in one pass * ci: add pdcc publish heartbeat and timeout * ci: fix pdcc publish workdir bootstrap * feat.Penguin Logistics Online Network Distribution System * ci: fix plonds s3 probe and signing fallback * ci: validate signing key and quiet missing baselines * ci: relax aws checksum mode for rainyun s3 * ci: avoid multipart uploads to rainyun s3 * ci: handle empty plonds baselines safely * ci.plonds * Rebuild release pipeline around PLONDS and DDSS * Fix Windows installer script path in release workflow
This commit is contained in:
229
scripts/Generate-DeltaPackage.ps1
Normal file
229
scripts/Generate-DeltaPackage.ps1
Normal file
@@ -0,0 +1,229 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PreviousVersion,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CurrentVersion,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PreviousDir,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CurrentDir,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputDir
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
|
||||
function Get-NormalizedRelativePath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RootDir,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FullPath
|
||||
)
|
||||
|
||||
$separator = [System.IO.Path]::DirectorySeparatorChar
|
||||
$altSeparator = [System.IO.Path]::AltDirectorySeparatorChar
|
||||
|
||||
$root = [System.IO.Path]::GetFullPath($RootDir).Replace($altSeparator, $separator).TrimEnd($separator)
|
||||
$path = [System.IO.Path]::GetFullPath($FullPath).Replace($altSeparator, $separator)
|
||||
|
||||
$comparison = if ($separator -eq '\') {
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
}
|
||||
else {
|
||||
[System.StringComparison]::Ordinal
|
||||
}
|
||||
|
||||
$rootWithSeparator = "$root$separator"
|
||||
if ($path.StartsWith($rootWithSeparator, $comparison)) {
|
||||
$relative = $path.Substring($rootWithSeparator.Length)
|
||||
}
|
||||
elseif ($path.Equals($root, $comparison)) {
|
||||
$relative = ""
|
||||
}
|
||||
else {
|
||||
throw "File path '$path' is not under root '$root'."
|
||||
}
|
||||
|
||||
return $relative.Replace('\', '/')
|
||||
}
|
||||
|
||||
function Get-FileSha256Hex {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Get-FileManifest {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RootDir
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $RootDir)) {
|
||||
throw "Directory does not exist: $RootDir"
|
||||
}
|
||||
|
||||
$resolvedRoot = (Resolve-Path -LiteralPath $RootDir).Path
|
||||
$manifest = @{}
|
||||
$files = Get-ChildItem -LiteralPath $resolvedRoot -Recurse -File
|
||||
|
||||
foreach ($file in $files) {
|
||||
$relativePath = Get-NormalizedRelativePath -RootDir $resolvedRoot -FullPath $file.FullName
|
||||
$manifest[$relativePath] = [ordered]@{
|
||||
Path = $relativePath
|
||||
Sha256 = Get-FileSha256Hex -Path $file.FullName
|
||||
Size = [long]$file.Length
|
||||
}
|
||||
}
|
||||
|
||||
return $manifest
|
||||
}
|
||||
|
||||
function New-DeltaArchive {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ZipPath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CurrentRoot,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[AllowEmptyCollection()]
|
||||
[object[]]$ChangedFiles = @()
|
||||
)
|
||||
|
||||
if (Test-Path -LiteralPath $ZipPath) {
|
||||
Remove-Item -LiteralPath $ZipPath -Force
|
||||
}
|
||||
|
||||
$zip = [System.IO.Compression.ZipFile]::Open($ZipPath, [System.IO.Compression.ZipArchiveMode]::Create)
|
||||
try {
|
||||
foreach ($file in $ChangedFiles) {
|
||||
$sourcePath = Join-Path $CurrentRoot $file.Path
|
||||
if (-not (Test-Path -LiteralPath $sourcePath)) {
|
||||
throw "Changed file was not found while building archive: $sourcePath"
|
||||
}
|
||||
|
||||
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
|
||||
$zip,
|
||||
$sourcePath,
|
||||
$file.Path,
|
||||
[System.IO.Compression.CompressionLevel]::Optimal
|
||||
) | Out-Null
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$zip.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Generating incremental package..."
|
||||
Write-Host "From: $PreviousVersion"
|
||||
Write-Host "To: $CurrentVersion"
|
||||
Write-Host "Prev: $PreviousDir"
|
||||
Write-Host "Curr: $CurrentDir"
|
||||
Write-Host "Out: $OutputDir"
|
||||
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
$previousManifest = Get-FileManifest -RootDir $PreviousDir
|
||||
$currentManifest = Get-FileManifest -RootDir $CurrentDir
|
||||
|
||||
$changedFiles = @()
|
||||
$reusedFiles = @()
|
||||
$deletedFiles = @()
|
||||
|
||||
foreach ($path in ($currentManifest.Keys | Sort-Object)) {
|
||||
$currentFile = $currentManifest[$path]
|
||||
|
||||
if ($previousManifest.ContainsKey($path)) {
|
||||
$previousFile = $previousManifest[$path]
|
||||
if ($currentFile.Sha256 -eq $previousFile.Sha256) {
|
||||
$reusedFiles += [ordered]@{
|
||||
Path = $path
|
||||
Action = "reuse"
|
||||
Sha256 = $currentFile.Sha256
|
||||
Size = $currentFile.Size
|
||||
}
|
||||
}
|
||||
else {
|
||||
$changedFiles += [ordered]@{
|
||||
Path = $path
|
||||
Action = "replace"
|
||||
Sha256 = $currentFile.Sha256
|
||||
Size = $currentFile.Size
|
||||
ArchivePath = $path
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$changedFiles += [ordered]@{
|
||||
Path = $path
|
||||
Action = "add"
|
||||
Sha256 = $currentFile.Sha256
|
||||
Size = $currentFile.Size
|
||||
ArchivePath = $path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($path in ($previousManifest.Keys | Sort-Object)) {
|
||||
if (-not $currentManifest.ContainsKey($path)) {
|
||||
$deletedFiles += [ordered]@{
|
||||
Path = $path
|
||||
Action = "delete"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Changed: $($changedFiles.Count)"
|
||||
Write-Host "Reused: $($reusedFiles.Count)"
|
||||
Write-Host "Deleted: $($deletedFiles.Count)"
|
||||
|
||||
$resolvedCurrentDir = (Resolve-Path -LiteralPath $CurrentDir).Path
|
||||
$updateZipPath = Join-Path $OutputDir "update.zip"
|
||||
New-DeltaArchive -ZipPath $updateZipPath -CurrentRoot $resolvedCurrentDir -ChangedFiles $changedFiles
|
||||
|
||||
$deltaZipPath = Join-Path $OutputDir ("delta-{0}-to-{1}.zip" -f $PreviousVersion, $CurrentVersion)
|
||||
Copy-Item -LiteralPath $updateZipPath -Destination $deltaZipPath -Force
|
||||
|
||||
$allEntries = @($changedFiles + $reusedFiles + $deletedFiles)
|
||||
$filesJson = [ordered]@{
|
||||
FromVersion = $PreviousVersion
|
||||
ToVersion = $CurrentVersion
|
||||
GeneratedAt = [DateTimeOffset]::UtcNow.ToString("o")
|
||||
Files = $allEntries
|
||||
}
|
||||
|
||||
$jsonText = $filesJson | ConvertTo-Json -Depth 10
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
|
||||
$filesJsonPath = Join-Path $OutputDir "files.json"
|
||||
[System.IO.File]::WriteAllText($filesJsonPath, $jsonText, $utf8NoBom)
|
||||
|
||||
$versionedFilesJsonPath = Join-Path $OutputDir ("files-{0}.json" -f $CurrentVersion)
|
||||
Copy-Item -LiteralPath $filesJsonPath -Destination $versionedFilesJsonPath -Force
|
||||
|
||||
$updateSizeBytes = (Get-Item -LiteralPath $updateZipPath).Length
|
||||
$updateSizeMb = [Math]::Round($updateSizeBytes / 1MB, 2)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Done."
|
||||
Write-Host "update.zip size: $updateSizeMb MB"
|
||||
Write-Host "Generated:"
|
||||
Write-Host " $updateZipPath"
|
||||
Write-Host " $filesJsonPath"
|
||||
Write-Host " $deltaZipPath"
|
||||
Write-Host " $versionedFilesJsonPath"
|
||||
87
scripts/Generate-PlondsArtifacts.ps1
Normal file
87
scripts/Generate-PlondsArtifacts.ps1
Normal file
@@ -0,0 +1,87 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CurrentVersion,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CurrentDir,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Platform,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputDir,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$PreviousVersion = "",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$PreviousDir = "",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Channel = "stable",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$DistributionId = "",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$RepoBaseUrl = "",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$FileMapUrl = "",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$FileMapSignatureUrl = "",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$InstallerDirectory = "",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$InstallerBaseUrl = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$toolProject = Join-Path $PSScriptRoot "..\PenguinLogisticsOnlineNetworkDistributionSystem\src\Plonds.Tool\Plonds.Tool.csproj"
|
||||
if (-not (Test-Path -LiteralPath $toolProject)) {
|
||||
throw "PLONDS tool project not found: $toolProject"
|
||||
}
|
||||
|
||||
$arguments = @(
|
||||
"run",
|
||||
"--project", $toolProject,
|
||||
"--",
|
||||
"generate",
|
||||
"--current-version", $CurrentVersion,
|
||||
"--current-dir", $CurrentDir,
|
||||
"--platform", $Platform,
|
||||
"--output-dir", $OutputDir,
|
||||
"--previous-version", $(if ([string]::IsNullOrWhiteSpace($PreviousVersion)) { "0.0.0" } else { $PreviousVersion }),
|
||||
"--channel", $Channel
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($PreviousDir)) {
|
||||
$arguments += @("--previous-dir", $PreviousDir)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($DistributionId)) {
|
||||
$arguments += @("--distribution-id", $DistributionId)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($RepoBaseUrl)) {
|
||||
$arguments += @("--repo-base-url", $RepoBaseUrl)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($FileMapUrl)) {
|
||||
$arguments += @("--file-map-url", $FileMapUrl)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($FileMapSignatureUrl)) {
|
||||
$arguments += @("--file-map-signature-url", $FileMapSignatureUrl)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($InstallerDirectory)) {
|
||||
$arguments += @("--installer-directory", $InstallerDirectory)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($InstallerBaseUrl)) {
|
||||
$arguments += @("--installer-base-url", $InstallerBaseUrl)
|
||||
}
|
||||
|
||||
& dotnet @arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "PLONDS generate command failed."
|
||||
}
|
||||
28
scripts/Generate-VersionFile.ps1
Normal file
28
scripts/Generate-VersionFile.ps1
Normal file
@@ -0,0 +1,28 @@
|
||||
# 生成版本信息文件
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$OutputPath,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$Version,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Codename = "Administrate"
|
||||
)
|
||||
|
||||
$versionInfo = @{
|
||||
Version = $Version
|
||||
Codename = $Codename
|
||||
}
|
||||
|
||||
$json = $versionInfo | ConvertTo-Json -Compress
|
||||
$dir = Split-Path -Parent $OutputPath
|
||||
|
||||
if (!(Test-Path $dir)) {
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
}
|
||||
|
||||
Set-Content -Path $OutputPath -Value $json -Encoding UTF8
|
||||
Write-Host "Generated version file: $OutputPath" -ForegroundColor Green
|
||||
Write-Host " Version: $Version" -ForegroundColor Gray
|
||||
Write-Host " Codename: $Codename" -ForegroundColor Gray
|
||||
97
scripts/Install-Pdcc.ps1
Normal file
97
scripts/Install-Pdcc.ps1
Normal file
@@ -0,0 +1,97 @@
|
||||
param(
|
||||
[string]$Repository = "ClassIsland/PhainonDistributionCenter",
|
||||
[string]$AssetName = "out_app_linux_x64.zip",
|
||||
[string]$Version = "",
|
||||
[string]$OutputDir = (Join-Path $PSScriptRoot "..\pdcc")
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Repository)) {
|
||||
throw "Repository is required."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($AssetName)) {
|
||||
throw "AssetName is required."
|
||||
}
|
||||
|
||||
$OutputDir = [System.IO.Path]::GetFullPath($OutputDir)
|
||||
if (-not (Test-Path -LiteralPath $OutputDir)) {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$clientName = if ($env:OS -eq "Windows_NT") { "PhainonDistributionCenter.Client.exe" } else { "PhainonDistributionCenter.Client" }
|
||||
$clientPath = Join-Path $OutputDir $clientName
|
||||
if (Test-Path -LiteralPath $clientPath) {
|
||||
Write-Host "PDCC client already installed at $clientPath"
|
||||
return
|
||||
}
|
||||
|
||||
$releaseTag = $Version
|
||||
if ([string]::IsNullOrWhiteSpace($releaseTag)) {
|
||||
$releaseTag = $env:PDC_CLIENT_VERSION
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($releaseTag)) {
|
||||
$releaseTag = $env:PDCC_VERSION
|
||||
}
|
||||
|
||||
$tempDir = Join-Path $env:RUNNER_TEMP "pdcc-install"
|
||||
if (Test-Path -LiteralPath $tempDir) {
|
||||
Remove-Item -LiteralPath $tempDir -Recurse -Force
|
||||
}
|
||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||
|
||||
$zipPath = Join-Path $tempDir $AssetName
|
||||
|
||||
if (Get-Command gh -ErrorAction SilentlyContinue) {
|
||||
Write-Host "Downloading PDCC via gh release download from $Repository ..."
|
||||
$ghArgs = @("release", "download", "--repo", $Repository, "--pattern", $AssetName, "--dir", $tempDir, "--clobber")
|
||||
if (-not [string]::IsNullOrWhiteSpace($releaseTag)) {
|
||||
$ghArgs = @("release", "download", $releaseTag, "--repo", $Repository, "--pattern", $AssetName, "--dir", $tempDir, "--clobber")
|
||||
}
|
||||
|
||||
& gh @ghArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "gh release download failed for $Repository/$AssetName."
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ([string]::IsNullOrWhiteSpace($releaseTag)) {
|
||||
throw "PDCC_VERSION is required when gh is unavailable."
|
||||
}
|
||||
|
||||
$downloadUrl = "https://github.com/$Repository/releases/download/$releaseTag/$AssetName"
|
||||
Write-Host "Downloading PDCC from $downloadUrl ..."
|
||||
Invoke-WebRequest -Uri $downloadUrl -OutFile $zipPath
|
||||
}
|
||||
|
||||
$extractDir = Join-Path $tempDir "extract"
|
||||
if (Test-Path -LiteralPath $extractDir) {
|
||||
Remove-Item -LiteralPath $extractDir -Recurse -Force
|
||||
}
|
||||
New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
|
||||
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractDir -Force
|
||||
|
||||
$copied = $false
|
||||
foreach ($file in Get-ChildItem -LiteralPath $extractDir -Recurse -File) {
|
||||
if ($file.Name -ieq $clientName) {
|
||||
Copy-Item -LiteralPath $file.FullName -Destination $clientPath -Force
|
||||
$copied = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $copied) {
|
||||
throw "PDCC client executable not found in downloaded archive."
|
||||
}
|
||||
|
||||
if ($IsLinux) {
|
||||
try {
|
||||
chmod +x $clientPath | Out-Null
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "PDCC installed to $clientPath"
|
||||
59
scripts/Prepare-PdccOut.ps1
Normal file
59
scripts/Prepare-PdccOut.ps1
Normal file
@@ -0,0 +1,59 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourceDir,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputDir,
|
||||
|
||||
[string]$PlatformKey = "",
|
||||
|
||||
[string[]]$InstallerFiles = @()
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$SourceDir = [System.IO.Path]::GetFullPath($SourceDir)
|
||||
$OutputDir = [System.IO.Path]::GetFullPath($OutputDir)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $SourceDir)) {
|
||||
throw "Source directory not found: $SourceDir"
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $OutputDir) {
|
||||
Remove-Item -LiteralPath $OutputDir -Recurse -Force
|
||||
}
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
$payloadRoot = if ([string]::IsNullOrWhiteSpace($PlatformKey)) {
|
||||
$OutputDir
|
||||
} else {
|
||||
Join-Path $OutputDir $PlatformKey
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $payloadRoot -Force | Out-Null
|
||||
Get-ChildItem -LiteralPath $SourceDir -Force | ForEach-Object {
|
||||
Copy-Item -LiteralPath $_.FullName -Destination $payloadRoot -Recurse -Force
|
||||
}
|
||||
|
||||
if ($InstallerFiles.Count -gt 0) {
|
||||
$installerRoot = Join-Path $OutputDir "installers"
|
||||
if (-not (Test-Path -LiteralPath $installerRoot)) {
|
||||
New-Item -ItemType Directory -Path $installerRoot -Force | Out-Null
|
||||
}
|
||||
|
||||
foreach ($installer in $InstallerFiles) {
|
||||
if ([string]::IsNullOrWhiteSpace($installer)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$installerPath = [System.IO.Path]::GetFullPath($installer)
|
||||
if (-not (Test-Path -LiteralPath $installerPath)) {
|
||||
throw "Installer file not found: $installerPath"
|
||||
}
|
||||
|
||||
$targetPath = Join-Path $installerRoot ([System.IO.Path]::GetFileName($installerPath))
|
||||
Copy-Item -LiteralPath $installerPath -Destination $targetPath -Force
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Prepared PDCC staging directory: $payloadRoot"
|
||||
137
scripts/Publish-AOT.ps1
Normal file
137
scripts/Publish-AOT.ps1
Normal file
@@ -0,0 +1,137 @@
|
||||
# Launcher AOT 单文件发布脚本
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Configuration = "Release",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RuntimeIdentifier = "win-x64",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$OutputDir = "",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$SelfContained = $true,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$SingleFile = $true,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$Compress = $true
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# 设置默认输出目录
|
||||
if ([string]::IsNullOrWhiteSpace($OutputDir)) {
|
||||
$OutputDir = "..\publish\aot\$RuntimeIdentifier"
|
||||
}
|
||||
|
||||
$projectPath = "..\LanMountainDesktop.Launcher\LanMountainDesktop.Launcher.csproj"
|
||||
$absoluteOutputDir = Resolve-Path $OutputDir -ErrorAction SilentlyContinue
|
||||
if (-not $absoluteOutputDir) {
|
||||
$absoluteOutputDir = Join-Path (Get-Location) $OutputDir
|
||||
}
|
||||
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " Launcher AOT 单文件发布" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "配置信息:" -ForegroundColor Yellow
|
||||
Write-Host " 项目: $projectPath"
|
||||
Write-Host " 配置: $Configuration"
|
||||
Write-Host " 运行时: $RuntimeIdentifier"
|
||||
Write-Host " 输出目录: $absoluteOutputDir"
|
||||
Write-Host " 自包含: $SelfContained"
|
||||
Write-Host " 单文件: $SingleFile"
|
||||
Write-Host " 压缩: $Compress"
|
||||
Write-Host ""
|
||||
|
||||
# 清理输出目录
|
||||
if (Test-Path $absoluteOutputDir) {
|
||||
Write-Host "清理旧输出目录..." -ForegroundColor Yellow
|
||||
Remove-Item -Path $absoluteOutputDir -Recurse -Force
|
||||
}
|
||||
New-Item -ItemType Directory -Path $absoluteOutputDir -Force | Out-Null
|
||||
|
||||
# 构建发布参数
|
||||
$publishArgs = @(
|
||||
"publish",
|
||||
$projectPath,
|
||||
"-c", $Configuration,
|
||||
"-r", $RuntimeIdentifier,
|
||||
"-o", $absoluteOutputDir,
|
||||
"-p:PublishAot=true",
|
||||
"-p:PublishTrimmed=true",
|
||||
"-p:TrimMode=partial"
|
||||
)
|
||||
|
||||
if ($SelfContained) {
|
||||
$publishArgs += "--self-contained"
|
||||
}
|
||||
|
||||
if ($SingleFile) {
|
||||
$publishArgs += "-p:PublishSingleFile=true"
|
||||
$publishArgs += "-p:IncludeNativeLibrariesForSelfExtract=true"
|
||||
}
|
||||
|
||||
if ($Compress) {
|
||||
$publishArgs += "-p:EnableCompressionInSingleFile=true"
|
||||
}
|
||||
|
||||
Write-Host "开始发布..." -ForegroundColor Green
|
||||
Write-Host "命令: dotnet $([string]::Join(' ', $publishArgs))" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
try {
|
||||
& dotnet @publishArgs
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "发布失败,退出代码: $LASTEXITCODE"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host " 发布成功!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# 显示输出文件
|
||||
$outputFiles = Get-ChildItem -Path $absoluteOutputDir -File
|
||||
Write-Host "输出文件:" -ForegroundColor Yellow
|
||||
foreach ($file in $outputFiles) {
|
||||
$size = if ($file.Length -gt 1MB) {
|
||||
"{0:N2} MB" -f ($file.Length / 1MB)
|
||||
} else {
|
||||
"{0:N2} KB" -f ($file.Length / 1KB)
|
||||
}
|
||||
Write-Host " $($file.Name) - $size"
|
||||
}
|
||||
|
||||
# 验证单文件
|
||||
$exeFile = Get-ChildItem -Path $absoluteOutputDir -Filter "*.exe" | Select-Object -First 1
|
||||
if ($exeFile) {
|
||||
Write-Host ""
|
||||
Write-Host "可执行文件: $($exeFile.FullName)" -ForegroundColor Green
|
||||
|
||||
# 检查是否为单文件
|
||||
if ($SingleFile -and $outputFiles.Count -eq 1) {
|
||||
Write-Host "✓ 单文件发布成功!" -ForegroundColor Green
|
||||
} elseif ($SingleFile) {
|
||||
Write-Host "⚠ 警告: 发现 $($outputFiles.Count) 个文件,可能不是完全的单文件" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "使用说明:" -ForegroundColor Cyan
|
||||
Write-Host " 1. 将 $($exeFile.Name) 复制到目标机器"
|
||||
Write-Host " 2. 确保目录结构包含 app-* 文件夹"
|
||||
Write-Host " 3. 直接运行即可,无需安装 .NET Runtime"
|
||||
|
||||
} catch {
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Red
|
||||
Write-Host " 发布失败!" -ForegroundColor Red
|
||||
Write-Host "========================================" -ForegroundColor Red
|
||||
Write-Host "错误: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
1044
scripts/Publish-Plonds.ps1
Normal file
1044
scripts/Publish-Plonds.ps1
Normal file
File diff suppressed because it is too large
Load Diff
72
scripts/Setup-DevEnvironment.ps1
Normal file
72
scripts/Setup-DevEnvironment.ps1
Normal file
@@ -0,0 +1,72 @@
|
||||
# 开发环境设置脚本
|
||||
# 创建模拟的生产目录结构,方便测试 Launcher
|
||||
|
||||
param(
|
||||
[string]$Configuration = "Debug",
|
||||
[string]$Version = "1.0.0-dev"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# 获取项目根目录
|
||||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||
$LauncherOutput = Join-Path $ProjectRoot "LanMountainDesktop.Launcher\bin\$Configuration\net10.0"
|
||||
$MainAppOutput = Join-Path $ProjectRoot "LanMountainDesktop\bin\$Configuration\net10.0"
|
||||
$DevRoot = Join-Path $ProjectRoot "dev-test"
|
||||
|
||||
Write-Host "Setting up development environment..." -ForegroundColor Cyan
|
||||
Write-Host "Project Root: $ProjectRoot"
|
||||
Write-Host "Launcher Output: $LauncherOutput"
|
||||
Write-Host "Main App Output: $MainAppOutput"
|
||||
Write-Host "Dev Root: $DevRoot"
|
||||
Write-Host ""
|
||||
|
||||
# 检查主程序是否已构建
|
||||
if (-not (Test-Path (Join-Path $MainAppOutput "LanMountainDesktop.exe"))) {
|
||||
Write-Host "Main application not found. Building..." -ForegroundColor Yellow
|
||||
dotnet build "$ProjectRoot\LanMountainDesktop.slnx" -c $Configuration
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Build failed!"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# 清理旧的开发环境
|
||||
if (Test-Path $DevRoot) {
|
||||
Write-Host "Cleaning old dev environment..." -ForegroundColor Yellow
|
||||
Remove-Item -Path $DevRoot -Recurse -Force
|
||||
}
|
||||
|
||||
# 创建目录结构
|
||||
$AppDir = Join-Path $DevRoot "app-$Version"
|
||||
New-Item -ItemType Directory -Path $AppDir -Force | Out-Null
|
||||
|
||||
# 复制主程序到 app-{version} 目录
|
||||
Write-Host "Copying main application to app-$Version..." -ForegroundColor Green
|
||||
Copy-Item -Path "$MainAppOutput\*" -Destination $AppDir -Recurse -Force
|
||||
|
||||
# 创建 .current 标记文件
|
||||
New-Item -ItemType File -Path (Join-Path $AppDir ".current") -Force | Out-Null
|
||||
|
||||
# 复制 Launcher
|
||||
Write-Host "Copying Launcher..." -ForegroundColor Green
|
||||
Copy-Item -Path "$LauncherOutput\LanMountainDesktop.Launcher.exe" -Destination (Join-Path $DevRoot "LanMountainDesktop.exe") -Force
|
||||
|
||||
# 复制 Launcher 依赖
|
||||
$LauncherDeps = Get-ChildItem -Path $LauncherOutput -Filter "*.dll" -File
|
||||
foreach ($dep in $LauncherDeps) {
|
||||
Copy-Item -Path $dep.FullName -Destination $DevRoot -Force
|
||||
}
|
||||
|
||||
# 复制 Avalonia 主题文件
|
||||
$ThemeFiles = Get-ChildItem -Path $LauncherOutput -Filter "*.xaml" -File
|
||||
foreach ($theme in $ThemeFiles) {
|
||||
Copy-Item -Path $theme.FullName -Destination $DevRoot -Force
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Development environment setup complete!" -ForegroundColor Green
|
||||
Write-Host "Run the Launcher from: $DevRoot\LanMountainDesktop.exe" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Directory structure:" -ForegroundColor Gray
|
||||
Get-ChildItem -Path $DevRoot | Format-Table Name, @{Label="Type"; Expression={if($_.PSIsContainer){"Directory"}else{"File"}}}
|
||||
26
scripts/Sign-FileMap.ps1
Normal file
26
scripts/Sign-FileMap.ps1
Normal file
@@ -0,0 +1,26 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilesJsonPath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PrivateKeyPath,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$OutputPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
|
||||
$OutputPath = "$FilesJsonPath.sig"
|
||||
}
|
||||
|
||||
$toolProject = Join-Path $PSScriptRoot "..\PenguinLogisticsOnlineNetworkDistributionSystem\src\Plonds.Tool\Plonds.Tool.csproj"
|
||||
if (-not (Test-Path -LiteralPath $toolProject)) {
|
||||
throw "PLONDS tool project not found: $toolProject"
|
||||
}
|
||||
|
||||
& dotnet run --project $toolProject -- sign --manifest $FilesJsonPath --private-key $PrivateKeyPath --output $OutputPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "PLONDS sign command failed."
|
||||
}
|
||||
206
scripts/pdc-mock-server.py
Normal file
206
scripts/pdc-mock-server.py
Normal file
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _utc_now_text() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
class PdcMockHandler(BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
token = ""
|
||||
data_dir = Path(".")
|
||||
|
||||
def _write_json(self, status_code: int, payload: dict) -> None:
|
||||
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
self.send_response(status_code)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
self.wfile.flush()
|
||||
self.close_connection = True
|
||||
|
||||
def handle_expect_100(self) -> bool:
|
||||
self.send_response_only(100)
|
||||
self.end_headers()
|
||||
return True
|
||||
|
||||
def _read_chunked_body(self) -> bytes:
|
||||
chunks = bytearray()
|
||||
while True:
|
||||
size_line = self.rfile.readline()
|
||||
if not size_line:
|
||||
break
|
||||
|
||||
size_line = size_line.strip()
|
||||
if not size_line:
|
||||
continue
|
||||
|
||||
size_text = size_line.split(b";", 1)[0]
|
||||
chunk_size = int(size_text, 16)
|
||||
if chunk_size == 0:
|
||||
# Consume optional trailer headers until the terminating blank line.
|
||||
while True:
|
||||
trailer = self.rfile.readline()
|
||||
if trailer in (b"", b"\r\n", b"\n"):
|
||||
break
|
||||
break
|
||||
|
||||
remaining = chunk_size
|
||||
while remaining > 0:
|
||||
part = self.rfile.read(remaining)
|
||||
if not part:
|
||||
raise ConnectionError("unexpected end of stream while reading chunked request body")
|
||||
chunks.extend(part)
|
||||
remaining -= len(part)
|
||||
|
||||
chunk_terminator = self.rfile.read(2)
|
||||
if chunk_terminator == b"\r\n":
|
||||
continue
|
||||
if chunk_terminator[:1] != b"\n":
|
||||
raise ValueError("invalid chunk terminator")
|
||||
|
||||
return bytes(chunks)
|
||||
|
||||
def _read_request_body(self) -> bytes:
|
||||
transfer_encoding = (self.headers.get("Transfer-Encoding") or "").lower()
|
||||
if "chunked" in transfer_encoding:
|
||||
return self._read_chunked_body()
|
||||
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
if length <= 0:
|
||||
return b""
|
||||
return self.rfile.read(length)
|
||||
|
||||
def _read_json_body(self) -> tuple[dict, bytes]:
|
||||
raw = self._read_request_body()
|
||||
if not raw:
|
||||
return {}, raw
|
||||
try:
|
||||
return json.loads(raw.decode("utf-8")), raw
|
||||
except Exception:
|
||||
return {}, raw
|
||||
|
||||
def _save_payload(self, name: str, payload: dict, raw_body: bytes) -> None:
|
||||
out = self.data_dir / f"{name}.json"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"savedAtUtc": _utc_now_text(),
|
||||
"path": self.path,
|
||||
"method": self.command,
|
||||
"headers": {key: value for key, value in self.headers.items()},
|
||||
"rawBodyLength": len(raw_body),
|
||||
"rawBodyPreview": raw_body[:4096].decode("utf-8", errors="replace"),
|
||||
"payload": payload,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def _check_token(self) -> bool:
|
||||
expected = (self.token or "").strip()
|
||||
if not expected:
|
||||
return True
|
||||
provided = (self.headers.get("X-PDC-Token") or "").strip()
|
||||
return provided == expected
|
||||
|
||||
def do_GET(self) -> None:
|
||||
if self.path == "/healthz":
|
||||
self._write_json(200, {"ok": True, "timeUtc": _utc_now_text()})
|
||||
return
|
||||
|
||||
self._write_json(404, {"error": "not_found", "path": self.path})
|
||||
|
||||
def do_POST(self) -> None:
|
||||
print(
|
||||
f"[pdc-mock] {self.command} {self.path} "
|
||||
f"content-length={self.headers.get('Content-Length', '')} "
|
||||
f"transfer-encoding={self.headers.get('Transfer-Encoding', '')} "
|
||||
f"expect={self.headers.get('Expect', '')}"
|
||||
)
|
||||
|
||||
if not self._check_token():
|
||||
self._write_json(401, {"error": "unauthorized"})
|
||||
return
|
||||
|
||||
payload, raw_body = self._read_json_body()
|
||||
|
||||
if self.path == "/api/v1/fileMaps/diff":
|
||||
items = payload.get("items") if isinstance(payload, dict) else {}
|
||||
keys = sorted(items.keys()) if isinstance(items, dict) else []
|
||||
self._save_payload("filemaps-diff-request", payload, raw_body)
|
||||
# CI fallback mode: return empty diff to avoid long object uploads
|
||||
# against a local mock endpoint. Real PDC endpoint will return
|
||||
# actual missing object hashes.
|
||||
result = {
|
||||
"success": True,
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"content": [],
|
||||
"Content": [],
|
||||
"requestedCount": len(keys),
|
||||
}
|
||||
self._write_json(200, result)
|
||||
return
|
||||
|
||||
if self.path == "/api/v1/fileMaps/upload":
|
||||
self._save_payload("filemaps-upload-request", payload, raw_body)
|
||||
result = {
|
||||
"success": True,
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"content": True,
|
||||
"Content": True,
|
||||
}
|
||||
self._write_json(200, result)
|
||||
return
|
||||
|
||||
m = re.match(r"^/api/v1/distribution/([^/]+)/([^/]+)$", self.path)
|
||||
if m:
|
||||
primary_version = m.group(1)
|
||||
version = m.group(2)
|
||||
self._save_payload("distribution-request", payload, raw_body)
|
||||
result = {
|
||||
"success": True,
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
}
|
||||
self._write_json(200, result)
|
||||
return
|
||||
|
||||
self._write_json(404, {"error": "not_found", "path": self.path})
|
||||
|
||||
def log_message(self, fmt: str, *args) -> None:
|
||||
print(f"[pdc-mock] {self.address_string()} - {fmt % args}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="PDC mock server for CI fallback")
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=18765)
|
||||
parser.add_argument("--token", default="")
|
||||
parser.add_argument("--data-dir", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
PdcMockHandler.token = args.token
|
||||
PdcMockHandler.data_dir = Path(args.data_dir)
|
||||
PdcMockHandler.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
server = ThreadingHTTPServer((args.host, args.port), PdcMockHandler)
|
||||
print(f"[pdc-mock] listening on http://{args.host}:{args.port}")
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
27
scripts/update-private-key.pem
Normal file
27
scripts/update-private-key.pem
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAxPqgXsrnG8Re0kV4HBb+x61HQpjCahJoilzKvvlnXanuGtGx
|
||||
bjZTB+kMzmPUwyx8gt1fcaBNoKPwpwP0UZRWjvJDZQ++5ex7LGGw0YRWtJmeeigS
|
||||
17YI90vEfX3xQ5InJoBKnndsRy2a742chE6YwHGrJ4b107ZJ+zd26FmokQS47Uza
|
||||
y3gomsbQHdehwCdCiW1mh8YFDm0xny+PYoYZkGXiDOYY0nvg4yJ/BG2fQkkC5TNi
|
||||
zr0lYcE3RrMRcyJB7zU3jN1QnjHIvIvwfCOXaLdcXtxgQFRv45sYpmj9amNjuurM
|
||||
5iUa20Mk1ilYBuLxqe6P9C8DakZY/akVxpzxrQIDAQABAoIBAE9CETlTJz7S+txc
|
||||
u4GB9y5dGKlBUijgE1RpFeNV8zOK5pW//ka8cRhju5VoMfn+cnMto/PSbqnOjUyG
|
||||
mM4ig9msvVVyyns1djJbdIw5VbIBhfTdHwfQ5TasM/nSrTtlGX+ya1Pr9ZOGVCtD
|
||||
rdDG10vH8PhMo6l2VbpRjPTc7qi6qv22UBnmhfTxlqusuunIAmDPwimj2+J5+NX5
|
||||
yH9xJamHNglPnNPujNh1IcPovSnm9MJ+JtSIztPSmdQ43SI+NOa2dNN4iQFHULO9
|
||||
LtZvbGJxmexkbjo0SWkvQ2Iut4gRaBpH19a9HnhG3CExji/XjLEqVcQZ0uzoHSQn
|
||||
3fStFjkCgYEA3oQCdgnzTFimDT8GTsxqBEDVQiLHBjcOPplmghBJyULb/XHIOvcp
|
||||
+fSmxeT4mQE0N/AsTnlBnYhIx5ZVh8/wljmXllHt0uVRWF4BLoSGnA2wzhk0Jrgo
|
||||
a2N9PzR8bjMA31zRKy2+TwSOSKnR+Yn5zhpa3qwQ7RN70j/GeMmndo8CgYEA4p7d
|
||||
uVlxch2/LhyzyV2HMAY1rJWOJ37B9Ut2oGK0LWfgr88J2O3yJ3rhcQBx41aI5OIC
|
||||
sq8mLyG0GuQpGe0s5xgUnZSpvoPjKElwHQM4sLPLs8isQdrv97XfeswhPOKHHVRz
|
||||
bfiU4MtfwXnGfi5CT7muJcELXDrDhEX2UcPnkgMCgYBFOQQa/JV31swxqr2nnegN
|
||||
Uq4FWRRZVp9T0h0VsUODHQ2bFt6XmXSxke6f+c9sqfc4v7rI3ugOvesGTDpnecT6
|
||||
twf1d59o0HYx62yqsAfAXHH4a9bRhNDuN5ErLITZM3y9//4CVMSziFNLP6lW3Bme
|
||||
iIxkYVsSpdELY1O3F+TE+QKBgB+spMDrR3fzwGzphhd3AxYrSAU/QgczKFjom0P/
|
||||
h79w7W6lOXMgjuAFxMzOixyDU87p6AahhGzCATJhAX2mMMh8DSWZScBfHrjaytjD
|
||||
QoEwICCYw7rQpwmwWfQH4/1mjAwFabzNKcHhqxiXtK6eOJZ8FWMhgDz72af7P1pe
|
||||
T1eRAoGBANJpd6mSlq5cXgyWUqdFQ/0Zf/Y2Yh0fNzm+pNi6F4LVW20mp9Zqh46P
|
||||
+BN5UvdNgZ4DbNQVjTLWVQU24/wyOkLLKaR7E/Ozd/L7zQmm+28bGQO/x3s+EvZD
|
||||
+BIighRvesjIbXff9rjWKUsRzeCTS2x1tqQP6J7IKrlgKMV2zEYM
|
||||
-----END RSA PRIVATE KEY-----
|
||||
Reference in New Issue
Block a user