Compare commits
4 Commits
204dbb6ab5
...
e55ce106d4
| Author | SHA1 | Date | |
|---|---|---|---|
| e55ce106d4 | |||
| 601e0741b1 | |||
| be121190b3 | |||
| 6274567219 |
@@ -151,6 +151,28 @@ async fn upload_local_change(
|
||||
if !path.is_file() {
|
||||
return Ok(());
|
||||
}
|
||||
// cfapi-Platzhalter oder gerade hydrierende Dateien NICHT hochladen -
|
||||
// sonst wird jede Wolken-Datei sofort komplett gesynct und wir haben
|
||||
// keinen On-Demand-Modus mehr.
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if super::windows::is_cfapi_placeholder(path) {
|
||||
super::windows::log_msg(
|
||||
&cfg.mount_point,
|
||||
&format!("skip upload (placeholder): {}", path.display()),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
// Eigene Log-Datei nicht mit hochladen.
|
||||
if path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.map(|n| n.starts_with(".minicloud-"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
// Relativer Pfad im Mount = Ziel-Pfad auf Server
|
||||
let rel = path
|
||||
.strip_prefix(&cfg.mount_point)
|
||||
|
||||
@@ -91,10 +91,12 @@ pub fn register_sync_root(
|
||||
policies.Population = CF::CF_POPULATION_POLICY::default();
|
||||
policies.InSync = CF::CF_INSYNC_POLICY::default();
|
||||
|
||||
// Das Struct-Feld ist `CF_HYDRATION_POLICY` (u16-Wrapper um das
|
||||
// _PRIMARY-Enum). Direkter Feldzugriff:
|
||||
// Hydration PARTIAL = Datei-Inhalt kommt bei Zugriff per FETCH_DATA.
|
||||
// Population FULL = Ordnerinhalt ist komplett vorgefuellt durch uns
|
||||
// (populate_placeholders). So muss Windows NICHT FETCH_PLACEHOLDERS
|
||||
// callen, den wir nicht implementieren - sonst timeout beim Oeffnen.
|
||||
policies.Hydration.Primary = CF::CF_HYDRATION_POLICY_PARTIAL;
|
||||
policies.Population.Primary = CF::CF_POPULATION_POLICY_PARTIAL;
|
||||
policies.Population.Primary = CF::CF_POPULATION_POLICY_FULL;
|
||||
|
||||
// Holder fuer displayname, damit wir ihn spaeter ggf. in ein eigenes
|
||||
// struct einbauen koennen. windows-rs verlangt hier nichts weiter.
|
||||
@@ -171,19 +173,75 @@ unsafe extern "system" fn on_fetch_data(
|
||||
// HTTPS-Download im separaten Thread (Callback darf nicht blockieren).
|
||||
let ctx = ctx_snapshot();
|
||||
std::thread::spawn(move || {
|
||||
let _ = transfer_range(connection_key, transfer_key, file_id, offset, length, ctx);
|
||||
log_msg(&ctx.mount_point, &format!(
|
||||
"FETCH_DATA file_id={file_id} offset={offset} len={length}"
|
||||
));
|
||||
match transfer_range(connection_key, transfer_key, file_id, offset, length, &ctx) {
|
||||
Ok(()) => log_msg(&ctx.mount_point, &format!(
|
||||
"fetch file_id={file_id} OK"
|
||||
)),
|
||||
Err(e) => {
|
||||
log_err(&ctx.mount_point, &format!(
|
||||
"fetch file_id={file_id} offset={offset} len={length} FAILED: {e}"
|
||||
));
|
||||
// Garantiert Fehler-Completion, damit Windows nicht in Timeout laeuft.
|
||||
let _ = complete_transfer(connection_key, transfer_key, None, offset, length);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn log_msg(mount: &Path, msg: &str) {
|
||||
use std::io::Write;
|
||||
// Log-Datei NEBEN den Mount, damit sie nicht selbst als Platzhalter
|
||||
// behandelt wird.
|
||||
let log = mount
|
||||
.parent()
|
||||
.map(|p| p.join(".minicloud-cloudfiles.log"))
|
||||
.unwrap_or_else(|| PathBuf::from(".minicloud-cloudfiles.log"));
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&log) {
|
||||
let _ = writeln!(f, "[{}] {}", chrono::Utc::now().to_rfc3339(), msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_err(mount: &Path, msg: &str) {
|
||||
log_msg(mount, msg);
|
||||
}
|
||||
|
||||
/// True wenn die Datei ein cfapi-Platzhalter ist (noch nicht hydriert)
|
||||
/// oder gerade vom Cloud-Filter verwaltet wird. Fuer solche Dateien
|
||||
/// duerfen wir KEINEN Upload ausloesen, sonst verwandelt der Sync-Loop
|
||||
/// jeden Platzhalter sofort in eine vollstaendig lokale Datei.
|
||||
pub fn is_cfapi_placeholder(path: &Path) -> bool {
|
||||
use windows::Win32::Storage::FileSystem::GetFileAttributesW;
|
||||
let Ok(w) = U16CString::from_str(path.to_string_lossy().as_ref()) else {
|
||||
return false;
|
||||
};
|
||||
let attrs = unsafe { GetFileAttributesW(PCWSTR(w.as_ptr())) };
|
||||
if attrs == u32::MAX {
|
||||
return false;
|
||||
}
|
||||
// FILE_ATTRIBUTE_OFFLINE (0x1000) oder
|
||||
// FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS (0x400000) oder
|
||||
// FILE_ATTRIBUTE_RECALL_ON_OPEN (0x40000)
|
||||
(attrs & 0x0040_1000) != 0 || (attrs & 0x0004_0000) != 0
|
||||
}
|
||||
|
||||
fn transfer_range(
|
||||
connection_key: CF::CF_CONNECTION_KEY,
|
||||
transfer_key: i64,
|
||||
file_id: i64,
|
||||
offset: i64,
|
||||
length: u64,
|
||||
ctx: CloudContext,
|
||||
ctx: &CloudContext,
|
||||
) -> Result<(), String> {
|
||||
let client = reqwest::blocking::Client::new();
|
||||
if ctx.server_url.is_empty() || ctx.access_token.is_empty() {
|
||||
return Err("CloudContext nicht gesetzt (Server/Token leer)".into());
|
||||
}
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.build()
|
||||
.map_err(|e| format!("client: {e}"))?;
|
||||
let url = format!(
|
||||
"{}/api/files/{}/download",
|
||||
ctx.server_url.trim_end_matches('/'),
|
||||
@@ -195,22 +253,26 @@ fn transfer_range(
|
||||
.bearer_auth(&ctx.access_token)
|
||||
.header("Range", &range)
|
||||
.send()
|
||||
.map_err(|e| format!("download: {e}"))?;
|
||||
.map_err(|e| format!("send: {e}"))?;
|
||||
let status = resp.status();
|
||||
if !status.is_success() && status.as_u16() != 206 {
|
||||
let _ = complete_transfer(connection_key, transfer_key, None, offset, length);
|
||||
return Err(format!("HTTP {}", status));
|
||||
}
|
||||
let bytes = resp
|
||||
.bytes()
|
||||
.map_err(|e: reqwest::Error| e.to_string())?;
|
||||
complete_transfer(
|
||||
connection_key,
|
||||
transfer_key,
|
||||
Some(&bytes),
|
||||
offset,
|
||||
length,
|
||||
)
|
||||
let bytes = resp.bytes().map_err(|e: reqwest::Error| e.to_string())?;
|
||||
// Wenn Server kein Range unterstuetzt und volle Datei liefert,
|
||||
// aus dem Body den angeforderten Bereich ausschneiden.
|
||||
let slice: &[u8] = if status.as_u16() == 206 {
|
||||
&bytes[..]
|
||||
} else {
|
||||
let start = offset as usize;
|
||||
let end = (start + length as usize).min(bytes.len());
|
||||
if start >= bytes.len() {
|
||||
&[]
|
||||
} else {
|
||||
&bytes[start..end]
|
||||
}
|
||||
};
|
||||
complete_transfer(connection_key, transfer_key, Some(slice), offset, slice.len() as u64)
|
||||
}
|
||||
|
||||
fn complete_transfer(
|
||||
|
||||
@@ -949,6 +949,12 @@ async fn cloud_files_enable(
|
||||
|
||||
*state.cloud_files_loop.lock().unwrap() = Some(handle);
|
||||
*state.cloud_files_watcher.lock().unwrap() = Some(watcher);
|
||||
|
||||
// Mount-Pfad persistieren, damit er beim Neustart wiederkommt.
|
||||
let mut cfg = AppConfig::load();
|
||||
cfg.cloud_files_mount = mount_point.clone();
|
||||
let _ = cfg.save();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -963,7 +969,33 @@ async fn cloud_files_disable(
|
||||
let _ = handle.tx.send(cloud_files::sync_loop::LoopMessage::Shutdown);
|
||||
}
|
||||
state.cloud_files_watcher.lock().unwrap().take();
|
||||
cloud_files::unregister_sync_root(&PathBuf::from(mount_point))
|
||||
let result = cloud_files::unregister_sync_root(&PathBuf::from(&mount_point));
|
||||
|
||||
// Auch bei Fehler Mount aus Config loeschen, damit der Client nicht
|
||||
// endlos versucht, einen toten Pfad wiederherzustellen.
|
||||
let mut cfg = AppConfig::load();
|
||||
cfg.cloud_files_mount.clear();
|
||||
let _ = cfg.save();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn cloud_files_get_mount() -> String {
|
||||
AppConfig::load().cloud_files_mount
|
||||
}
|
||||
|
||||
/// Notfall-Aufraeumen: Ordner als Sync-Root deregistrieren, auch wenn
|
||||
/// kein Callback-Handle existiert. Nuetzlich wenn der Client hart beendet
|
||||
/// wurde und ein "toter" Ordner in Windows haengt.
|
||||
#[tauri::command]
|
||||
async fn cloud_files_force_cleanup(mount_point: String) -> Result<(), String> {
|
||||
let mp = PathBuf::from(&mount_point);
|
||||
let _ = cloud_files::unregister_sync_root(&mp);
|
||||
let mut cfg = AppConfig::load();
|
||||
cfg.cloud_files_mount.clear();
|
||||
let _ = cfg.save();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1179,6 +1211,8 @@ pub fn run() {
|
||||
cloud_files_supported,
|
||||
cloud_files_enable,
|
||||
cloud_files_disable,
|
||||
cloud_files_get_mount,
|
||||
cloud_files_force_cleanup,
|
||||
cloud_files_pin,
|
||||
cloud_files_unpin,
|
||||
])
|
||||
|
||||
@@ -13,6 +13,10 @@ pub struct AppConfig {
|
||||
pub auto_start: bool,
|
||||
#[serde(default)]
|
||||
pub start_minimized: bool,
|
||||
/// Persistierter Mount-Punkt der Cloud-Files-Integration.
|
||||
/// Leer = nicht aktiv. Wird beim App-Start wieder aktiviert.
|
||||
#[serde(default)]
|
||||
pub cloud_files_mount: String,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
|
||||
@@ -42,6 +42,27 @@ const cloudFilesError = ref("");
|
||||
async function checkCloudFilesSupport() {
|
||||
try { cloudFilesSupported.value = await invoke("cloud_files_supported"); }
|
||||
catch { cloudFilesSupported.value = false; }
|
||||
try {
|
||||
const saved = await invoke("cloud_files_get_mount");
|
||||
if (saved) cloudFilesMountPoint.value = saved;
|
||||
} catch { /* no saved mount */ }
|
||||
}
|
||||
|
||||
async function forceCleanupCloudFiles() {
|
||||
if (!cloudFilesMountPoint.value) return;
|
||||
if (!confirm(`Sync-Root unter ${cloudFilesMountPoint.value} zwangsweise aufraeumen?\n\nDanach kann der Ordner ggf. geloescht werden.`)) return;
|
||||
cloudFilesError.value = "";
|
||||
cloudFilesBusy.value = true;
|
||||
try {
|
||||
await invoke("cloud_files_force_cleanup", { mountPoint: cloudFilesMountPoint.value });
|
||||
cloudFilesActive.value = false;
|
||||
cloudFilesMountPoint.value = "";
|
||||
syncLog.value = [`[${ts()}] Cloud-Files Zwangsbereinigung durchgefuehrt`, ...syncLog.value].slice(0, 200);
|
||||
} catch (err) {
|
||||
cloudFilesError.value = String(err);
|
||||
} finally {
|
||||
cloudFilesBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function browseCfMount() {
|
||||
@@ -337,7 +358,7 @@ function formatSize(b) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
checkCloudFilesSupport();
|
||||
await checkCloudFilesSupport();
|
||||
// Try auto-login with saved credentials
|
||||
try {
|
||||
const saved = await invoke("load_saved_config");
|
||||
@@ -357,6 +378,15 @@ onMounted(async () => {
|
||||
if (syncPaths.value.length > 0) {
|
||||
await startSync();
|
||||
}
|
||||
// Cloud-Files automatisch reaktivieren, wenn Mount gespeichert.
|
||||
if (cloudFilesSupported.value && cloudFilesMountPoint.value) {
|
||||
try {
|
||||
await invoke("cloud_files_enable", { mountPoint: cloudFilesMountPoint.value });
|
||||
cloudFilesActive.value = true;
|
||||
} catch (e) {
|
||||
cloudFilesError.value = `Auto-Reaktivierung fehlgeschlagen: ${e}`;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
syncStatus.value = "Auto-Login fehlgeschlagen";
|
||||
// Show login screen with pre-filled fields
|
||||
@@ -463,6 +493,12 @@ onUnmounted(() => { unlistenStatus?.(); unlistenLog?.(); unlistenError?.(); unli
|
||||
</button>
|
||||
<button v-else class="btn-secondary" :disabled="cloudFilesBusy"
|
||||
@click="disableCloudFiles">Deaktivieren</button>
|
||||
<button v-if="cloudFilesMountPoint && !cloudFilesActive"
|
||||
class="btn-secondary" :disabled="cloudFilesBusy"
|
||||
@click="forceCleanupCloudFiles"
|
||||
title="Toten Sync-Root nach hartem Beenden des Clients aufraeumen">
|
||||
Aufraeumen
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="cloudFilesError" class="error" style="margin-top:0.5rem">{{ cloudFilesError }}</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user