浏览代码

Applets/ClipboardHistory: Add persistent storage

Clipboard entries are now preserved upon reboot :^). Unfortunately, it
only supports data with the mimetype "text/".

This is done by writing all entries as a JSON object in a file located
in ~/.data.

Co-authored-by: Sagittarius-a <sagittarius-a@users.noreply.github.com>
Lucas CHOLLET 2 年之前
父节点
当前提交
07c6cebbab

+ 92 - 5
Userland/Applets/ClipboardHistory/ClipboardHistoryModel.cpp

@@ -7,6 +7,7 @@
  */
 
 #include "ClipboardHistoryModel.h"
+#include <AK/JsonParser.h>
 #include <AK/NumberFormat.h>
 #include <AK/StringBuilder.h>
 #include <LibConfig/Client.h>
@@ -117,29 +118,40 @@ void ClipboardHistoryModel::clipboard_content_did_change(DeprecatedString const&
         add_item(data_and_type);
 }
 
+ErrorOr<void> ClipboardHistoryModel::invalidate_model_and_file(bool rewrite_all)
+{
+    invalidate();
+
+    TRY(write_to_file(rewrite_all));
+    return {};
+}
+
 void ClipboardHistoryModel::add_item(const GUI::Clipboard::DataAndType& item)
 {
+    bool has_deleted_an_item = false;
     m_history_items.remove_first_matching([&](ClipboardItem& existing) {
         return existing.data_and_type.data == item.data && existing.data_and_type.mime_type == item.mime_type;
     });
 
-    if (m_history_items.size() == m_history_limit)
+    if (m_history_items.size() == m_history_limit) {
         m_history_items.take_last();
+        has_deleted_an_item = true;
+    }
 
     m_history_items.prepend({ item, Core::DateTime::now() });
-    invalidate();
+    invalidate_model_and_file(has_deleted_an_item).release_value_but_fixme_should_propagate_errors();
 }
 
 void ClipboardHistoryModel::remove_item(int index)
 {
     m_history_items.remove(index);
-    invalidate();
+    invalidate_model_and_file(true).release_value_but_fixme_should_propagate_errors();
 }
 
 void ClipboardHistoryModel::clear()
 {
     m_history_items.clear();
-    invalidate();
+    invalidate_model_and_file(true).release_value_but_fixme_should_propagate_errors();
 }
 
 void ClipboardHistoryModel::config_string_did_change(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key, DeprecatedString const& value_string)
@@ -155,9 +167,84 @@ void ClipboardHistoryModel::config_string_did_change(DeprecatedString const& dom
         auto value = value_or_error.value();
         if (value < (int)m_history_items.size()) {
             m_history_items.remove(value, m_history_items.size() - value);
-            invalidate();
+            invalidate_model_and_file(false).release_value_but_fixme_should_propagate_errors();
         }
         m_history_limit = value;
         return;
     }
 }
+
+ErrorOr<ClipboardHistoryModel::ClipboardItem> ClipboardHistoryModel::ClipboardItem::from_json(JsonObject const& object)
+{
+    if (!object.has("data_and_type"sv) && !object.has("time"sv))
+        return Error::from_string_literal("JsonObject does not contain necessary fields");
+
+    ClipboardItem result;
+    result.data_and_type = TRY(GUI::Clipboard::DataAndType::from_json(*object.get_object("data_and_type"sv)));
+    result.time = Core::DateTime::from_timestamp(*object.get_integer<time_t>("time"sv));
+
+    return result;
+}
+
+ErrorOr<JsonObject> ClipboardHistoryModel::ClipboardItem::to_json() const
+{
+    JsonObject object;
+
+    object.set("data_and_type", TRY(data_and_type.to_json()));
+    object.set("time", time.timestamp());
+
+    return object;
+}
+
+ErrorOr<void> ClipboardHistoryModel::read_from_file(DeprecatedString const& path)
+{
+    m_path = path;
+
+    auto read_from_file_impl = [this]() -> ErrorOr<void> {
+        auto file = TRY(Core::File::open(m_path, Core::File::OpenMode::Read));
+        auto buffered_file = TRY(Core::BufferedFile::create(move(file)));
+
+        auto buffer = TRY(ByteBuffer::create_uninitialized(PAGE_SIZE));
+
+        while (TRY(buffered_file->can_read_line())) {
+            auto line = TRY(buffered_file->read_line(buffer));
+            auto object = TRY(JsonParser { line }.parse()).as_object();
+            TRY(m_history_items.try_append(TRY(ClipboardItem::from_json(object))));
+        }
+        return {};
+    };
+
+    auto maybe_error = read_from_file_impl();
+    if (maybe_error.is_error())
+        dbgln("Unable to load clipboard history: {}", maybe_error.release_error());
+
+    return {};
+}
+
+ErrorOr<void> ClipboardHistoryModel::write_to_file(bool rewrite_all)
+{
+    if (m_history_items.is_empty()) {
+        // This will proceed to empty the file
+        rewrite_all = true;
+    }
+
+    auto const write_element = [](Core::File& file, ClipboardItem const& item) -> ErrorOr<void> {
+        if (!item.data_and_type.mime_type.starts_with("text/"sv))
+            return {};
+        TRY(file.write_until_depleted(TRY(item.to_json()).to_deprecated_string().bytes()));
+        TRY(file.write_until_depleted("\n"sv.bytes()));
+        return {};
+    };
+
+    if (!rewrite_all) {
+        auto file = TRY(Core::File::open(m_path, Core::File::OpenMode::Write | Core::File::OpenMode::Append));
+        TRY(write_element(*file, m_history_items.first()));
+    } else {
+        auto file = TRY(Core::File::open(m_path, Core::File::OpenMode::Write | Core::File::OpenMode::Truncate));
+        for (auto const& item : m_history_items) {
+            TRY(write_element(*file, item));
+        }
+    }
+
+    return {};
+}

+ 10 - 0
Userland/Applets/ClipboardHistory/ClipboardHistoryModel.h

@@ -31,6 +31,9 @@ public:
     struct ClipboardItem {
         GUI::Clipboard::DataAndType data_and_type;
         Core::DateTime time;
+
+        static ErrorOr<ClipboardItem> from_json(JsonObject const& object);
+        ErrorOr<JsonObject> to_json() const;
     };
 
     virtual ~ClipboardHistoryModel() override = default;
@@ -41,6 +44,11 @@ public:
     void clear();
     bool is_empty() { return m_history_items.is_empty(); }
 
+    ErrorOr<void> read_from_file(DeprecatedString const& path);
+    ErrorOr<void> write_to_file(bool rewrite_all);
+
+    ErrorOr<void> invalidate_model_and_file(bool rewrite_all);
+
     // ^GUI::Model
     virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
 
@@ -60,4 +68,6 @@ private:
 
     Vector<ClipboardItem> m_history_items;
     size_t m_history_limit;
+
+    DeprecatedString m_path;
 };

+ 14 - 2
Userland/Applets/ClipboardHistory/main.cpp

@@ -6,6 +6,8 @@
 
 #include "ClipboardHistoryModel.h"
 #include <LibConfig/Client.h>
+#include <LibCore/Directory.h>
+#include <LibCore/StandardPaths.h>
 #include <LibCore/System.h>
 #include <LibGUI/Action.h>
 #include <LibGUI/Application.h>
@@ -17,14 +19,22 @@
 
 ErrorOr<int> serenity_main(Main::Arguments arguments)
 {
-    TRY(Core::System::pledge("stdio recvfd sendfd rpath unix"));
+    TRY(Core::System::pledge("stdio recvfd sendfd rpath unix cpath wpath"));
     auto app = TRY(GUI::Application::create(arguments));
+    auto clipboard_config = TRY(Core::ConfigFile::open_for_app("ClipboardHistory"));
+
+    auto const default_path = DeprecatedString::formatted("{}/{}", Core::StandardPaths::data_directory(), "Clipboard/ClipboardHistory.json"sv);
+    auto const clipboard_file_path = clipboard_config->read_entry("Clipboard", "ClipboardFilePath", default_path);
+    auto const parent_path = LexicalPath(clipboard_file_path);
+    TRY(Core::Directory::create(parent_path.dirname(), Core::Directory::CreateDirectories::Yes));
 
     Config::pledge_domain("ClipboardHistory");
     Config::monitor_domain("ClipboardHistory");
 
-    TRY(Core::System::pledge("stdio recvfd sendfd rpath"));
+    TRY(Core::System::pledge("stdio recvfd sendfd rpath cpath wpath"));
     TRY(Core::System::unveil("/res", "r"));
+    TRY(Core::System::unveil(parent_path.dirname(), "rwc"sv));
+
     TRY(Core::System::unveil(nullptr, nullptr));
     auto app_icon = TRY(GUI::Icon::try_create_default_icon("edit-copy"sv));
 
@@ -36,6 +46,8 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
     auto table_view = TRY(main_window->set_main_widget<GUI::TableView>());
     auto model = ClipboardHistoryModel::create();
 
+    TRY(model->read_from_file(clipboard_file_path));
+
     auto data_and_type = GUI::Clipboard::the().fetch_data_and_type();
     if (!(data_and_type.data.is_empty() && data_and_type.mime_type.is_empty() && data_and_type.metadata.is_empty()))
         model->add_item(data_and_type);

+ 23 - 0
Userland/Libraries/LibGUI/Clipboard.cpp

@@ -124,6 +124,29 @@ RefPtr<Gfx::Bitmap> Clipboard::DataAndType::as_bitmap() const
     return bitmap;
 }
 
+ErrorOr<Clipboard::DataAndType> Clipboard::DataAndType::from_json(JsonObject const& object)
+{
+    if (!object.has("data"sv) && !object.has("mime_type"sv))
+        return Error::from_string_literal("JsonObject does not contain necessary fields");
+
+    DataAndType result;
+    result.data = object.get_deprecated_string("data"sv)->to_byte_buffer();
+    result.mime_type = *object.get_deprecated_string("mime_type"sv);
+    // FIXME: Also read metadata
+
+    return result;
+}
+
+ErrorOr<JsonObject> Clipboard::DataAndType::to_json() const
+{
+    JsonObject object;
+    object.set("data", TRY(DeprecatedString::from_utf8(data.bytes())));
+    object.set("mime_type", mime_type);
+    // FIXME: Also write metadata
+
+    return object;
+}
+
 void Clipboard::set_data(ReadonlyBytes data, DeprecatedString const& type, HashMap<DeprecatedString, DeprecatedString> const& metadata)
 {
     if (data.is_empty()) {

+ 4 - 0
Userland/Libraries/LibGUI/Clipboard.h

@@ -11,6 +11,7 @@
 #include <AK/DeprecatedString.h>
 #include <AK/Function.h>
 #include <AK/HashMap.h>
+#include <AK/JsonObject.h>
 #include <LibGUI/Forward.h>
 #include <LibGfx/Forward.h>
 
@@ -34,6 +35,9 @@ public:
         HashMap<DeprecatedString, DeprecatedString> metadata;
 
         RefPtr<Gfx::Bitmap> as_bitmap() const;
+
+        static ErrorOr<Clipboard::DataAndType> from_json(JsonObject const& object);
+        ErrorOr<JsonObject> to_json() const;
     };
 
     static ErrorOr<void> initialize(Badge<Application>);