mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-05 23:04:59 +03:00
fix(clipboard): unix, refresh cached file size/mtime on re-copy (#15392)
* fix(clipboard): unix, refresh cached file size/mtime on re-copy sync_files() deduped re-copies by path string only, so editing a file and re-copying it (same path) skipped refreshing the cached size/mtime and the file-group descriptor; the peer then received the file truncated to the old cached size (silent corruption for PDF/zip/pptx). Widen the early-return guard to also compare a top-level (size, mtime) fingerprint and to always rebuild when a directory is selected. The Windows wf_cliprdr.c path re-stats per request and is unaffected. Signed-off-by: RAIT-09 <51452399+RAIT-09@users.noreply.github.com> * opt(clipboard): unix, compute file fingerprint once and pass into sync_files fingerprint() was computed before taking the CLIP_FILES lock and then recomputed inside ClipFiles::sync_files under the lock. Pass the precomputed value in so the top-level stat runs once and outside the critical section. No behavior change. Signed-off-by: RAIT-09 <51452399+RAIT-09@users.noreply.github.com> --------- Signed-off-by: RAIT-09 <51452399+RAIT-09@users.noreply.github.com>
This commit is contained in:
@@ -5,7 +5,7 @@ use hbb_common::{
|
|||||||
log,
|
log,
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{path::PathBuf, sync::Arc, usize};
|
use std::{path::PathBuf, sync::Arc, time::SystemTime, usize};
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
// local files are cached, this value should not be changed when copying files
|
// local files are cached, this value should not be changed when copying files
|
||||||
@@ -30,9 +30,35 @@ enum FileContentsRequest {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cheap fingerprint of one top-level selected entry. A change in size/mtime --
|
||||||
|
// or a directory in the selection -- forces sync_files() to rebuild (see below).
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
struct FileSig {
|
||||||
|
size: u64,
|
||||||
|
mtime: Option<SystemTime>,
|
||||||
|
is_dir: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat the top-level selected paths only (no recursion), same order as `files`.
|
||||||
|
fn fingerprint(files: &[String]) -> Vec<FileSig> {
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.map(|s| match std::fs::metadata(s) {
|
||||||
|
Ok(mt) => FileSig {
|
||||||
|
size: mt.len(),
|
||||||
|
mtime: mt.modified().ok(),
|
||||||
|
is_dir: mt.is_dir(),
|
||||||
|
},
|
||||||
|
Err(_) => FileSig::default(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct ClipFiles {
|
struct ClipFiles {
|
||||||
files: Vec<String>,
|
files: Vec<String>,
|
||||||
|
// Fingerprint of `files` (same len/order); detects in-place edits on re-copy.
|
||||||
|
sigs: Vec<FileSig>,
|
||||||
file_list: Vec<LocalFile>,
|
file_list: Vec<LocalFile>,
|
||||||
first_file_index: usize,
|
first_file_index: usize,
|
||||||
files_pdu: Vec<u8>,
|
files_pdu: Vec<u8>,
|
||||||
@@ -41,12 +67,17 @@ struct ClipFiles {
|
|||||||
impl ClipFiles {
|
impl ClipFiles {
|
||||||
fn clear(&mut self) {
|
fn clear(&mut self) {
|
||||||
self.files.clear();
|
self.files.clear();
|
||||||
|
self.sigs.clear();
|
||||||
self.file_list.clear();
|
self.file_list.clear();
|
||||||
self.first_file_index = usize::MAX;
|
self.first_file_index = usize::MAX;
|
||||||
self.files_pdu.clear();
|
self.files_pdu.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> {
|
fn sync_files(
|
||||||
|
&mut self,
|
||||||
|
clipboard_files: &[String],
|
||||||
|
sigs: Vec<FileSig>,
|
||||||
|
) -> Result<(), CliprdrError> {
|
||||||
let clipboard_paths = clipboard_files
|
let clipboard_paths = clipboard_files
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| PathBuf::from(s))
|
.map(|s| PathBuf::from(s))
|
||||||
@@ -58,6 +89,7 @@ impl ClipFiles {
|
|||||||
.position(|f| !f.path.is_dir())
|
.position(|f| !f.path.is_dir())
|
||||||
.unwrap_or(usize::MAX);
|
.unwrap_or(usize::MAX);
|
||||||
self.files = clipboard_files.to_vec();
|
self.files = clipboard_files.to_vec();
|
||||||
|
self.sigs = sigs;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,14 +290,128 @@ pub fn read_file_contents(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> {
|
pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> {
|
||||||
|
// Dedup: skip the rebuild only when paths + sizes + mtimes match and no dir is
|
||||||
|
// selected (a dir's own mtime doesn't change when a file inside it is edited).
|
||||||
|
let current = fingerprint(files);
|
||||||
let mut files_lock = CLIP_FILES.lock();
|
let mut files_lock = CLIP_FILES.lock();
|
||||||
if files_lock.files == files {
|
if files_lock.files == files
|
||||||
|
&& files_lock.sigs == current
|
||||||
|
&& !current.iter().any(|sig| sig.is_dir)
|
||||||
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
files_lock.sync_files(files)?;
|
files_lock.sync_files(files, current)?;
|
||||||
Ok(files_lock.build_file_list_pdu())
|
Ok(files_lock.build_file_list_pdu())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_file_list_pdu() -> Vec<u8> {
|
pub fn get_file_list_pdu() -> Vec<u8> {
|
||||||
CLIP_FILES.lock().files_pdu.clone()
|
CLIP_FILES.lock().files_pdu.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod sig_test {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
// Unique temp dir under the system temp dir; removed on drop (no dev-dep).
|
||||||
|
struct TmpDir(PathBuf);
|
||||||
|
impl TmpDir {
|
||||||
|
fn new(tag: &str) -> Self {
|
||||||
|
let nanos = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let mut dir = std::env::temp_dir();
|
||||||
|
dir.push(format!("rustdesk_sig_test_{}_{}", tag, nanos));
|
||||||
|
fs::create_dir_all(&dir).unwrap();
|
||||||
|
TmpDir(dir)
|
||||||
|
}
|
||||||
|
fn join(&self, name: &str) -> PathBuf {
|
||||||
|
self.0.join(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for TmpDir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = fs::remove_dir_all(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_str(p: &PathBuf) -> String {
|
||||||
|
p.to_string_lossy().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fingerprint_missing_path_is_default() {
|
||||||
|
let tmp = TmpDir::new("missing");
|
||||||
|
let missing = path_str(&tmp.join("does_not_exist.bin"));
|
||||||
|
let sigs = fingerprint(&[missing]);
|
||||||
|
assert_eq!(sigs.len(), 1);
|
||||||
|
// A path that can't be stat'd -> default sig, which forces a rebuild.
|
||||||
|
assert_eq!(sigs[0], FileSig::default());
|
||||||
|
assert_eq!(sigs[0].mtime, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fingerprint_detects_inplace_edit() {
|
||||||
|
let tmp = TmpDir::new("edit");
|
||||||
|
let file = tmp.join("a.bin");
|
||||||
|
fs::write(&file, b"small").unwrap();
|
||||||
|
let p = path_str(&file);
|
||||||
|
|
||||||
|
let before = fingerprint(&[p.clone()]);
|
||||||
|
// Same content, same path: fingerprint must be stable.
|
||||||
|
let again = fingerprint(&[p.clone()]);
|
||||||
|
assert_eq!(before, again);
|
||||||
|
assert_eq!(before[0].size, 5);
|
||||||
|
assert!(!before[0].is_dir);
|
||||||
|
|
||||||
|
// Edit in place so the file grows.
|
||||||
|
fs::write(&file, b"much larger contents than before").unwrap();
|
||||||
|
let after = fingerprint(&[p]);
|
||||||
|
assert_ne!(before, after);
|
||||||
|
assert!(after[0].size > before[0].size);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fingerprint_flags_directory() {
|
||||||
|
let tmp = TmpDir::new("dir");
|
||||||
|
let sub = tmp.join("subdir");
|
||||||
|
fs::create_dir_all(&sub).unwrap();
|
||||||
|
let sigs = fingerprint(&[path_str(&sub)]);
|
||||||
|
assert_eq!(sigs.len(), 1);
|
||||||
|
assert!(sigs[0].is_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recopy_after_edit_refreshes_cached_size() {
|
||||||
|
let tmp = TmpDir::new("recopy");
|
||||||
|
let file = tmp.join("doc.bin");
|
||||||
|
fs::write(&file, b"v1").unwrap(); // 2 bytes
|
||||||
|
let files = vec![path_str(&file)];
|
||||||
|
|
||||||
|
// Drive the public, guarded `sync_files` over the global CLIP_FILES;
|
||||||
|
// reset first (this is the only test that touches the global).
|
||||||
|
clear_files();
|
||||||
|
|
||||||
|
sync_files(&files).unwrap();
|
||||||
|
{
|
||||||
|
let cache = CLIP_FILES.lock();
|
||||||
|
let idx = cache.first_file_index;
|
||||||
|
assert_eq!(cache.file_list[idx].size, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-place edit grows the file; the re-copy must rebuild. Pre-fix the
|
||||||
|
// path-only guard early-returned and left the cached size stale at 2.
|
||||||
|
fs::write(&file, b"v2 is bigger").unwrap(); // 12 bytes
|
||||||
|
sync_files(&files).unwrap();
|
||||||
|
{
|
||||||
|
let cache = CLIP_FILES.lock();
|
||||||
|
let idx = cache.first_file_index;
|
||||||
|
assert_eq!(cache.file_list[idx].size, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_files(); // leave the global clean for other tests
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user