feat: improve sync workspace and updater stability in 0.1.27
This commit is contained in:
@@ -112,7 +112,7 @@ const DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS = Math.max(
|
|||||||
10,
|
10,
|
||||||
Math.min(3600, Number(process.env.DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS || 30))
|
Math.min(3600, Number(process.env.DOWNLOAD_SIGNED_URL_EXPIRES_SECONDS || 30))
|
||||||
);
|
);
|
||||||
const DEFAULT_DESKTOP_VERSION = process.env.DESKTOP_LATEST_VERSION || '0.1.26';
|
const DEFAULT_DESKTOP_VERSION = process.env.DESKTOP_LATEST_VERSION || '0.1.27';
|
||||||
const DEFAULT_DESKTOP_INSTALLER_URL = process.env.DESKTOP_INSTALLER_URL || '';
|
const DEFAULT_DESKTOP_INSTALLER_URL = process.env.DESKTOP_INSTALLER_URL || '';
|
||||||
const DEFAULT_DESKTOP_INSTALLER_SHA256 = String(process.env.DESKTOP_INSTALLER_SHA256 || '').trim().toLowerCase();
|
const DEFAULT_DESKTOP_INSTALLER_SHA256 = String(process.env.DESKTOP_INSTALLER_SHA256 || '').trim().toLowerCase();
|
||||||
const DEFAULT_DESKTOP_INSTALLER_SIZE = Math.max(0, Number(process.env.DESKTOP_INSTALLER_SIZE || 0));
|
const DEFAULT_DESKTOP_INSTALLER_SIZE = Math.max(0, Number(process.env.DESKTOP_INSTALLER_SIZE || 0));
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "desktop-client",
|
"name": "desktop-client",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.26",
|
"version": "0.1.27",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
2
desktop-client/src-tauri/Cargo.lock
generated
2
desktop-client/src-tauri/Cargo.lock
generated
@@ -693,7 +693,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "desktop-client"
|
name = "desktop-client"
|
||||||
version = "0.1.26"
|
version = "0.1.27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "desktop-client"
|
name = "desktop-client"
|
||||||
version = "0.1.26"
|
version = "0.1.27"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ use std::io::{Read, Seek, SeekFrom};
|
|||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use std::process::Stdio;
|
||||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
@@ -25,6 +27,10 @@ use windows_sys::Win32::Foundation::LocalFree;
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||||
const RESUMABLE_CHUNK_MAX_RETRIES: u32 = 3;
|
const RESUMABLE_CHUNK_MAX_RETRIES: u32 = 3;
|
||||||
const RESUMABLE_CHUNK_RETRY_BASE_DELAY_MS: u64 = 900;
|
const RESUMABLE_CHUNK_RETRY_BASE_DELAY_MS: u64 = 900;
|
||||||
|
|
||||||
@@ -440,6 +446,61 @@ fn build_download_resume_temp_path(download_dir: &Path, preferred_name: &str, ur
|
|||||||
download_dir.join(temp_name)
|
download_dir.join(temp_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_update_installer_file_name(file_name: &str) -> bool {
|
||||||
|
let lower = file_name.trim().to_ascii_lowercase();
|
||||||
|
if !lower.ends_with(".exe") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lower.starts_with("wanwan-cloud-desktop_v") || file_name.trim().starts_with("玩玩云_v")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_old_update_installers(
|
||||||
|
download_dir: &Path,
|
||||||
|
keep_file_name: &str,
|
||||||
|
keep_latest: usize,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut entries: Vec<(PathBuf, SystemTime)> = Vec::new();
|
||||||
|
let normalized_keep = keep_file_name.trim();
|
||||||
|
for entry in fs::read_dir(download_dir).map_err(|err| format!("扫描下载目录失败: {}", err))? {
|
||||||
|
let path = match entry {
|
||||||
|
Ok(item) => item.path(),
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !is_update_installer_file_name(file_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let modified = fs::metadata(&path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|meta| meta.modified().ok())
|
||||||
|
.unwrap_or(SystemTime::UNIX_EPOCH);
|
||||||
|
entries.push((path, modified));
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.sort_by(|a, b| b.1.cmp(&a.1));
|
||||||
|
let retain_count = keep_latest.max(1);
|
||||||
|
let mut retained = 0usize;
|
||||||
|
for (path, _) in entries {
|
||||||
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string();
|
||||||
|
let should_keep = file_name == normalized_keep || retained < retain_count;
|
||||||
|
if should_keep {
|
||||||
|
retained += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ = fs::remove_file(&path);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_local_state_dir() -> PathBuf {
|
fn resolve_local_state_dir() -> PathBuf {
|
||||||
if let Some(appdata) = env::var_os("APPDATA") {
|
if let Some(appdata) = env::var_os("APPDATA") {
|
||||||
return PathBuf::from(appdata).join("wanwan-cloud-desktop");
|
return PathBuf::from(appdata).join("wanwan-cloud-desktop");
|
||||||
@@ -502,10 +563,17 @@ fn load_login_state_record() -> Result<Option<(String, String, String)>, String>
|
|||||||
record.get::<_, String>(2)?,
|
record.get::<_, String>(2)?,
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
|
drop(stmt);
|
||||||
match row {
|
match row {
|
||||||
Ok((base_url, username, password)) => {
|
Ok((base_url, username, password)) => {
|
||||||
let decoded_password = decode_login_password(&password)?;
|
match decode_login_password(&password) {
|
||||||
Ok(Some((base_url, username, decoded_password)))
|
Ok(decoded_password) => Ok(Some((base_url, username, decoded_password))),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("decode login state failed, clearing invalid state: {}", err);
|
||||||
|
let _ = conn.execute("DELETE FROM login_state WHERE id = 1", []);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||||
Err(err) => Err(format!("读取登录状态失败: {}", err)),
|
Err(err) => Err(format!("读取登录状态失败: {}", err)),
|
||||||
@@ -1124,6 +1192,11 @@ async fn api_native_download(
|
|||||||
let save_path = alloc_download_path(&download_dir, preferred_name);
|
let save_path = alloc_download_path(&download_dir, preferred_name);
|
||||||
fs::rename(&resume_temp_path, &save_path)
|
fs::rename(&resume_temp_path, &save_path)
|
||||||
.map_err(|err| format!("完成断点下载失败: {}", err))?;
|
.map_err(|err| format!("完成断点下载失败: {}", err))?;
|
||||||
|
if let Some(saved_name) = save_path.file_name().and_then(|name| name.to_str()) {
|
||||||
|
if is_update_installer_file_name(saved_name) {
|
||||||
|
let _ = cleanup_old_update_installers(&download_dir, saved_name, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(ref id) = task_id {
|
if let Some(ref id) = task_id {
|
||||||
emit_native_download_progress(
|
emit_native_download_progress(
|
||||||
&window,
|
&window,
|
||||||
@@ -1232,6 +1305,11 @@ async fn api_native_download(
|
|||||||
let save_path = alloc_download_path(&download_dir, preferred_name);
|
let save_path = alloc_download_path(&download_dir, preferred_name);
|
||||||
fs::rename(&resume_temp_path, &save_path)
|
fs::rename(&resume_temp_path, &save_path)
|
||||||
.map_err(|err| format!("保存下载文件失败: {}", err))?;
|
.map_err(|err| format!("保存下载文件失败: {}", err))?;
|
||||||
|
if let Some(saved_name) = save_path.file_name().and_then(|name| name.to_str()) {
|
||||||
|
if is_update_installer_file_name(saved_name) {
|
||||||
|
let _ = cleanup_old_update_installers(&download_dir, saved_name, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(ref id) = task_id {
|
if let Some(ref id) = task_id {
|
||||||
emit_native_download_progress(
|
emit_native_download_progress(
|
||||||
@@ -1419,7 +1497,10 @@ del \"%~f0\" >nul 2>nul\r\n",
|
|||||||
.arg("call")
|
.arg("call")
|
||||||
.arg(&script_path)
|
.arg(&script_path)
|
||||||
.current_dir(&temp_dir)
|
.current_dir(&temp_dir)
|
||||||
.creation_flags(CREATE_NO_WINDOW)
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)
|
||||||
.spawn();
|
.spawn();
|
||||||
if let Err(err) = spawn_result {
|
if let Err(err) = spawn_result {
|
||||||
let _ = fs::OpenOptions::new()
|
let _ = fs::OpenOptions::new()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "玩玩云",
|
"productName": "玩玩云",
|
||||||
"version": "0.1.26",
|
"version": "0.1.27",
|
||||||
"identifier": "cn.workyai.wanwancloud.desktop",
|
"identifier": "cn.workyai.wanwancloud.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ const syncState = reactive({
|
|||||||
nextRunAt: "",
|
nextRunAt: "",
|
||||||
});
|
});
|
||||||
const updateState = reactive({
|
const updateState = reactive({
|
||||||
currentVersion: "0.1.26",
|
currentVersion: "0.1.27",
|
||||||
latestVersion: "",
|
latestVersion: "",
|
||||||
available: false,
|
available: false,
|
||||||
mandatory: false,
|
mandatory: false,
|
||||||
@@ -725,6 +725,30 @@ function normalizeRelativePath(rawPath: string) {
|
|||||||
return String(rawPath || "").replace(/\\/g, "/").replace(/^\/+/, "");
|
return String(rawPath || "").replace(/\\/g, "/").replace(/^\/+/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeSyncFolderSegment(raw: string, fallback = "默认同步盘") {
|
||||||
|
const normalized = String(raw || "")
|
||||||
|
.trim()
|
||||||
|
.replace(/[\/\\:*?"<>|]/g, "_")
|
||||||
|
.replace(/\.\./g, "_")
|
||||||
|
.replace(/\s+/g, " ");
|
||||||
|
const compact = normalized.replace(/^_+|_+$/g, "").trim();
|
||||||
|
if (!compact) return fallback;
|
||||||
|
return compact.slice(0, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveDefaultSyncRemoteBasePath() {
|
||||||
|
const localDirName = sanitizeSyncFolderSegment(extractFileNameFromPath(syncState.localDir || ""), "");
|
||||||
|
const owner = sanitizeSyncFolderSegment(String(user.value?.username || user.value?.id || "desktop"), "desktop");
|
||||||
|
const folderName = localDirName || `同步盘_${owner}`;
|
||||||
|
return normalizePath(`/同步盘/${folderName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDefaultSyncRemoteBasePath(force = false) {
|
||||||
|
const normalizedCurrent = normalizePath(syncState.remoteBasePath || "/");
|
||||||
|
if (!force && normalizedCurrent !== "/") return;
|
||||||
|
syncState.remoteBasePath = deriveDefaultSyncRemoteBasePath();
|
||||||
|
}
|
||||||
|
|
||||||
function getSyncConfigStorageKey() {
|
function getSyncConfigStorageKey() {
|
||||||
const userId = String(user.value?.id || "guest");
|
const userId = String(user.value?.id || "guest");
|
||||||
return `wanwan_desktop_sync_config_v2_${userId}`;
|
return `wanwan_desktop_sync_config_v2_${userId}`;
|
||||||
@@ -748,7 +772,10 @@ function safeParseObject(raw: string | null) {
|
|||||||
function loadSyncConfig() {
|
function loadSyncConfig() {
|
||||||
const key = getSyncConfigStorageKey();
|
const key = getSyncConfigStorageKey();
|
||||||
const raw = localStorage.getItem(key);
|
const raw = localStorage.getItem(key);
|
||||||
if (!raw) return;
|
if (!raw) {
|
||||||
|
applyDefaultSyncRemoteBasePath(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
if (!parsed || typeof parsed !== "object") return;
|
if (!parsed || typeof parsed !== "object") return;
|
||||||
@@ -760,6 +787,7 @@ function loadSyncConfig() {
|
|||||||
} catch {
|
} catch {
|
||||||
// ignore invalid cache
|
// ignore invalid cache
|
||||||
}
|
}
|
||||||
|
applyDefaultSyncRemoteBasePath(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSyncConfig() {
|
function saveSyncConfig() {
|
||||||
@@ -1052,8 +1080,12 @@ async function installLatestUpdate(): Promise<boolean> {
|
|||||||
const launchResponse = await invokeBridge("api_silent_install_and_restart", {
|
const launchResponse = await invokeBridge("api_silent_install_and_restart", {
|
||||||
installerPath: savePath,
|
installerPath: savePath,
|
||||||
});
|
});
|
||||||
|
const logFilePath = String(launchResponse.data?.logFilePath || "").trim();
|
||||||
if (launchResponse.ok && launchResponse.data?.success) {
|
if (launchResponse.ok && launchResponse.data?.success) {
|
||||||
showToast("静默安装已启动,完成后会自动重启客户端", "success");
|
const launchTip = logFilePath
|
||||||
|
? `静默安装已启动(日志:${logFilePath})`
|
||||||
|
: "静默安装已启动,完成后会自动重启客户端";
|
||||||
|
showToast(launchTip, "success");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const win = getCurrentWindow();
|
const win = getCurrentWindow();
|
||||||
void (async () => {
|
void (async () => {
|
||||||
@@ -1070,6 +1102,9 @@ async function installLatestUpdate(): Promise<boolean> {
|
|||||||
}, 400);
|
}, 400);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (logFilePath) {
|
||||||
|
console.warn("silent updater log path:", logFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await openPath(savePath);
|
await openPath(savePath);
|
||||||
@@ -1077,7 +1112,7 @@ async function installLatestUpdate(): Promise<boolean> {
|
|||||||
console.error("open installer fallback failed", error);
|
console.error("open installer fallback failed", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
showToast("更新包已下载,请手动运行安装程序", "info");
|
showToast("更新包已下载,静默安装未启动,请手动运行安装程序", "info");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resetUpdateRuntime();
|
resetUpdateRuntime();
|
||||||
}, 1200);
|
}, 1200);
|
||||||
@@ -1197,7 +1232,11 @@ async function chooseSyncDirectory() {
|
|||||||
title: "选择本地同步文件夹",
|
title: "选择本地同步文件夹",
|
||||||
});
|
});
|
||||||
if (typeof result === "string" && result.trim()) {
|
if (typeof result === "string" && result.trim()) {
|
||||||
|
const hadCustomRemoteBase = normalizePath(syncState.remoteBasePath || "/") !== "/";
|
||||||
syncState.localDir = result.trim();
|
syncState.localDir = result.trim();
|
||||||
|
if (!hadCustomRemoteBase) {
|
||||||
|
applyDefaultSyncRemoteBasePath(true);
|
||||||
|
}
|
||||||
showToast("同步目录已更新", "success");
|
showToast("同步目录已更新", "success");
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1302,6 +1341,36 @@ async function clearSyncSnapshot() {
|
|||||||
showToast("同步索引已清理,下次会全量扫描", "success");
|
showToast("同步索引已清理,下次会全量扫描", "success");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureRemoteFolderPath(targetPath: string) {
|
||||||
|
const normalized = normalizePath(targetPath || "/");
|
||||||
|
if (normalized === "/") {
|
||||||
|
return { ok: true, path: "/" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = normalized.split("/").filter(Boolean);
|
||||||
|
let current = "/";
|
||||||
|
for (const segment of segments) {
|
||||||
|
const response = await invokeBridge("api_mkdir", {
|
||||||
|
baseUrl: appConfig.baseUrl,
|
||||||
|
path: current,
|
||||||
|
folderName: segment,
|
||||||
|
});
|
||||||
|
if (!(response.ok && response.data?.success)) {
|
||||||
|
const message = String(response.data?.message || "");
|
||||||
|
if (!message.includes("已存在")) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
path: normalizePath(`${current}/${segment}`),
|
||||||
|
message: message || "创建远程同步目录失败",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = normalizePath(`${current}/${segment}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, path: normalized };
|
||||||
|
}
|
||||||
|
|
||||||
async function runSyncOnce(trigger: "manual" | "auto" = "manual") {
|
async function runSyncOnce(trigger: "manual" | "auto" = "manual") {
|
||||||
if (!authenticated.value) return;
|
if (!authenticated.value) return;
|
||||||
const localDir = syncState.localDir.trim();
|
const localDir = syncState.localDir.trim();
|
||||||
@@ -1355,6 +1424,21 @@ async function runSyncOnce(trigger: "manual" | "auto" = "manual") {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyDefaultSyncRemoteBasePath(false);
|
||||||
|
const remoteBase = normalizePath(syncState.remoteBasePath || "/");
|
||||||
|
const ensuredRemotePaths = new Set<string>();
|
||||||
|
const baseEnsureResult = await ensureRemoteFolderPath(remoteBase);
|
||||||
|
if (!baseEnsureResult.ok) {
|
||||||
|
syncState.syncing = false;
|
||||||
|
syncState.lastRunAt = new Date().toISOString();
|
||||||
|
syncState.lastSummary = String(baseEnsureResult.message || "创建远程同步目录失败");
|
||||||
|
if (trigger === "manual") {
|
||||||
|
showToast(syncState.lastSummary, "error");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ensuredRemotePaths.add(remoteBase);
|
||||||
|
|
||||||
const successPaths = new Set<string>();
|
const successPaths = new Set<string>();
|
||||||
for (let index = 0; index < changedItems.length; index += 1) {
|
for (let index = 0; index < changedItems.length; index += 1) {
|
||||||
await waitForTransferQueue();
|
await waitForTransferQueue();
|
||||||
@@ -1362,7 +1446,6 @@ async function runSyncOnce(trigger: "manual" | "auto" = "manual") {
|
|||||||
const relPath = normalizeRelativePath(item.relativePath);
|
const relPath = normalizeRelativePath(item.relativePath);
|
||||||
const fileName = extractFileNameFromPath(relPath) || `同步文件${index + 1}`;
|
const fileName = extractFileNameFromPath(relPath) || `同步文件${index + 1}`;
|
||||||
const parent = getRelativeParentPath(relPath);
|
const parent = getRelativeParentPath(relPath);
|
||||||
const remoteBase = normalizePath(syncState.remoteBasePath || "/");
|
|
||||||
const targetPath = parent ? normalizePath(`${remoteBase}/${parent}`) : remoteBase;
|
const targetPath = parent ? normalizePath(`${remoteBase}/${parent}`) : remoteBase;
|
||||||
const taskId = `S-${Date.now()}-${index}`;
|
const taskId = `S-${Date.now()}-${index}`;
|
||||||
|
|
||||||
@@ -1380,6 +1463,21 @@ async function runSyncOnce(trigger: "manual" | "auto" = "manual") {
|
|||||||
});
|
});
|
||||||
updateTransferTask(taskId, { status: "uploading", speed: "上传中", progress: 10 });
|
updateTransferTask(taskId, { status: "uploading", speed: "上传中", progress: 10 });
|
||||||
|
|
||||||
|
if (!ensuredRemotePaths.has(targetPath)) {
|
||||||
|
const ensureResult = await ensureRemoteFolderPath(targetPath);
|
||||||
|
if (!ensureResult.ok) {
|
||||||
|
syncState.failedCount += 1;
|
||||||
|
updateTransferTask(taskId, {
|
||||||
|
speed: "-",
|
||||||
|
progress: 0,
|
||||||
|
status: "failed",
|
||||||
|
note: String(ensureResult.message || "创建远程同步目录失败"),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ensuredRemotePaths.add(targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
const resp = await uploadFileWithResume(item.path, targetPath, taskId);
|
const resp = await uploadFileWithResume(item.path, targetPath, taskId);
|
||||||
|
|
||||||
if (resp.ok && resp.data?.success) {
|
if (resp.ok && resp.data?.success) {
|
||||||
|
|||||||
Reference in New Issue
Block a user