Files
app/native/sync-core/src/uploader.rs
T
gongyun 17673b2862
Android APK Release / Build Android APK (push) Successful in 55m29s
1.3.21.3.2
2026-05-26 16:32:18 +08:00

300 lines
11 KiB
Rust

use crate::api_client::ApiClient;
use crate::errors::{Result, SyncError};
use crate::file_lock::FileLockRegistry;
use crate::models::*;
use crate::sync_db::SyncDb;
use dashmap::DashMap;
use std::path::Path;
use tokio::sync::Semaphore;
/// 上传单个文件(含重试),受并发信号量控制
/// 逐块读取文件,避免全量加载到内存
#[allow(clippy::too_many_arguments)]
pub async fn upload_file(
task_id: &str,
action: &SyncAction,
config: &WorkerConfig,
api: &ApiClient,
db: &SyncDb,
file_locks: &FileLockRegistry,
ensured_dirs: &DashMap<String, ()>,
semaphore: &Semaphore,
root_id: &str,
) -> Result<()> {
let local = action.local_entry.as_ref().ok_or_else(|| {
SyncError::Internal("上传操作缺少本地文件信息".into())
})?;
let _lock = file_locks.acquire(&action.relative_path).await;
if local.is_dir {
let remote_uri = format!("{}/{}", config.remote_root, action.relative_path);
ensure_remote_dirs(task_id, config.remote_root.as_str(), &action.relative_path, api, ensured_dirs).await?;
db.upsert_file_mapping(&FileMapping {
id: 0,
sync_root_id: root_id.to_string(),
local_path: local.relative_path.clone(),
remote_uri,
remote_file_id: None,
local_hash: Some(local.quick_hash.clone()),
remote_hash: None,
local_mtime: Some(local.mtime_ms),
remote_mtime: None,
local_size: Some(local.size),
remote_size: None,
sync_status: SyncFileStatus::Synced,
is_placeholder: false,
}).await?;
return Ok(());
}
let local_path = config.local_root.join(&local.relative_path);
let file_uri = format!("{}/{}", config.remote_root, action.relative_path);
let _permit = semaphore.acquire().await
.map_err(|e| SyncError::Internal(format!("获取传输信号量失败: {}", e)))?;
// 信号量获取后标记为 Running(实际开始传输)
let _ = db
.update_task_item_status_by_path(
task_id,
&action.relative_path,
"upload",
&TaskItemStatus::Running,
None,
)
.await;
let max_retries = 3u32;
// 确保远程父目录链存在
if let Some(parent) = Path::new(&action.relative_path).parent() {
let parent_str = parent.to_string_lossy().to_string();
if !parent_str.is_empty() {
if let Err(e) = ensure_remote_dirs(task_id, &config.remote_root, &parent_str, api, ensured_dirs).await {
tracing::warn!("[{}] 确保远程父目录失败 {}: {}", task_id, parent_str, e);
}
}
}
let overwrite = action.db_mapping.is_some();
// 打开文件(加重试,处理文件被占用的情况)
let file = {
let mut read_retries = 0u32;
loop {
match tokio::fs::File::open(&local_path).await {
Ok(f) => break f,
Err(e) if e.raw_os_error() == Some(32) && read_retries < 5 => {
read_retries += 1;
let delay = read_retries * 1000;
tracing::warn!("[{}] 文件被占用,{}ms后重试 ({}): {}", task_id, delay, read_retries, local_path.display());
tokio::time::sleep(std::time::Duration::from_millis(delay as u64)).await;
}
Err(e) => return Err(e.into()),
}
}
};
let last_modified = if local.mtime_ms > 0 { Some(local.mtime_ms) } else { None };
let mime_type_str = local.mime_type.as_deref();
let session = retry_upload_session(
task_id, &file_uri, local.size, max_retries, overwrite, last_modified, mime_type_str, None, api,
).await?;
let chunk_size = session.chunk_size as usize;
tracing::info!("[{}] chunk_size={}bytes, 开始上传: {} ({}bytes)", task_id, chunk_size, action.relative_path, local.size);
// 逐块读取 + 上传,内存占用仅为一个 chunk
// 分片上传要求:每个分片(除最后一个)必须恰好 chunk_size 字节
// 因此需要循环 read 直到读满 buffer 或到达 EOF,才能作为一个分片上传
let mut reader = tokio::io::BufReader::new(file);
let mut buf = vec![0u8; chunk_size];
let mut index: u32 = 0;
loop {
use tokio::io::AsyncReadExt;
let mut filled = 0usize;
loop {
let n = reader.read(&mut buf[filled..]).await?;
if n == 0 { break; }
filled += n;
if filled >= chunk_size { break; }
}
if filled == 0 { break; }
let chunk = &buf[..filled];
let mut chunk_retries = 0u32;
loop {
match api.upload_chunk(&session.session_id, index, chunk).await {
Ok(_) => break,
Err(SyncError::Auth(_)) => return Err(SyncError::Auth("Token 过期".into())),
Err(e) if chunk_retries < max_retries => {
chunk_retries += 1;
let delay = crate::utils::retry_delay_ms(chunk_retries, 1000, 30000);
tracing::warn!("[{}] 上传重试 ({}/{}): {}: {}", task_id, chunk_retries, max_retries, action.relative_path, e);
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
}
Err(e) => return Err(e),
}
}
index += 1;
}
// 上传完成后获取远程文件信息
let remote_uri = file_uri.clone();
let (remote_file_id, remote_hash) = match api.get_file_info(&remote_uri).await {
Ok(info) => (info.file_id.clone(), info.hash.clone()),
Err(e) => {
tracing::warn!("[{}] 上传后获取文件信息失败: {}", task_id, e);
(None, None)
}
};
db.upsert_file_mapping(&FileMapping {
id: 0,
sync_root_id: root_id.to_string(),
local_path: local.relative_path.clone(),
remote_uri,
remote_file_id,
local_hash: Some(local.quick_hash.clone()),
remote_hash: remote_hash.or(Some(local.quick_hash.clone())),
local_mtime: Some(local.mtime_ms),
remote_mtime: Some(local.mtime_ms),
local_size: Some(local.size),
remote_size: Some(local.size),
sync_status: SyncFileStatus::Synced,
is_placeholder: false,
}).await?;
tracing::info!("[{}] 上传完成: {}", task_id, action.relative_path);
Ok(())
}
/// 带重试的创建上传会话(遇到锁冲突自动强制解锁)
#[allow(clippy::too_many_arguments)]
pub async fn retry_upload_session(
task_id: &str,
file_uri: &str,
file_size: u64,
max_retries: u32,
overwrite: bool,
last_modified: Option<i64>,
mime_type: Option<&str>,
policy_id: Option<&str>,
api: &ApiClient,
) -> Result<UploadSession> {
let mut attempt = 0u32;
let mut tried_overwrite = overwrite;
loop {
attempt += 1;
match api.create_upload_session(file_uri, file_size, tried_overwrite, last_modified, mime_type, policy_id).await {
Ok(session) => return Ok(session),
Err(SyncError::Auth(_)) => return Err(SyncError::Auth("Token 过期".into())),
Err(SyncError::ObjectExisted) if !tried_overwrite => {
tracing::info!("[{}] 远程文件已存在,切换为覆盖上传", task_id);
tried_overwrite = true;
continue;
}
Err(SyncError::LockConflict { tokens }) => {
tracing::warn!("[{}] 文件锁定冲突,强制解锁 {} 个锁后重试", task_id, tokens.len());
if let Err(e) = api.force_unlock_files(&tokens).await {
tracing::error!("[{}] 强制解锁失败: {}", task_id, e);
}
if attempt <= max_retries {
continue;
}
return Err(SyncError::LockConflict { tokens });
}
Err(e) if attempt <= max_retries => {
let delay = crate::utils::retry_delay_ms(attempt, 1000, 30000);
tracing::warn!("[{}] 创建上传会话失败,{}ms后重试 ({}): {}", task_id, delay, attempt, e);
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
}
Err(e) => return Err(e),
}
}
}
/// 递归确保远程目录链存在(带缓存)
pub async fn ensure_remote_dirs(
task_id: &str,
remote_root: &str,
relative_path: &str,
api: &ApiClient,
ensured_dirs: &DashMap<String, ()>,
) -> Result<()> {
let parts: Vec<&str> = relative_path.split('/').filter(|p| !p.is_empty()).collect();
if parts.is_empty() {
return Ok(());
}
let mut current = remote_root.to_string();
let mut dirs_to_create: Vec<(String, String)> = Vec::new();
for part in &parts {
let next_uri = format!("{}/{}", current, part);
if ensured_dirs.contains_key(&next_uri) {
current = next_uri;
continue;
}
dirs_to_create.push((current.clone(), part.to_string()));
current = next_uri;
}
if dirs_to_create.is_empty() {
return Ok(());
}
for (parent_uri, dir_name) in &dirs_to_create {
let uri = format!("{}/{}", parent_uri, dir_name);
match api.create_directory(parent_uri, dir_name).await {
Ok(_) => {
tracing::debug!("[{}] 创建远程目录: {}", task_id, uri);
ensured_dirs.insert(uri.clone(), ());
}
Err(e) => {
let msg = e.to_string();
if msg.contains("exist") || msg.contains("already") || msg.contains("40004") {
ensured_dirs.insert(uri.clone(), ());
} else {
tracing::warn!("[{}] 创建远程目录失败 {}: {}", task_id, uri, e);
}
}
}
}
Ok(())
}
/// 逐块读取文件并上传分片(用于相册同步等场景,避免全量加载到内存)
/// 分片上传要求:每个分片(除最后一个)必须恰好 chunk_size 字节
pub async fn upload_file_chunked(
api: &ApiClient,
local_path: &Path,
session: &UploadSession,
) -> Result<()> {
let chunk_size = session.chunk_size as usize;
let file = tokio::fs::File::open(local_path).await?;
let mut reader = tokio::io::BufReader::new(file);
let mut buf = vec![0u8; chunk_size];
let mut index: u32 = 0;
loop {
use tokio::io::AsyncReadExt;
let mut filled = 0usize;
loop {
let n = reader.read(&mut buf[filled..]).await?;
if n == 0 { break; }
filled += n;
if filled >= chunk_size { break; }
}
if filled == 0 { break; }
api.upload_chunk(&session.session_id, index, &buf[..filled]).await?;
index += 1;
}
Ok(())
}