Search by

buting / cliapp-updater

BuTing

Framework-independent PHP updater for the apps.cli.cc release and licensing service

Package info

github.com/BuTingBa/cliapp-updater

pkg:composer/buting/cliapp-updater

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-10-13 05:41 UTC

This package is auto-updated.

Last update: 2026-10-13 05:52:16 UTC


README

GitHub · Packagist

用于 apps.cli.cc 发布平台的 PHP 下载更新库。ThinkPHP/FPM、Webman、普通 PHP、队列和 CLI 共用同一套方法。后台按钮可直接调用 update(),同步完成检查、下载、校验、备份和文件覆盖,不要求客户登录服务器运行命令。

要求:64 位 PHP 7.4 或 PHP 8.x,curl、json、hash、zip 扩展;离线授权验签另需 sodium。没有第三方 PHP 包依赖。

安装与最小调用

composer require buting/cliapp-updater:dev-main

目前提供 dev-main 开发分支,适合接入验证;请提交应用的 composer.lock 固定实际安装的提交。稳定版本发布后可使用 composer require buting/cliapp-updater,或按已发布版本指定约束,不需要降低整个项目的 minimum-stability。

Composer 包名是 buting/cliapp-updater,安装目录为 vendor/buting/cliapp-updater;PHP 命名空间仍为 BuTing\CliAppUpdater,可选命令入口仍为 vendor/bin/cli-app-updater。后两者与 Composer 包名不是同一项配置。

<?php

use BuTing\CliAppUpdater\Updater;

require __DIR__ . '/vendor/autoload.php'; // 框架已自动加载时省略。

$updater = new Updater([
    'base_url' => 'https://apps.example.com',
    'product_code' => 'my-shop',
    'product_key' => getenv('UPDATER_PRODUCT_KEY'),
    'project_dir' => __DIR__,                         // 被更新的应用根目录。
    'work_dir' => dirname(__DIR__) . '/shop-updater', // 独立持久工作目录。
    'version_code' => 100,                           // 首次接入时的实际版本序号。
    'domain' => 'shop.example.com',
]);

$clientKey = $updater->getClientKey(); // 后台展示实例标识;不是安装前置条件。
$release = $updater->check();         // 自动初始化 client_key,可单独调用。
$display = Updater::summary($release);

// 在已通过后台登录、升级权限和 CSRF 校验的 POST 控制器中:
$result = $updater->update(); // 无须先 check/download;完成后才返回。
// null:没有可获取的新版本;array:文件已安装,含版本摘要和本地备份目录。

check() 自动登记实例,但登记不等于授权。授权下载产品需在平台配置更新权益。检查结果可能含在线凭证和短期下载票据,展示或记录前请用 summary() 提取摘要。

后台点击升级怎么接

后台按钮 POST 到应用自己的升级控制器,控制器校验权限后调用方法,并返回框架的 JSON 响应。库不提供公开路由、不接管登录或会话。下面是 ThinkPHP/Webman 控制器内的核心代码:

// 前置:路由限制 POST;后台登录、升级权限、CSRF 校验均已完成。
// $config 来自服务器配置,不能允许请求传入目录、密钥或回调。
$updater = new Updater($config);

// 若应用使用 PHP 原生 session,鉴权及必要写入完成后释放会话锁。
if (session_status() === PHP_SESSION_ACTIVE) {
    session_write_close();
}

$handler = require '/你的示例文件目录/http.php'; // 把 examples/http.php 复制到应用内。
$data = $handler($updater, 'update');            // 同步执行,完成后返回。
return json(['success' => true, 'data' => $data]); // 使用所在框架的响应方法。

examples/http.php 支持 check、update、rollback、status、client-key。它只返回函数,直接访问文件不会安装。示例返回的 updated / rolled_back 是展示字段,与下文库方法的原始返回值不同。异常由应用统一处理,不向浏览器返回密钥、完整堆栈或磁盘路径。

前端等待返回,期间禁用重复点击并显示“正在更新”。超时、断网或页面关闭表示结果尚未确认,不能据此认定安装失败,也不要自动重试或自动恢复。

执行方式

方式 如何调用 适用条件
FPM 同步请求 控制器直接 update() 包体/耗时可控,服务器允许请求等待;不需要命令执行权限
已有队列/任务 worker 控制器投递任务,任务内 update() 大包/长任务;页面查应用自己的任务状态,客户仍只需点击
Webman 请求或自定义 worker 直接 update() 同步执行;较长更新建议交给已有专用 worker
CLI 命令内调用相同方法 可选运维入口,不是安装必需步骤

本库不使用 fastcgi_finish_request(),不在安装时执行 shell 或拉起进程,不内置队列、实时百分比或分段续传。Windows 自动探测系统 ID 时可选调用 reg.exe,禁用 proc_open 会回退;安装不要求开启它,也可显式配置稳定 machine_id。

FPM 需给下载、解压、备份、覆盖留出完整时间。核对 PHP max_execution_time、FPM request_terminate_timeout、Nginx fastcgi_read_timeout 及上游代理/浏览器超时。http.download_timeout 仅限制出站 cURL 下载,不能延长入站请求期限。set_time_limit(0)、ignore_user_abort(true) 也不能保证进程不被服务器终止;库不会改动这些全局设置。PHP FPM 配置、Nginx FastCGI 超时

常驻不等于自动异步。同步 cURL、ZIP 和文件操作在普通 Webman worker 中会占用该 worker。协程 I/O 行为取决于驱动和 hook,本库不承诺协程非阻塞;Fiber 本身不会把阻塞 I/O 变为异步。重载框架由应用或外部管理器完成。Webman 协程说明

构造配置:new Updater(array $config)

所有配置在构造时传入,没有公开可写属性或公共常量。改变配置时创建新实例,同一站点仍共用 work_dir。

参数 类型 默认值 说明
base_url string 必填 可信 HTTPS 平台地址,可带部署子路径;不能含账号、查询串、片段
product_code string 必填 平台产品 Code,非数字 ID
product_key string 必填 产品通信密钥,通过 Product-Key 请求头发送
project_dir string 必填 已存在的应用根目录,推荐绝对路径;ZIP 根直接对应这里
work_dir string 必填 可自动创建的独立持久工作目录;不得与项目根互相包含,不可通过 Web 访问
version_code int 0 0~9007199254740991;仅首次初始化使用,之后读状态;如100,不是字符串1.0.0
client_key string '' 空值自动生成;显式值8~64位字母、数字、下划线或短横线,必须匹配已有状态
machine_id string或null null null自动探测;非空值为稳定实例ID,最多512字节;空串禁用探测、使用持久随机ID
upgrade_token string '' 可选客户在线凭证;通过 Upgrade-Token 发送,空值由平台按实例查授权
domain string '' 可信配置域名,不取请求Host;无协议/端口/路径,不支持IP;无域名绑定可空
client_name string '' 平台展示的安装实例名称
channel string 'stable' stable 或 beta
preserve string[] [] 追加项目根相对保护路径,如 config/local.php;目录含子文件,不支持通配符
max_package_bytes int 2147483648 ZIP下载上限,正整数,默认2 GiB;不是服务器承载能力保证
http array [] 下表中的传输选项
archive_limits array [] 下表中的解压选项

http 完整参数:

参数 类型 默认值 说明
allow_http bool false 仅本地开发显式允许明文HTTP
connect_timeout int 10 cURL连接超时,正整数秒
timeout int 30 单次平台API总超时,正整数秒
download_timeout int 1800 单次下载总超时,正整数秒
ca_file string '' 自定义可信CA路径;空值使用环境默认CA

HTTPS始终校验证书,不跟随重定向。下载仅接受受限相对票据路径,不使用 file_full_url 跳转其他来源。

archive_limits 完整参数:

参数 类型 默认值 说明
max_files int 50000 ZIP条目数上限,正整数
max_expanded_bytes int 10737418240 解压总字节上限,正整数,默认10 GiB
'http' => ['connect_timeout' => 10, 'timeout' => 30, 'download_timeout' => 600],
'archive_limits' => ['max_files' => 20000, 'max_expanded_bytes' => 2147483648],
'preserve' => ['config/local.php', 'public/customer-files', 'data/customer.sqlite'],

两个目录与权限

/srv/shop/
├── app/                    project_dir:被更新的程序
│   ├── app/
│   ├── public/              网站 Web 根目录
│   ├── vendor/
│   └── .env
└── shop-updater/            work_dir:状态、下载、备份
    ├── state.json
    ├── update.lock
    ├── transaction.json
    ├── downloads/
    └── backups/

执行PHP的账户需能读写相应文件,并预留ZIP、解压和备份空间。同站点Web、队列、CLI共用工作目录;不同站点分别配置。不要把工作目录放进 vendor/bin、系统临时目录或网站公开目录。

Updater 方法参考

所有实例方法使用同一工作目录的非阻塞排他锁:冲突立即抛异常,不排队;返回/抛出时释放。status/getClientKey也使用这把锁。summary是纯静态转换,不取锁。

getClientKey(): string

无参数。读取或首次生成实例标识,无网络请求,可在后台展示,无须先status/report。

新实例默认按系统安装ID、平台地址、产品Code、项目真实路径派生32位十六进制ID。Linux读取machine-id,Windows读取MachineGuid;不可读则随机生成并保存。原始系统ID不上传、不保存,旧随机ID继续复用。

同机同产品同路径可稳定派生;不同站点路径会区分。复制站点应使用新的工作目录,镜像/容器可能复制系统ID,应配置不同的持久machine_id或client_key。它不是硬件防复制凭据。即使ID可重建,也不要删除状态来重置版本。

status(): array

无参数,无网络请求,可能首次创建状态。返回本地已提交版本和最近文件事务:

字段 类型 说明
client_key string 实例标识
client_key_source string machine / random / configured / legacy
version_code int 当前已提交版本序号
version string,可缺省 更新后记录的显示版本,首次初始化可能没有
updated_at string,可缺省 更新记录的UTC ISO 8601时间;恢复时回到旧状态
transaction_status string none / applying / complete / rolling_back / rolled_back / unknown

安装持锁时会抛异常,不是实时进度接口。无锁时出现applying/rolling_back表示未完成事务,新check/update会拒绝,先处理恢复。complete仅表示文件与版本提交,不代表框架已重载或所有回调成功。

report(): array

无参数。POST登记/心跳,自动初始化并上报版本、目标php、渠道、配置域名及系统/PHP信息。返回平台data:registered: bool、client_id: int、last_time: string。登记不表示授权,check也会登记,通常不用单独调用。

authorize(): array

无参数。POST在线验证更新权益,返回平台data:

字段 类型 说明
authorized bool 成功为true;拒绝抛异常
license_value string或null 在线凭证,不交给浏览器或普通日志
expires_at string或null 更新服务截止日,不是软件运行到期日
license_mode int 平台下载模式:1授权、2公开

不安装文件,也不自动保存返回凭证到配置。

check(): array

无参数。自动初始化、登记、验证权益,返回平台data。有更新时校验完整PHP ZIP元数据及递增版本。

字段 类型 说明
update bool 有无可获取的新版本
status_code int 有更新200,无更新304,这是data中的业务字段
license_value string或null 可能携带在线凭证
version string 有更新时的显示版本
version_code int 有更新时的版本序号
changelog string 有更新时的说明
published_at string 有更新时的平台首次发布时间
package array 有更新时的包信息,见下表

package中本库使用的字段:

字段 类型 说明
platform string 必须php
package_type int 必须1(完整包)
file_name string 必须ZIP文件名
file_size int ZIP字节数,正数且不超过上限
sha256 string 64位小写十六进制校验值
file_url string 短期票据相对路径,供download使用,不展示
requirements string 运行要求文字,不自动解析成环境检查

平台可能返回更多元数据,file_full_url/config不会用于跨站下载或执行命令。无更新时不保证存在版本/package字段。拒绝授权、传输错误、非法元数据抛异常,不伪装成update=false。

download(array $release): string

release是近期check的完整返回值且update=true,不能传summary的结果。返回校验完毕的ZIP绝对路径,位于work_dir/downloads。只下载、不安装、不推进版本。

成功后的文件由调用方保管或清理,失败临时下载自动清理。票据失效或出口IP变化时重新check。没有断点续传;不能把下载路径传给update,update会重新检查并下载。

update(array $hooks = []): ?array

hooks是可选本地回调,见后文。在FPM/Webman/队列/CLI中均可调用,同步完成后返回,不返回后台任务ID。

顺序:取锁 → 检查授权与版本 → 下载校验 → 安全解压 → 备份与恢复计划落盘 → before_apply → 覆盖文件 → after_apply → 提交版本/事务 → after_commit → 返回。

返回null表示无更新,不运行安装回调;成功返回summary字段加 backup_dir: string(本地备份目录)。update=true是本次发布摘要,不表示还有下一次更新。

覆盖或提交前回调发生可捕获异常时,尝试恢复文件和版本;数据库及回调副作用不自动回滚。after_commit失败会抛异常,但已安装版本保留,需补完后续动作。硬终止进程不能依赖catch/finally,需按持久事务恢复。

rollback(array $hooks = []): void

hooks为可选恢复回调。同步恢复最近文件事务和原版本,删除该事务新增文件及空目录。无事务时抛异常,已rolled_back时直接返回。不能指定任意历史版本,不回滚数据库、不重启框架。

恢复前校验全部旧文件备份,损坏则拒绝覆盖。保留最近事务引用的backups和状态文件。

Updater::summary(array $release): array

静态方法,输入原始check结果,保留其中已有的update/status_code/version/version_code/changelog/published_at,并添加requirements(取package.requirements,缺省空串)。去掉凭证、票据和其他包元数据,适合展示/日志,不能传给download。只是字段筛选,不验证发布数据。

回调参数与时机

形式为 array<string, callable>,每个回调接收一个 array $release 版本摘要。返回false或抛异常视为失败,无返回值/其他值视为成功。未知名称或不可调用值抛InvalidArgumentException。回调只能来自本地应用,不能来自平台或ZIP。

名称 所属方法 时机与用途
before_apply update 备份/事务已写好、覆盖前;检查条件并确认维护状态
after_apply update 覆盖后、版本提交前;应用健康检查或自行实现的迁移
after_commit update 提交后;通知外部管理器/应用后续动作,失败不自动撤销安装
before_rollback rollback 备份校验后、恢复前;确认维护状态
after_rollback rollback 文件/版本恢复后;应用检查与后续处理
$result = $updater->update([
    'after_apply' => static function (array $release): void {
        // 调用应用自己的健康检查,不通过则抛 RuntimeException。
        // 数据库迁移及其恢复策略由应用负责。
    },
    'after_commit' => static function (array $release): void {
        // 可通知已有进程管理器;重载自身前确保任务结果已记录。
    },
]);

回调在锁内执行,不要在回调里再次调用同站点的Updater实例方法。自动故障恢复不调用before_rollback/after_rollback,它们只用于显式rollback。

需要一致性备份时,在update前就进入维护并暂停相关写入;before_apply已经晚于备份。逐个文件替换不是整站原子切换,其他请求可能读到混合代码。库不创建维护页面或管理业务流量。

错误处理与恢复

参数错误通常抛InvalidArgumentException;传输、授权、锁、ZIP/文件/状态错误通常抛RuntimeException;非法JSON可能抛JsonException。无专用错误枚举,错误文本不是稳定业务码;调用边界可捕获Throwable。不要把所有异常都解释为版本未安装,after_commit异常发生在提交后。

情况 处理
升级接口超时/失联 结果待确认,不自动重试;等原操作结束,查询状态并核对应用
另一操作持锁 等待;status同样可能被锁拒绝,不能查百分比
无锁且事务applying/rolling_back 保持维护,通过rollback处理后再继续
after_commit失败、事务complete 文件已提交,补完缓存/进程/维护动作
恢复失败/备份损坏 保留工作目录,修复备份/权限后再恢复,不删状态绕过检查

HTTP恢复入口可调用同一个rollback。若更新破坏了应用路由,该路由就无法承担恢复。需要较强恢复能力的项目,可将受保护的恢复入口及其更新器副本部署在被更新目录之外。

ZIP 与文件更新规则

ZIP根对应project_dir,不要多套项目文件夹。发布前准备依赖与构建产物,库不在线执行Composer、数据库命令或包内脚本。

默认保护 .git、.env、任意层级的 .env.*、.user.ini、runtime、storage、upload、uploads、public/upload、public/uploads、public/storage;preserve追加客户配置/数据。ZIP未包含的旧文件不自动删除。拒绝危险路径、符号链接、大小写碰撞及超限/损坏ZIP。

SHA-256核对可信平台提供的包内容;离线授权签名不等于ZIP签名。框架缓存、常驻进程重载及数据库由宿主应用负责。

OfflineLicense 方法参考

use BuTing\CliAppUpdater\OfflineLicense;

$license = OfflineLicense::verify(
    file_get_contents('/srv/private/license.json'),
    $trustedPublicKeyBase64,
    'my-shop',
    $updater->getClientKey(),
    'shop.example.com',
    '2026-10-01T12:00:00+08:00'
);

OfflineLicense::verify(string $document, string $publicKey, string $productCode, string $clientKey, string $domain = '', ?string $publishedAt = null): array

参数 必填 说明
document 是 授权文件JSON原文,不是路径;最多65536字节
publicKey 是 可信预置的Ed25519公钥Base64(解码32字节),不能取待验证文件自带公钥
productCode 是 应匹配的产品Code
clientKey 是 当前实例标识;证书声明该绑定时必须匹配
domain 否 当前可信域名;证书声明该绑定时必须匹配
publishedAt 否 可信版本首次发布时间,ISO 8601带时区,供更新权益截止日比较

返回验证后的原始签名payload数组:schema、key_id、license_id(string)、product_code、issued_at、updates_until(string或null)、constraints(证书声明的client_key/domain)。未知绑定拒绝,失败抛异常。

updates_until=null表示不限制版本日期;不传publishedAt时只检查签名、产品、绑定等,不证明任意版本均可获取。截止日不限制软件运行。离线文件不是下载票据,导出后不能即时远程撤销。本库尚无离线ZIP安装方法。

OfflineLicense::domain(string $domain): string

去首尾空白、尾部点、转小写;空串返回空串。有intl时可转IDN,否则请传punycode。必须带点的合法主机名,拒绝IP、协议、端口、路径;非法输入抛InvalidArgumentException。

API 范围

以上是应用接入API的全部公开方法与配置,没有公开属性、全局配置变量、队列开关或async参数。Files、HttpClient、Installer、ClientIdentity是包内实现辅助类,通过Updater/OfflineLicense接入,不依赖其内部格式作为应用接口。

可选命令行与示例

php vendor/bin/cli-app-updater check /srv/private/updater.php
php vendor/bin/cli-app-updater update /srv/private/updater.php
php vendor/bin/cli-app-updater rollback /srv/private/updater.php

配置文件return数组,与构造配置一致;CLI专用可另加hooks数组。PHP API需把hooks单独传给update/rollback,放进构造配置不能代替方法参数。

支持client-key/status/report/authorize/check/download/update/rollback,输出JSON;退出码0成功/无更新,1失败,2参数错误。