diff --git a/.cargo/config.toml b/.cargo/config.toml index d971518eb1dd..42a4adb55e1a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -12,3 +12,5 @@ rustflags = [ #rustflags = [ # "-C", "link-args=-Wl,-Bstatic -lc -Wl,-Bdynamic" #] +[net] +git-fetch-with-cli = true diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 05c79ae86a75..d4ace7578e24 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -100,6 +100,10 @@ class FileModel { fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); } + void receiveEmptyDirs(Map evt) { + fileFetcher.tryCompleteEmptyDirsTask(evt['value'], evt['is_local']); + } + Future postOverrideFileConfirm(Map evt) async { evtLoop.pushEvent( _FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt)); @@ -470,7 +474,8 @@ class FileController { } /// sendFiles from current side (FileController.isLocal) to other side (SelectedItems). - void sendFiles(SelectedItems items, DirectoryData otherSideData) { + Future sendFiles( + SelectedItems items, DirectoryData otherSideData) async { /// ignore wrong items side status if (items.isLocal != isLocal) { return; @@ -496,6 +501,42 @@ class FileController { debugPrint( "path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}"); } + + if (!isLocal && + versionCmp(rootState.target!.ffiModel.pi.version, '1.3.3') < 0) { + return; + } + + final List entrys = items.items.toList(); + var isRemote = isLocal == true ? true : false; + + await Future.forEach(entrys, (Entry item) async { + if (!item.isDirectory) { + return; + } + + final List paths = []; + + final emptyDirs = + await fileFetcher.readEmptyDirs(item.path, isLocal, showHidden); + + if (emptyDirs.isEmpty) { + return; + } else { + for (var dir in emptyDirs) { + paths.add(dir.path); + } + } + + final dirs = paths.map((path) { + return PathUtil.getOtherSidePath(directory.value.path, path, + options.value.isWindows, toPath, isWindows); + }); + + for (var dir in dirs) { + createDirWithRemote(dir, isRemote); + } + }); } bool _removeCheckboxRemember = false; @@ -689,12 +730,16 @@ class FileController { sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal); } - Future createDir(String path) async { + Future createDirWithRemote(String path, bool isRemote) async { bind.sessionCreateDir( sessionId: sessionId, actId: JobController.jobID.next(), path: path, - isRemote: !isLocal); + isRemote: isRemote); + } + + Future createDir(String path) async { + await createDirWithRemote(path, !isLocal); } Future renameAction(Entry item, bool isLocal) async { @@ -1064,6 +1109,7 @@ class JobResultListener { class FileFetcher { // Map> localTasks = {}; // now we only use read local dir sync Map> remoteTasks = {}; + Map>> remoteEmptyDirsTasks = {}; Map> readRecursiveTasks = {}; final GetSessionID getSessionID; @@ -1071,6 +1117,24 @@ class FileFetcher { FileFetcher(this.getSessionID); + Future> registerReadEmptyDirsTask( + bool isLocal, String path) { + // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later + final tasks = remoteEmptyDirsTasks; // bypass now + if (tasks.containsKey(path)) { + throw "Failed to registerReadEmptyDirsTask, already have same read job"; + } + final c = Completer>(); + tasks[path] = c; + + Timer(Duration(seconds: 2), () { + tasks.remove(path); + if (c.isCompleted) return; + c.completeError("Failed to read empty dirs, timeout"); + }); + return c.future; + } + Future registerReadTask(bool isLocal, String path) { // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later final tasks = remoteTasks; // bypass now @@ -1104,6 +1168,25 @@ class FileFetcher { return c.future; } + tryCompleteEmptyDirsTask(String? msg, String? isLocalStr) { + if (msg == null || isLocalStr == null) return; + late final Map>> tasks; + try { + final map = jsonDecode(msg); + final String path = map["path"]; + final List fdJsons = map["empty_dirs"]; + final List fds = + fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList(); + + tasks = remoteEmptyDirsTasks; + final completer = tasks.remove(path); + + completer?.complete(fds); + } catch (e) { + debugPrint("tryCompleteJob err: $e"); + } + } + tryCompleteTask(String? msg, String? isLocalStr) { if (msg == null || isLocalStr == null) return; late final Map> tasks; @@ -1127,6 +1210,28 @@ class FileFetcher { } } + Future> readEmptyDirs( + String path, bool isLocal, bool showHidden) async { + try { + if (isLocal) { + final res = await bind.sessionReadLocalEmptyDirsRecursiveSync( + sessionId: sessionId, path: path, includeHidden: showHidden); + + final List fdJsons = jsonDecode(res); + + final List fds = + fdJsons.map((fdJson) => FileDirectory.fromJson(fdJson)).toList(); + return fds; + } else { + await bind.sessionReadRemoteEmptyDirsRecursiveSync( + sessionId: sessionId, path: path, includeHidden: showHidden); + return registerReadEmptyDirsTask(isLocal, path); + } + } catch (e) { + return Future.error(e); + } + } + Future fetchDirectory( String path, bool isLocal, bool showHidden) async { try { @@ -1373,6 +1478,24 @@ class PathUtil { static final windowsContext = path.Context(style: path.Style.windows); static final posixContext = path.Context(style: path.Style.posix); + static String getOtherSidePath(String mainRootPath, String mainPath, + bool isMainWindows, String otherRootPath, bool isOtherWindows) { + final mainPathUtil = isMainWindows ? windowsContext : posixContext; + final relativePath = mainPathUtil.relative(mainPath, from: mainRootPath); + + final names = mainPathUtil.split(relativePath); + + final otherPathUtil = isOtherWindows ? windowsContext : posixContext; + + String path = otherRootPath; + + for (var name in names) { + path = otherPathUtil.join(path, name); + } + + return path; + } + static String join(String path1, String path2, bool isWindows) { final pathUtil = isWindows ? windowsContext : posixContext; return pathUtil.join(path1, path2); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 5446856fe5ab..d029aa3951ff 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -309,6 +309,8 @@ class FfiModel with ChangeNotifier { .receive(int.parse(evt['id'] as String), evt['text'] ?? ''); } else if (name == 'file_dir') { parent.target?.fileModel.receiveFileDir(evt); + } else if (name == 'empty_dirs') { + parent.target?.fileModel.receiveEmptyDirs(evt); } else if (name == 'job_progress') { parent.target?.fileModel.jobController.tryUpdateJobProgress(evt); } else if (name == 'job_done') { diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 21f9e7aea0de..090b378a22cc 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -368,6 +368,16 @@ message ReadDir { bool include_hidden = 2; } +message ReadEmptyDirs { + string path = 1; + bool include_hidden = 2; +} + +message ReadEmptyDirsResponse { + string path = 1; + repeated FileDirectory empty_dirs = 2; +} + message ReadAllFiles { int32 id = 1; string path = 2; @@ -392,6 +402,7 @@ message FileAction { FileTransferCancel cancel = 8; FileTransferSendConfirmRequest send_confirm = 9; FileRename rename = 10; + ReadEmptyDirs read_empty_dirs = 11; } } @@ -404,6 +415,7 @@ message FileResponse { FileTransferError error = 3; FileTransferDone done = 4; FileTransferDigest digest = 5; + ReadEmptyDirsResponse empty_dirs = 6; } } diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index 3f236fd3ae38..8031516972ea 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -185,6 +185,51 @@ pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType ResultType> { + let mut dirs = Vec::new(); + if path.is_dir() { + // to-do: symbol link handling, cp the link rather than the content + // to-do: file mode, for unix + let fd = read_dir(path, include_hidden)?; + if fd.entries.is_empty() { + dirs.push(fd); + } else { + for entry in fd.entries.iter() { + match entry.entry_type.enum_value() { + Ok(FileType::Dir) => { + if let Ok(mut tmp) = read_empty_dirs_recursive( + &path.join(&entry.name), + &prefix.join(&entry.name), + include_hidden, + ) { + for entry in tmp.drain(0..) { + dirs.push(entry); + } + } + } + _ => {} + } + } + } + Ok(dirs) + } else if path.is_file() { + Ok(dirs) + } else { + bail!("Not exists"); + } +} + +pub fn get_empty_dirs_recursive( + path: &str, + include_hidden: bool, +) -> ResultType> { + read_empty_dirs_recursive(&get_path(path), &get_path(""), include_hidden) +} + #[inline] pub fn is_file_exists(file_path: &str) -> bool { return Path::new(file_path).exists(); diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 71ddfb09cf4b..88f0b14a5d63 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -43,6 +43,18 @@ pub trait FileManager: Interface { self.send(Data::CancelJob(id)); } + fn read_empty_dirs(&self, path: String, include_hidden: bool) { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_read_empty_dirs(ReadEmptyDirs { + path, + include_hidden, + ..Default::default() + }); + msg_out.set_file_action(file_action); + self.send(Data::Message(msg_out)); + } + fn read_remote_dir(&self, path: String, include_hidden: bool) { let mut msg_out = Message::new(); let mut file_action = FileAction::new(); diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 8dfdffcfe1c6..df07331cfeac 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1299,6 +1299,9 @@ impl Remote { } Some(message::Union::FileResponse(fr)) => { match fr.union { + Some(file_response::Union::EmptyDirs(res)) => { + self.handler.update_empty_dirs(res); + } Some(file_response::Union::Dir(fd)) => { #[cfg(windows)] let entries = fd.entries.to_vec(); diff --git a/src/common.rs b/src/common.rs index b1f97e27bdde..294ab97cc4f7 100644 --- a/src/common.rs +++ b/src/common.rs @@ -5,7 +5,7 @@ use std::{ task::Poll, }; -use serde_json::Value; +use serde_json::{json, Map, Value}; use hbb_common::{ allow_err, @@ -1051,6 +1051,11 @@ pub fn get_supported_keyboard_modes(version: i64, peer_platform: &str) -> Vec) -> String { + let fd_json = _make_fd_to_json(id, path, entries); + serde_json::to_string(&fd_json).unwrap_or("".into()) +} + +pub fn _make_fd_to_json(id: i32, path: String, entries: &Vec) -> Map { use serde_json::json; let mut fd_json = serde_json::Map::new(); fd_json.insert("id".into(), json!(id)); @@ -1066,7 +1071,33 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin entries_out.push(entry_map); } fd_json.insert("entries".into(), json!(entries_out)); - serde_json::to_string(&fd_json).unwrap_or("".into()) + fd_json +} + +pub fn make_vec_fd_to_json(fds: &[FileDirectory]) -> String { + let mut fd_jsons = vec![]; + + for fd in fds.iter() { + let fd_json = _make_fd_to_json(fd.id, fd.path.clone(), &fd.entries); + fd_jsons.push(fd_json); + } + + serde_json::to_string(&fd_jsons).unwrap_or("".into()) +} + +pub fn make_empty_dirs_response_to_json(res: &ReadEmptyDirsResponse) -> String { + let mut map: Map = serde_json::Map::new(); + map.insert("path".into(), json!(res.path)); + + let mut fd_jsons = vec![]; + + for fd in res.empty_dirs.iter() { + let fd_json = _make_fd_to_json(fd.id, fd.path.clone(), &fd.entries); + fd_jsons.push(fd_json); + } + map.insert("empty_dirs".into(), fd_jsons.into()); + + serde_json::to_string(&map).unwrap_or("".into()) } /// The function to handle the url scheme sent by the system. diff --git a/src/flutter.rs b/src/flutter.rs index f6fff4234813..fe0a77e39d36 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -726,6 +726,20 @@ impl InvokeUiSession for FlutterHandler { } } + fn update_empty_dirs(&self, res: ReadEmptyDirsResponse) { + self.push_event( + "empty_dirs", + &[ + ("is_local", "false"), + ( + "value", + &crate::common::make_empty_dirs_response_to_json(&res), + ), + ], + &[], + ); + } + // unused in flutter fn update_transfer_list(&self) {} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 90dafccd027e..4c875be49b72 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,6 +1,6 @@ use crate::{ client::file_trait::FileManager, - common::make_fd_to_json, + common::{make_fd_to_json, make_vec_fd_to_json}, flutter::{ self, session_add, session_add_existed, session_start_, sessions, try_sync_peer_option, }, @@ -682,6 +682,27 @@ pub fn session_read_local_dir_sync( "".to_string() } +pub fn session_read_local_empty_dirs_recursive_sync( + _session_id: SessionID, + path: String, + include_hidden: bool, +) -> String { + if let Ok(fds) = fs::get_empty_dirs_recursive(&path, include_hidden) { + return make_vec_fd_to_json(&fds); + } + "".to_string() +} + +pub fn session_read_remote_empty_dirs_recursive_sync( + session_id: SessionID, + path: String, + include_hidden: bool, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.read_empty_dirs(path, include_hidden); + } +} + pub fn session_get_platform(session_id: SessionID, is_remote: bool) -> String { if let Some(session) = sessions::get_session_by_session_id(&session_id) { return session.get_platform(is_remote); diff --git a/src/ipc.rs b/src/ipc.rs index 81693a735587..e3bcfac9a49f 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -45,6 +45,10 @@ pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true); #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] pub enum FS { + ReadEmptyDirs { + dir: String, + include_hidden: bool, + }, ReadDir { dir: String, include_hidden: bool, diff --git a/src/server/connection.rs b/src/server/connection.rs index 4bdda795f0b3..1aa7d7e8ac49 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2149,6 +2149,9 @@ impl Connection { } } match fa.union { + Some(file_action::Union::ReadEmptyDirs(rd)) => { + self.read_empty_dirs(&rd.path, rd.include_hidden); + } Some(file_action::Union::ReadDir(rd)) => { self.read_dir(&rd.path, rd.include_hidden); } @@ -3145,6 +3148,14 @@ impl Connection { raii::AuthedConnID::check_remove_session(self.inner.id(), self.session_key()); } + fn read_empty_dirs(&mut self, dir: &str, include_hidden: bool) { + let dir = dir.to_string(); + self.send_fs(ipc::FS::ReadEmptyDirs { + dir, + include_hidden, + }); + } + fn read_dir(&mut self, dir: &str, include_hidden: bool) { let dir = dir.to_string(); self.send_fs(ipc::FS::ReadDir { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index c34671d57a56..2ff5e086287d 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -751,6 +751,12 @@ async fn handle_fs( use hbb_common::fs::serialize_transfer_job; match fs { + ipc::FS::ReadEmptyDirs { + dir, + include_hidden, + } => { + read_empty_dirs(&dir, include_hidden, tx).await; + } ipc::FS::ReadDir { dir, include_hidden, @@ -907,6 +913,26 @@ async fn handle_fs( } } +#[cfg(not(any(target_os = "ios")))] +async fn read_empty_dirs(dir: &str, include_hidden: bool, tx: &UnboundedSender) { + let path = dir.to_owned(); + let path_clone = dir.to_owned(); + + if let Ok(Ok(fds)) = + spawn_blocking(move || fs::get_empty_dirs_recursive(&path, include_hidden)).await + { + let mut msg_out = Message::new(); + let mut file_response = FileResponse::new(); + file_response.set_empty_dirs(ReadEmptyDirsResponse { + path: path_clone, + empty_dirs: fds, + ..Default::default() + }); + msg_out.set_file_response(file_response); + send_raw(msg_out, tx); + } +} + #[cfg(not(any(target_os = "ios")))] async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender) { let path = { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 5194b12db867..176426464149 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1555,6 +1555,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { #[cfg(feature = "flutter")] fn is_multi_ui_session(&self) -> bool; fn update_record_status(&self, start: bool); + fn update_empty_dirs(&self, _res: ReadEmptyDirsResponse) {} } impl Deref for Session {