feat: Add empty dirs on sendfiles

This commit is contained in:
zuiyu1998 2024-11-20 19:52:18 +08:00
parent 8b710f62c8
commit f6f419761d
13 changed files with 314 additions and 7 deletions

View File

@ -100,6 +100,10 @@ class FileModel {
fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
}
void receiveEmptyDirs(Map<String, dynamic> evt) {
fileFetcher.tryCompleteEmptyDirsTask(evt['value'], evt['is_local']);
}
Future<void> postOverrideFileConfirm(Map<String, dynamic> 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<void> 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<Entry> entrys = items.items.toList();
var isRemote = isLocal == true ? true : false;
await Future.forEach(entrys, (Entry item) async {
if (!item.isDirectory) {
return;
}
final List<String> 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<void> createDir(String path) async {
Future<void> createDirWithRemote(String path, bool isRemote) async {
bind.sessionCreateDir(
sessionId: sessionId,
actId: JobController.jobID.next(),
path: path,
isRemote: !isLocal);
isRemote: isRemote);
}
Future<void> createDir(String path) async {
await createDirWithRemote(path, !isLocal);
}
Future<void> renameAction(Entry item, bool isLocal) async {
@ -1064,6 +1109,7 @@ class JobResultListener<T> {
class FileFetcher {
// Map<String,Completer<FileDirectory>> localTasks = {}; // now we only use read local dir sync
Map<String, Completer<FileDirectory>> remoteTasks = {};
Map<String, Completer<List<FileDirectory>>> remoteEmptyDirsTasks = {};
Map<int, Completer<FileDirectory>> readRecursiveTasks = {};
final GetSessionID getSessionID;
@ -1071,6 +1117,24 @@ class FileFetcher {
FileFetcher(this.getSessionID);
Future<List<FileDirectory>> 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<List<FileDirectory>>();
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<FileDirectory> 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<String, Completer<List<FileDirectory>>> tasks;
try {
final map = jsonDecode(msg);
final String path = map["path"];
final List<dynamic> fdJsons = map["empty_dirs"];
final List<FileDirectory> 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<Object, Completer<FileDirectory>> tasks;
@ -1127,6 +1210,28 @@ class FileFetcher {
}
}
Future<List<FileDirectory>> readEmptyDirs(
String path, bool isLocal, bool showHidden) async {
try {
if (isLocal) {
final res = await bind.sessionReadLocalEmptyDirsRecursiveSync(
sessionId: sessionId, path: path, includeHidden: showHidden);
final List<dynamic> fdJsons = jsonDecode(res);
final List<FileDirectory> 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<FileDirectory> 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);

View File

@ -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') {

View File

@ -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;
}
}

View File

@ -185,6 +185,51 @@ pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType<Vec<F
read_dir_recursive(&get_path(path), &get_path(""), include_hidden)
}
fn read_empty_dirs_recursive(
path: &PathBuf,
prefix: &Path,
include_hidden: bool,
) -> ResultType<Vec<FileDirectory>> {
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<Vec<FileDirectory>> {
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();

View File

@ -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();

View File

@ -1274,6 +1274,9 @@ impl<T: InvokeUiSession> Remote<T> {
}
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();

View File

@ -5,7 +5,7 @@ use std::{
task::Poll,
};
use serde_json::Value;
use serde_json::{json, Map, Value};
use hbb_common::{
allow_err,
@ -837,7 +837,7 @@ async fn check_software_update_() -> hbb_common::ResultType<()> {
let _ = crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, data);
}
}
*SOFTWARE_UPDATE_URL.lock().unwrap() = response_url;
*SOFTWARE_UPDATE_URL.lock().unwrap() = response_url;
}
Ok(())
}
@ -1051,6 +1051,11 @@ pub fn get_supported_keyboard_modes(version: i64, peer_platform: &str) -> Vec<Ke
}
pub fn make_fd_to_json(id: i32, path: String, entries: &Vec<FileEntry>) -> 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<FileEntry>) -> Map<String, Value> {
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<FileEntry>) -> 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<String, Value> = 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.

View File

@ -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) {}

View File

@ -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);

View File

@ -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,

View File

@ -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,16 @@ impl Connection {
raii::AuthedConnID::check_remove_session(self.inner.id(), self.session_key());
}
fn read_empty_dirs(&mut self, dir: &str, include_hidden: bool) {
log::debug!("empty_dirs path is: {}", dir);
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 {

View File

@ -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<Data>) {
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<Data>) {
let path = {

View File

@ -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<T: InvokeUiSession> Deref for Session<T> {