ladybird/Userland/Libraries/LibFileSystemAccessClient/Client.cpp
thankyouverycool 37e621a3c7 LibFileSystemAccessClient: Improve error propagation
Previously FSAC displayed some but not all errors and always
rejected directories and devices. This has led most apps to ignore
response errors in open/save actions or show redundant messages.

Now FSAC displays all errors including fd failures and has the ability
to silence messages for directories, devices and ENOENT, which some
apps handle differently. Silenced directory and device errors now
return files on success.

A request's access mode is now stored in RequestData to format more
accurate error messages from the user's perspective.

Resolved promises don't require callback propagation so they're voided
2023-05-19 06:20:41 +02:00

193 lines
8 KiB
C++

/*
* Copyright (c) 2021, timmot <tiwwot@protonmail.com>
* Copyright (c) 2022, Mustafa Quraish <mustafa@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/LexicalPath.h>
#include <LibFileSystem/FileSystem.h>
#include <LibFileSystemAccessClient/Client.h>
#include <LibGUI/ConnectionToWindowServer.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Window.h>
namespace FileSystemAccessClient {
static RefPtr<Client> s_the = nullptr;
Client& Client::the()
{
if (!s_the || !s_the->is_open())
s_the = Client::try_create().release_value_but_fixme_should_propagate_errors();
return *s_the;
}
Result Client::request_file_read_only_approved(GUI::Window* parent_window, DeprecatedString const& path)
{
auto const id = get_new_id();
m_promises.set(id, RequestData { { Core::Promise<Result>::construct() }, parent_window, Core::File::OpenMode::Read });
auto parent_window_server_client_id = GUI::ConnectionToWindowServer::the().expose_client_id();
auto child_window_server_client_id = expose_window_server_client_id();
auto parent_window_id = parent_window->window_id();
GUI::ConnectionToWindowServer::the().add_window_stealing_for_client(child_window_server_client_id, parent_window_id);
ScopeGuard guard([parent_window_id, child_window_server_client_id] {
GUI::ConnectionToWindowServer::the().remove_window_stealing_for_client(child_window_server_client_id, parent_window_id);
});
if (path.starts_with('/')) {
async_request_file_read_only_approved(id, parent_window_server_client_id, parent_window_id, path);
} else {
auto full_path = LexicalPath::join(TRY(FileSystem::current_working_directory()), path).string();
async_request_file_read_only_approved(id, parent_window_server_client_id, parent_window_id, full_path);
}
return handle_promise(id);
}
Result Client::request_file(GUI::Window* parent_window, DeprecatedString const& path, Core::File::OpenMode mode)
{
auto const id = get_new_id();
m_promises.set(id, RequestData { { Core::Promise<Result>::construct() }, parent_window, mode });
auto parent_window_server_client_id = GUI::ConnectionToWindowServer::the().expose_client_id();
auto child_window_server_client_id = expose_window_server_client_id();
auto parent_window_id = parent_window->window_id();
GUI::ConnectionToWindowServer::the().add_window_stealing_for_client(child_window_server_client_id, parent_window_id);
ScopeGuard guard([parent_window_id, child_window_server_client_id] {
GUI::ConnectionToWindowServer::the().remove_window_stealing_for_client(child_window_server_client_id, parent_window_id);
});
if (path.starts_with('/')) {
async_request_file(id, parent_window_server_client_id, parent_window_id, path, mode);
} else {
auto full_path = LexicalPath::join(TRY(FileSystem::current_working_directory()), path).string();
async_request_file(id, parent_window_server_client_id, parent_window_id, full_path, mode);
}
return handle_promise(id);
}
Result Client::open_file(GUI::Window* parent_window, DeprecatedString const& window_title, StringView path, Core::File::OpenMode requested_access, Optional<Vector<GUI::FileTypeFilter>> const& allowed_file_types)
{
auto const id = get_new_id();
m_promises.set(id, RequestData { { Core::Promise<Result>::construct() }, parent_window, requested_access });
auto parent_window_server_client_id = GUI::ConnectionToWindowServer::the().expose_client_id();
auto child_window_server_client_id = expose_window_server_client_id();
auto parent_window_id = parent_window->window_id();
GUI::ConnectionToWindowServer::the().add_window_stealing_for_client(child_window_server_client_id, parent_window_id);
ScopeGuard guard([parent_window_id, child_window_server_client_id] {
GUI::ConnectionToWindowServer::the().remove_window_stealing_for_client(child_window_server_client_id, parent_window_id);
});
async_prompt_open_file(id, parent_window_server_client_id, parent_window_id, window_title, path, requested_access, allowed_file_types);
return handle_promise(id);
}
Result Client::save_file(GUI::Window* parent_window, DeprecatedString const& name, DeprecatedString const ext, Core::File::OpenMode requested_access)
{
auto const id = get_new_id();
m_promises.set(id, RequestData { { Core::Promise<Result>::construct() }, parent_window, requested_access });
auto parent_window_server_client_id = GUI::ConnectionToWindowServer::the().expose_client_id();
auto child_window_server_client_id = expose_window_server_client_id();
auto parent_window_id = parent_window->window_id();
GUI::ConnectionToWindowServer::the().add_window_stealing_for_client(child_window_server_client_id, parent_window_id);
ScopeGuard guard([parent_window_id, child_window_server_client_id] {
GUI::ConnectionToWindowServer::the().remove_window_stealing_for_client(child_window_server_client_id, parent_window_id);
});
async_prompt_save_file(id, parent_window_server_client_id, parent_window_id, name.is_null() ? "Untitled" : name, ext.is_null() ? "txt" : ext, Core::StandardPaths::home_directory(), requested_access);
return handle_promise(id);
}
void Client::handle_prompt_end(i32 request_id, i32 error, Optional<IPC::File> const& ipc_file, Optional<DeprecatedString> const& chosen_file)
{
auto potential_data = m_promises.get(request_id);
VERIFY(potential_data.has_value());
auto& request_data = potential_data.value();
auto action = "Requesting"sv;
if (has_flag(request_data.mode, Core::File::OpenMode::Read))
action = "Opening"sv;
else if (has_flag(request_data.mode, Core::File::OpenMode::Write))
action = "Saving"sv;
if (ipc_file.has_value()) {
if (FileSystem::is_device(ipc_file->fd()))
error = is_silencing_devices() ? ESUCCESS : EINVAL;
else if (FileSystem::is_directory(ipc_file->fd()))
error = is_silencing_directories() ? ESUCCESS : EISDIR;
}
switch (error) {
case ESUCCESS:
case ECANCELED:
break;
case ENOENT:
if (is_silencing_nonexistent_entries())
break;
[[fallthrough]];
default:
auto maybe_message = ErrorOr<String>({});
if (error == ECONNRESET)
maybe_message = String::formatted("FileSystemAccessClient: {}", Error::from_errno(error));
else
maybe_message = String::formatted("{} \"{}\" failed: {}", action, *chosen_file, Error::from_errno(error));
if (!maybe_message.is_error())
(void)GUI::MessageBox::try_show_error(request_data.parent_window, maybe_message.release_value());
}
if (error != ESUCCESS)
return (void)request_data.promise->resolve(Error::from_errno(error));
auto file_or_error = [&]() -> ErrorOr<File> {
auto stream = TRY(Core::File::adopt_fd(ipc_file->take_fd(), Core::File::OpenMode::ReadWrite));
auto filename = TRY(String::from_deprecated_string(*chosen_file));
return File({}, move(stream), filename);
}();
if (file_or_error.is_error()) {
auto maybe_message = String::formatted("{} \"{}\" failed: {}", action, *chosen_file, file_or_error.error());
if (!maybe_message.is_error())
(void)GUI::MessageBox::try_show_error(request_data.parent_window, maybe_message.release_value());
return (void)request_data.promise->resolve(file_or_error.release_error());
}
(void)request_data.promise->resolve(file_or_error.release_value());
}
void Client::die()
{
for (auto const& entry : m_promises)
handle_prompt_end(entry.key, ECONNRESET, {}, "");
}
int Client::get_new_id()
{
auto const new_id = m_last_id++;
// Note: This verify shouldn't fail, and we should provide a valid ID
// But we probably have more issues if this test fails.
VERIFY(!m_promises.contains(new_id));
return new_id;
}
Result Client::handle_promise(int id)
{
auto result = TRY(m_promises.get(id)->promise->await());
m_promises.remove(id);
return result;
}
}