You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Txgy.EWS.Client/docs/VELOPACK_SUPABASE_AUTO_UPDA...

453 lines
12 KiB
Markdown

# Velopack + Supabase Storage 自动更新复用指南
本文档用于在桌面程序中复用“Velopack 生成安装包和更新包Supabase Storage 托管更新源”的自动更新流程。示例均使用占位符,不绑定任何具体项目。
## 方案概览
更新链路分为两部分:
1. 发布端使用 Velopack CLI 将程序发布目录打包成安装包、全量包、差分包和 `releases.win.json`
2. 上传端将这些产物放到 Supabase Storage 的公开桶目录中。
3. 客户端运行时只读取公开更新源 URL调用 Velopack 检查、下载并应用更新。
客户端不需要 Supabase key。Supabase key 只应出现在发布脚本、CI/CD 或受信任的维护机器中。
## Supabase 配置
### 1. 创建 Storage 桶
推荐使用一个专门存放发布包的公开 File bucket
| 配置项 | 推荐值 | 说明 |
| --- | --- | --- |
| Bucket name | `app-releases` | 可按组织统一命名 |
| Public bucket | `true` | 客户端需要匿名下载 `releases.win.json` 和安装包 |
| File size limit | 按安装包大小设置 | 例如 `512MB`、`1GB` 或更高 |
| Allowed MIME types | 留空或允许二进制 | Velopack 产物包含 `.exe`、`.nupkg`、`.json` 等 |
Dashboard 路径通常是:
```text
Supabase Dashboard -> Storage -> New bucket
```
也可以用 SQL 创建桶:
```sql
insert into storage.buckets (
id,
name,
public,
file_size_limit,
allowed_mime_types
)
values (
'app-releases',
'app-releases',
true,
1073741824,
null
)
on conflict (id) do update
set
public = excluded.public,
file_size_limit = excluded.file_size_limit,
allowed_mime_types = excluded.allowed_mime_types;
```
### 2. 目录规划
建议每个应用、平台和发布通道使用独立目录:
```text
app-releases/
my-app/
windows/
stable/
releases.win.json
RELEASES
MyApp-win-Setup.exe
MyApp-1.2.3-full.nupkg
MyApp-1.2.3-delta.nupkg
beta/
releases.win.json
...
```
对应的 Velopack 更新源根地址为:
```text
https://<project-ref>.supabase.co/storage/v1/object/public/app-releases/my-app/windows/stable
```
Velopack 会在该根地址下读取 `releases.win.json`,所以不要把 `releases.win.json` 再放到额外子目录里。
### 3. 权限策略
推荐做法:
- 桶设为 public只开放匿名读取。
- 发布上传使用 `service_role` key并且只在 CI/CD 或维护机器环境变量中保存。
- 不给 `anon` 角色配置 `INSERT`、`UPDATE`、`DELETE` 权限。
- 不在客户端配置、日志、提交信息或文档中写入真实 key。
Supabase 的 public bucket 只代表对象可以通过公开 URL 读取;上传、覆盖、删除仍受访问控制影响。使用 `service_role` key 上传时会绕过 RLS适合发布流程。
如果必须用非 service role 的登录用户上传,需要在 `storage.objects` 上创建受限策略。下面示例只允许已登录用户写入指定目录,实际项目应再结合用户、角色或后端校验收紧权限:
```sql
create policy "release upload insert"
on storage.objects
for insert
to authenticated
with check (
bucket_id = 'app-releases'
and (storage.foldername(name))[1] = 'my-app'
);
create policy "release upload select"
on storage.objects
for select
to authenticated
using (
bucket_id = 'app-releases'
and (storage.foldername(name))[1] = 'my-app'
);
create policy "release upload update"
on storage.objects
for update
to authenticated
using (
bucket_id = 'app-releases'
and (storage.foldername(name))[1] = 'my-app'
)
with check (
bucket_id = 'app-releases'
and (storage.foldername(name))[1] = 'my-app'
);
```
覆盖已有文件需要 `SELECT``UPDATE` 权限。发布流程更推荐使用 service role而不是给普通客户端发放写权限。
## 应用配置文件
建议在应用中放一个独立配置文件,例如 `Config/update_config.json`
```json
{
"enable_auto_update": true,
"update_server_type": "supabase",
"update_server_url": "https://<project-ref>.supabase.co/storage/v1/object/public/app-releases/my-app/windows/stable",
"update_check_interval_hours": 24,
"silent_install": false,
"supabase": {
"project_url": "https://<project-ref>.supabase.co",
"bucket_name": "app-releases",
"directory": "my-app/windows/stable"
}
}
```
字段说明:
| 字段 | 用途 |
| --- | --- |
| `enable_auto_update` | 是否启用自动更新 |
| `update_server_type` | 可固定为 `supabase`,用于程序内部区分更新源 |
| `update_server_url` | 客户端 Velopack 更新源根地址 |
| `update_check_interval_hours` | 自动检查间隔;手动检查可忽略该间隔 |
| `silent_install` | 是否下载后延迟到下次启动自动应用 |
| `supabase.project_url` | 发布脚本上传时使用的 Supabase 项目 URL |
| `supabase.bucket_name` | 发布包所在 bucket |
| `supabase.directory` | bucket 内目录 |
`supabase` 节只给发布脚本使用。客户端运行时检查更新只需要 `update_server_url`
## 客户端集成
### 1. 安装 NuGet 包
```powershell
dotnet add <YourApp.csproj> package Velopack
```
### 2. 在程序入口尽早初始化
Velopack 初始化应尽量放在应用启动最早位置:
```csharp
using Velopack;
internal static class Program
{
[STAThread]
public static void Main(string[] args)
{
VelopackApp.Build()
.SetArgs(args)
.SetAutoApplyOnStartup(true)
.Run();
StartApplication(args);
}
}
```
`SetAutoApplyOnStartup(true)` 用于在下次启动时自动应用已下载但尚未安装的更新。
### 3. 检查、下载并应用更新
下面是最小可复用逻辑:
```csharp
using Velopack;
public sealed class AppUpdateService
{
private readonly string _updateServerUrl;
public AppUpdateService(string updateServerUrl)
{
_updateServerUrl = updateServerUrl;
}
public async Task<bool> CheckDownloadAndRestartAsync(
IProgress<int>? progress = null,
CancellationToken cancellationToken = default)
{
var manager = new UpdateManager(_updateServerUrl);
if (!manager.IsInstalled)
{
return false;
}
var updateInfo = await manager.CheckForUpdatesAsync().ConfigureAwait(false);
if (updateInfo?.TargetFullRelease is null)
{
return false;
}
await manager.DownloadUpdatesAsync(
updateInfo,
value => progress?.Report(Math.Clamp(value, 0, 100)),
cancellationToken).ConfigureAwait(false);
manager.ApplyUpdatesAndRestart(updateInfo.TargetFullRelease);
return true;
}
}
```
建议在 UI 层:
- 启动后延迟 1 到 3 秒检查,避免影响首屏。
- 发现新版本后先提示用户,再下载并重启。
- 手动“检查更新”按钮忽略检查间隔。
- 开发环境或直接运行 `dotnet run` 时,`manager.IsInstalled` 通常为 `false`,应跳过更新检查。
## 打包发布
### 1. 安装 Velopack CLI
```powershell
dotnet tool install -g vpk
```
### 2. 提升版本号
更新项目文件中的版本号,确保新版本高于线上版本:
```xml
<Version>1.2.3</Version>
```
### 3. 发布应用目录
```powershell
dotnet publish .\src\MyApp\MyApp.csproj `
-c Release `
-r win-x64 `
--self-contained false `
-o .\artifacts\publish\my-app
```
如果目标机器不保证安装 .NET Runtime可以改用 self-contained 发布,或按 Velopack 文档为安装包配置所需 framework。
### 4. 生成 Velopack 包
```powershell
vpk pack `
--packId MyCompany.MyApp `
--packVersion 1.2.3 `
--packDir .\artifacts\publish\my-app `
--mainExe MyApp.exe `
--packTitle "My App" `
--packAuthors "MyCompany" `
--outputDir .\artifacts\releases\my-app
```
输出目录应至少包含:
```text
MyApp-win-Setup.exe
MyCompany.MyApp-1.2.3-full.nupkg
MyCompany.MyApp-1.2.3-delta.nupkg
releases.win.json
RELEASES
```
首次发布可能没有 delta 包;从第二次发布开始应包含 delta 包。
## 上传到 Supabase Storage
发布脚本应读取配置文件中的 `supabase.project_url`、`supabase.bucket_name` 和 `supabase.directory`,并从环境变量读取服务端 key
```powershell
$env:SUPABASE_SERVICE_ROLE_KEY = "<service-role-key>"
```
通用上传脚本示例:
```powershell
param(
[Parameter(Mandatory = $true)]
[string]$ConfigPath,
[Parameter(Mandatory = $true)]
[string]$ReleasePath,
[string]$ServiceRoleKey = $env:SUPABASE_SERVICE_ROLE_KEY
)
$ErrorActionPreference = "Stop"
function Join-UrlPath {
param(
[string]$Left,
[string]$Right
)
if ([string]::IsNullOrWhiteSpace($Left)) {
return $Right
}
if ([string]::IsNullOrWhiteSpace($Right)) {
return $Left.TrimEnd("/")
}
return $Left.TrimEnd("/") + "/" + $Right.TrimStart("/")
}
if ([string]::IsNullOrWhiteSpace($ServiceRoleKey)) {
throw "SUPABASE_SERVICE_ROLE_KEY is empty."
}
$config = Get-Content -Encoding UTF8 $ConfigPath -Raw | ConvertFrom-Json
$baseUrl = $config.supabase.project_url.TrimEnd("/")
$bucketName = $config.supabase.bucket_name
$directory = $config.supabase.directory
if ([string]::IsNullOrWhiteSpace($baseUrl) -or
[string]::IsNullOrWhiteSpace($bucketName) -or
[string]::IsNullOrWhiteSpace($directory)) {
throw "Supabase project_url, bucket_name, or directory is missing."
}
$files = Get-ChildItem -Path $ReleasePath -File | Sort-Object Name
if (-not $files) {
throw "No release files were found."
}
$headers = @{
"apikey" = $ServiceRoleKey
"Authorization" = "Bearer $ServiceRoleKey"
}
foreach ($file in $files) {
$objectPath = Join-UrlPath $directory $file.Name
$uploadUrl = Join-UrlPath "$baseUrl/storage/v1/object/$bucketName" $objectPath
$uploadHeaders = $headers.Clone()
$uploadHeaders["Content-Type"] = "application/octet-stream"
$uploadHeaders["x-upsert"] = "true"
Invoke-RestMethod `
-Uri $uploadUrl `
-Method Post `
-Headers $uploadHeaders `
-InFile $file.FullName
}
$publicBaseUrl = Join-UrlPath "$baseUrl/storage/v1/object/public/$bucketName" $directory
Write-Host "Public feed root: $publicBaseUrl"
Write-Host "Velopack feed: $(Join-UrlPath $publicBaseUrl 'releases.win.json')"
```
上传命令:
```powershell
.\UploadReleaseToSupabase.ps1 `
-ConfigPath .\Config\update_config.json `
-ReleasePath .\artifacts\releases\my-app
```
## 发布后验证
### 1. 验证 feed 可访问
```powershell
$feedUrl = "https://<project-ref>.supabase.co/storage/v1/object/public/app-releases/my-app/windows/stable/releases.win.json"
$feed = Invoke-RestMethod -Uri $feedUrl -Method Get
$feed.Assets | Sort-Object {[version]$_.Version} -Descending | Select-Object -First 5 PackageId,Version,Type,FileName,Size
```
确认:
- 最新版本等于本次发布版本。
- 最新版本至少有 `Full` 资产。
- 非首次发布时应有 `Delta` 资产。
### 2. 验证安装包 URL
```powershell
curl.exe -I "https://<project-ref>.supabase.co/storage/v1/object/public/app-releases/my-app/windows/stable/MyApp-win-Setup.exe"
```
期望结果:
```text
HTTP/1.1 200 OK
```
### 3. 验证客户端更新
建议用两个版本验证:
1. 安装旧版本。
2. 上传新版本发布包。
3. 启动旧版本并触发检查更新。
4. 确认能发现新版本、下载进度正常、重启后版本号变为新版本。
## 复用检查清单
- Supabase Storage 已创建 public bucket。
- 更新包目录规划稳定,不同应用和通道互不覆盖。
- 客户端配置的 `update_server_url` 指向包含 `releases.win.json` 的目录。
- 发布脚本只在受信环境使用 `SUPABASE_SERVICE_ROLE_KEY`
- 客户端没有保存 Supabase key。
- 每次发布前提高应用版本号。
- `vpk pack` 输出包含 `releases.win.json`、`Setup.exe` 和 `.nupkg`
- 上传后 `releases.win.json` 可通过公开 URL 访问。
- 安装包 URL 返回 `200 OK`
- 至少用旧版本安装包做一次真实升级测试。
## 参考文档
- [Velopack C# Getting Started](https://docs.velopack.io/getting-started/csharp)
- [Velopack Integrating Overview](https://docs.velopack.io/integrating/overview)
- [Supabase Storage Buckets](https://supabase.com/docs/guides/storage/buckets/fundamentals)
- [Supabase Creating Buckets](https://supabase.com/docs/guides/storage/buckets/creating-buckets)
- [Supabase Storage Access Control](https://supabase.com/docs/guides/storage/security/access-control)
- [Supabase Standard Uploads](https://supabase.com/docs/guides/storage/uploads/standard-uploads)