feat: improve upload resilience and release 0.1.23
This commit is contained in:
@@ -153,7 +153,7 @@ const syncState = reactive({
|
||||
nextRunAt: "",
|
||||
});
|
||||
const updateState = reactive({
|
||||
currentVersion: "0.1.22",
|
||||
currentVersion: "0.1.23",
|
||||
latestVersion: "",
|
||||
available: false,
|
||||
mandatory: false,
|
||||
@@ -241,6 +241,7 @@ let unlistenNativeDownloadProgress: UnlistenFn | null = null;
|
||||
let unlistenNativeUploadProgress: UnlistenFn | null = null;
|
||||
let syncTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let hasCheckedUpdateAfterAuth = false;
|
||||
let authRefreshPromise: Promise<boolean> | null = null;
|
||||
|
||||
const toast = reactive({
|
||||
visible: false,
|
||||
@@ -806,12 +807,40 @@ function toggleFileSortOrder() {
|
||||
fileViewState.sortOrder = fileViewState.sortOrder === "asc" ? "desc" : "asc";
|
||||
}
|
||||
|
||||
async function invokeBridge(command: string, payload: Record<string, any>) {
|
||||
function isAuthBridgeCommand(command: string) {
|
||||
return command === "api_login" || command === "api_refresh_token";
|
||||
}
|
||||
|
||||
async function refreshAccessToken() {
|
||||
if (authRefreshPromise) {
|
||||
return authRefreshPromise;
|
||||
}
|
||||
authRefreshPromise = (async () => {
|
||||
try {
|
||||
const response = await invoke<BridgeResponse>("api_refresh_token", {
|
||||
baseUrl: appConfig.baseUrl,
|
||||
});
|
||||
return Boolean(response.ok && response.data?.success);
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
authRefreshPromise = null;
|
||||
}
|
||||
})();
|
||||
return authRefreshPromise;
|
||||
}
|
||||
|
||||
async function invokeBridge(
|
||||
command: string,
|
||||
payload: Record<string, any>,
|
||||
allowAuthRetry = true,
|
||||
) {
|
||||
let response: BridgeResponse;
|
||||
try {
|
||||
return await invoke<BridgeResponse>(command, payload);
|
||||
response = await invoke<BridgeResponse>(command, payload);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
response = {
|
||||
ok: false,
|
||||
status: 0,
|
||||
data: {
|
||||
@@ -820,6 +849,20 @@ async function invokeBridge(command: string, payload: Record<string, any>) {
|
||||
},
|
||||
} satisfies BridgeResponse;
|
||||
}
|
||||
|
||||
if (
|
||||
response.status === 401
|
||||
&& allowAuthRetry
|
||||
&& !isAuthBridgeCommand(command)
|
||||
&& authenticated.value
|
||||
) {
|
||||
const refreshed = await refreshAccessToken();
|
||||
if (refreshed) {
|
||||
return invokeBridge(command, payload, false);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function initClientVersion() {
|
||||
@@ -1106,34 +1149,78 @@ async function chooseSyncDirectory() {
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFileWithResume(filePath: string, targetPath: string, taskId?: string) {
|
||||
const resumableResponse = await invokeBridge("api_upload_file_resumable", {
|
||||
baseUrl: appConfig.baseUrl,
|
||||
filePath,
|
||||
targetPath,
|
||||
chunkSize: 4 * 1024 * 1024,
|
||||
taskId: taskId || null,
|
||||
});
|
||||
|
||||
if (resumableResponse.ok && resumableResponse.data?.success) {
|
||||
return resumableResponse;
|
||||
function isRetryableUploadResponse(response: BridgeResponse) {
|
||||
const status = Number(response.status || 0);
|
||||
if ([0, 408, 425, 429, 500, 502, 503, 504].includes(status)) {
|
||||
return true;
|
||||
}
|
||||
const message = String(response.data?.message || "").toLowerCase();
|
||||
return (
|
||||
message.includes("timeout")
|
||||
|| message.includes("timed out")
|
||||
|| message.includes("network")
|
||||
|| message.includes("connection")
|
||||
|| message.includes("超时")
|
||||
|| message.includes("稍后重试")
|
||||
);
|
||||
}
|
||||
|
||||
const message = String(resumableResponse.data?.message || "");
|
||||
if (
|
||||
message.includes("当前存储模式不支持分片上传")
|
||||
|| message.includes("分片上传会话")
|
||||
|| message.includes("上传会话")
|
||||
) {
|
||||
return await invokeBridge("api_upload_file", {
|
||||
async function waitMs(ms: number) {
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
||||
}
|
||||
|
||||
async function uploadFileWithResume(filePath: string, targetPath: string, taskId?: string) {
|
||||
const maxAttempts = 3;
|
||||
let lastResponse: BridgeResponse | null = null;
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||
const resumableResponse = await invokeBridge("api_upload_file_resumable", {
|
||||
baseUrl: appConfig.baseUrl,
|
||||
filePath,
|
||||
targetPath,
|
||||
chunkSize: 4 * 1024 * 1024,
|
||||
taskId: taskId || null,
|
||||
});
|
||||
|
||||
if (resumableResponse.ok && resumableResponse.data?.success) {
|
||||
return resumableResponse;
|
||||
}
|
||||
|
||||
const message = String(resumableResponse.data?.message || "");
|
||||
if (
|
||||
message.includes("当前存储模式不支持分片上传")
|
||||
|| message.includes("分片上传会话")
|
||||
|| message.includes("上传会话")
|
||||
) {
|
||||
const fallbackResponse = await invokeBridge("api_upload_file", {
|
||||
baseUrl: appConfig.baseUrl,
|
||||
filePath,
|
||||
targetPath,
|
||||
taskId: taskId || null,
|
||||
});
|
||||
if (fallbackResponse.ok && fallbackResponse.data?.success) {
|
||||
return fallbackResponse;
|
||||
}
|
||||
lastResponse = fallbackResponse;
|
||||
} else {
|
||||
lastResponse = resumableResponse;
|
||||
}
|
||||
|
||||
if (attempt < maxAttempts - 1 && lastResponse && isRetryableUploadResponse(lastResponse)) {
|
||||
await waitMs(600 * (attempt + 1));
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return resumableResponse;
|
||||
return lastResponse || {
|
||||
ok: false,
|
||||
status: 0,
|
||||
data: {
|
||||
success: false,
|
||||
message: "上传失败",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rebuildSyncScheduler() {
|
||||
|
||||
Reference in New Issue
Block a user