mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-10 17:26:38 +08:00
Merge commit '63219e4802e99d87b0b3ce28349aa464d4331456'
This commit is contained in:
@@ -376,7 +376,15 @@ func buildLatestServerReleaseView(release *githubReleaseResponse, channel Releas
|
|||||||
isDevBuild := currentVersion == "" || strings.EqualFold(currentVersion, "dev")
|
isDevBuild := currentVersion == "" || strings.EqualFold(currentVersion, "dev")
|
||||||
hasUpdate := false
|
hasUpdate := false
|
||||||
if release != nil && !isDevBuild {
|
if release != nil && !isDevBuild {
|
||||||
hasUpdate = isVersionNewer(currentVersion, release.TagName)
|
if channel == ReleaseChannelPreview {
|
||||||
|
// Preview releases use a "major.minor.patch-git-<commit>" scheme that cannot
|
||||||
|
// be meaningfully compared against the running stable version, so we skip the
|
||||||
|
// version check and always allow upgrading when the user explicitly selects
|
||||||
|
// the preview channel.
|
||||||
|
hasUpdate = true
|
||||||
|
} else {
|
||||||
|
hasUpdate = isVersionNewer(currentVersion, release.TagName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serverUpgradeState.Lock()
|
serverUpgradeState.Lock()
|
||||||
|
|||||||
@@ -140,6 +140,36 @@ func TestBuildLatestServerReleaseViewPreview(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBuildLatestServerReleaseViewPreviewBypassVersionCheck verifies that switching to
|
||||||
|
// the preview channel always reports has_update=true, even when the preview tag uses a
|
||||||
|
// "major.minor.patch-git-<commit>" scheme that would otherwise compare as equal-or-older
|
||||||
|
// than the currently running stable version.
|
||||||
|
func TestBuildLatestServerReleaseViewPreviewBypassVersionCheck(t *testing.T) {
|
||||||
|
originalVersion := common.Version
|
||||||
|
common.Version = "v1.0.0"
|
||||||
|
t.Cleanup(func() {
|
||||||
|
common.Version = originalVersion
|
||||||
|
resetServerUpgradeTestState(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
// A typical preview tag: same base version as stable but with a git-commit suffix.
|
||||||
|
// Without the bypass, isVersionNewer("v1.0.0", "v1.0.0-git-abc1234") returns false
|
||||||
|
// because a version without a prerelease identifier is considered higher than one
|
||||||
|
// with a prerelease identifier under semver rules.
|
||||||
|
view := buildLatestServerReleaseView(&githubReleaseResponse{
|
||||||
|
TagName: "v1.0.0-git-abc1234",
|
||||||
|
Prerelease: true,
|
||||||
|
PublishedAt: "2026-03-12T00:00:00Z",
|
||||||
|
}, ReleaseChannelPreview)
|
||||||
|
|
||||||
|
if !view.HasUpdate {
|
||||||
|
t.Fatal("expected preview channel to bypass version comparison and report has_update=true")
|
||||||
|
}
|
||||||
|
if view.Channel != ReleaseChannelPreview.String() {
|
||||||
|
t.Fatalf("unexpected channel: %s", view.Channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUploadManualServerBinary(t *testing.T) {
|
func TestUploadManualServerBinary(t *testing.T) {
|
||||||
originalVersion := common.Version
|
originalVersion := common.Version
|
||||||
common.Version = "v0.4.0"
|
common.Version = "v0.4.0"
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export function DashboardTopbar() {
|
|||||||
);
|
);
|
||||||
const [uploadedBinary, setUploadedBinary] =
|
const [uploadedBinary, setUploadedBinary] =
|
||||||
useState<UploadedServerBinaryInfo | null>(null);
|
useState<UploadedServerBinaryInfo | null>(null);
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
const isRoot = (user?.role ?? 0) >= 100;
|
const isRoot = (user?.role ?? 0) >= 100;
|
||||||
const upgradeStatusPollInterval = 3000;
|
const upgradeStatusPollInterval = 3000;
|
||||||
@@ -102,14 +103,19 @@ export function DashboardTopbar() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const uploadBinaryMutation = useMutation({
|
const uploadBinaryMutation = useMutation({
|
||||||
mutationFn: uploadServerBinary,
|
mutationFn: (binary: File) =>
|
||||||
|
uploadServerBinary(binary, (progress) => {
|
||||||
|
setUploadProgress(progress);
|
||||||
|
}),
|
||||||
onSuccess: (candidate) => {
|
onSuccess: (candidate) => {
|
||||||
|
setUploadProgress(0);
|
||||||
setVersionFeedback(null);
|
setVersionFeedback(null);
|
||||||
setManualUpgradeError(null);
|
setManualUpgradeError(null);
|
||||||
setUploadedBinary(candidate);
|
setUploadedBinary(candidate);
|
||||||
setManualUpgradeStatus(candidate.comparison_message);
|
setManualUpgradeStatus(candidate.comparison_message);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
setUploadProgress(0);
|
||||||
setUploadedBinary(null);
|
setUploadedBinary(null);
|
||||||
setManualUpgradeStatus(null);
|
setManualUpgradeStatus(null);
|
||||||
setManualUpgradeError(
|
setManualUpgradeError(
|
||||||
@@ -217,6 +223,7 @@ export function DashboardTopbar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUploadBinary = (binary: File) => {
|
const handleUploadBinary = (binary: File) => {
|
||||||
|
setUploadProgress(0);
|
||||||
setManualUpgradeStatus(null);
|
setManualUpgradeStatus(null);
|
||||||
setManualUpgradeError(null);
|
setManualUpgradeError(null);
|
||||||
uploadBinaryMutation.mutate(binary);
|
uploadBinaryMutation.mutate(binary);
|
||||||
@@ -359,6 +366,7 @@ export function DashboardTopbar() {
|
|||||||
}
|
}
|
||||||
isUpgrading={upgradeMutation.isPending}
|
isUpgrading={upgradeMutation.isPending}
|
||||||
isUploadingBinary={uploadBinaryMutation.isPending}
|
isUploadingBinary={uploadBinaryMutation.isPending}
|
||||||
|
uploadProgress={uploadProgress}
|
||||||
isConfirmingManualUpgrade={confirmManualUpgradeMutation.isPending}
|
isConfirmingManualUpgrade={confirmManualUpgradeMutation.isPending}
|
||||||
onChannelChange={handleReleaseChannelChange}
|
onChannelChange={handleReleaseChannelChange}
|
||||||
onCheck={handleCheckRelease}
|
onCheck={handleCheckRelease}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { apiRequest } from '@/lib/api/client';
|
import { ApiError, apiRequest, getApiUrl } from '@/lib/api/client';
|
||||||
|
import type { ApiEnvelope } from '@/types/api';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
LatestReleaseInfo,
|
LatestReleaseInfo,
|
||||||
@@ -19,13 +20,63 @@ export function upgradeServer(channel: ReleaseChannel = 'stable') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uploadServerBinary(binary: File) {
|
export function uploadServerBinary(
|
||||||
|
binary: File,
|
||||||
|
onProgress?: (progress: number) => void,
|
||||||
|
): Promise<UploadedServerBinaryInfo> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('binary', binary);
|
formData.append('binary', binary);
|
||||||
|
|
||||||
return apiRequest<UploadedServerBinaryInfo>('/update/manual-upload', {
|
if (!onProgress) {
|
||||||
method: 'POST',
|
return apiRequest<UploadedServerBinaryInfo>('/update/manual-upload', {
|
||||||
body: formData,
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', getApiUrl('/update/manual-upload'));
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
|
||||||
|
xhr.upload.addEventListener('progress', (event) => {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
onProgress(Math.round((event.loaded / event.total) * 100));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
let payload: ApiEnvelope<UploadedServerBinaryInfo> | null = null;
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(xhr.responseText) as ApiEnvelope<UploadedServerBinaryInfo>;
|
||||||
|
} catch {
|
||||||
|
payload = null;
|
||||||
|
}
|
||||||
|
if (xhr.status < 200 || xhr.status >= 300) {
|
||||||
|
reject(
|
||||||
|
new ApiError(
|
||||||
|
payload?.message || `请求失败(${xhr.status})`,
|
||||||
|
xhr.status,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!payload) {
|
||||||
|
reject(new ApiError('响应格式无效', xhr.status));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!payload.success) {
|
||||||
|
reject(new ApiError(payload.message || '请求失败', xhr.status));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(payload.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('error', () => {
|
||||||
|
reject(new ApiError('上传过程中网络连接中断,请检查网络后重试', 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.send(formData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ interface VersionUpgradeModalProps {
|
|||||||
isChecking: boolean;
|
isChecking: boolean;
|
||||||
isUpgrading: boolean;
|
isUpgrading: boolean;
|
||||||
isUploadingBinary: boolean;
|
isUploadingBinary: boolean;
|
||||||
|
uploadProgress: number;
|
||||||
isConfirmingManualUpgrade: boolean;
|
isConfirmingManualUpgrade: boolean;
|
||||||
onChannelChange: (channel: ReleaseChannel) => void;
|
onChannelChange: (channel: ReleaseChannel) => void;
|
||||||
onCheck: () => void;
|
onCheck: () => void;
|
||||||
@@ -71,6 +72,7 @@ export function VersionUpgradeModal({
|
|||||||
isChecking,
|
isChecking,
|
||||||
isUpgrading,
|
isUpgrading,
|
||||||
isUploadingBinary,
|
isUploadingBinary,
|
||||||
|
uploadProgress,
|
||||||
isConfirmingManualUpgrade,
|
isConfirmingManualUpgrade,
|
||||||
onChannelChange,
|
onChannelChange,
|
||||||
onCheck,
|
onCheck,
|
||||||
@@ -290,6 +292,21 @@ export function VersionUpgradeModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isUploadingBinary ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between text-xs text-[var(--foreground-secondary)]">
|
||||||
|
<span>上传中...</span>
|
||||||
|
<span>{uploadProgress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 w-full overflow-hidden rounded-full bg-[var(--border-default)]">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-[var(--brand-primary)] transition-all duration-200"
|
||||||
|
style={{width: `${uploadProgress}%`}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{manualErrorMessage ? (
|
{manualErrorMessage ? (
|
||||||
<ErrorState
|
<ErrorState
|
||||||
title="手动升级检查失败"
|
title="手动升级检查失败"
|
||||||
|
|||||||
Reference in New Issue
Block a user