main.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. /*
  2. * Copyright (c) 2021, the SerenityOS developers.
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include "TreeMapWidget.h"
  7. #include <AK/LexicalPath.h>
  8. #include <AK/Queue.h>
  9. #include <AK/QuickSort.h>
  10. #include <AK/URL.h>
  11. #include <Applications/SpaceAnalyzer/SpaceAnalyzerGML.h>
  12. #include <LibCore/DirIterator.h>
  13. #include <LibCore/File.h>
  14. #include <LibDesktop/Launcher.h>
  15. #include <LibGUI/Application.h>
  16. #include <LibGUI/BoxLayout.h>
  17. #include <LibGUI/Breadcrumbbar.h>
  18. #include <LibGUI/Clipboard.h>
  19. #include <LibGUI/FileIconProvider.h>
  20. #include <LibGUI/Icon.h>
  21. #include <LibGUI/Label.h>
  22. #include <LibGUI/Menu.h>
  23. #include <LibGUI/Menubar.h>
  24. #include <LibGUI/MessageBox.h>
  25. #include <LibGUI/Statusbar.h>
  26. #include <fcntl.h>
  27. #include <sys/stat.h>
  28. #include <unistd.h>
  29. static const char* APP_NAME = "Space Analyzer";
  30. static constexpr size_t FILES_ENCOUNTERED_UPDATE_STEP_SIZE = 25;
  31. struct TreeNode : public SpaceAnalyzer::TreeMapNode {
  32. TreeNode(String name)
  33. : m_name(move(name)) {};
  34. virtual String name() const override { return m_name; }
  35. virtual i64 area() const override { return m_area; }
  36. virtual size_t num_children() const override
  37. {
  38. if (m_children) {
  39. return m_children->size();
  40. }
  41. return 0;
  42. }
  43. virtual const TreeNode& child_at(size_t i) const override { return m_children->at(i); }
  44. virtual void sort_children_by_area() const override
  45. {
  46. if (m_children) {
  47. Vector<TreeNode>* children = const_cast<Vector<TreeNode>*>(m_children.ptr());
  48. quick_sort(*children, [](auto& a, auto& b) { return b.m_area < a.m_area; });
  49. }
  50. }
  51. String m_name;
  52. i64 m_area { 0 };
  53. OwnPtr<Vector<TreeNode>> m_children;
  54. };
  55. struct Tree : public SpaceAnalyzer::TreeMap {
  56. Tree(String root_name)
  57. : m_root(move(root_name)) {};
  58. virtual ~Tree() {};
  59. TreeNode m_root;
  60. virtual const SpaceAnalyzer::TreeMapNode& root() const override
  61. {
  62. return m_root;
  63. };
  64. };
  65. struct MountInfo {
  66. String mount_point;
  67. String source;
  68. };
  69. static void fill_mounts(Vector<MountInfo>& output)
  70. {
  71. // Output info about currently mounted filesystems.
  72. auto file = Core::File::construct("/proc/df");
  73. if (!file->open(Core::OpenMode::ReadOnly)) {
  74. warnln("Failed to open {}: {}", file->name(), file->error_string());
  75. return;
  76. }
  77. auto content = file->read_all();
  78. auto json = JsonValue::from_string(content).release_value_but_fixme_should_propagate_errors();
  79. json.as_array().for_each([&output](auto& value) {
  80. auto& filesystem_object = value.as_object();
  81. MountInfo mount_info;
  82. mount_info.mount_point = filesystem_object.get("mount_point").to_string();
  83. mount_info.source = filesystem_object.get("source").as_string_or("none");
  84. output.append(mount_info);
  85. });
  86. }
  87. static MountInfo* find_mount_for_path(String path, Vector<MountInfo>& mounts)
  88. {
  89. MountInfo* result = nullptr;
  90. size_t length = 0;
  91. for (auto& mount_info : mounts) {
  92. String& mount_point = mount_info.mount_point;
  93. if (path.starts_with(mount_point)) {
  94. if (!result || mount_point.length() > length) {
  95. result = &mount_info;
  96. length = mount_point.length();
  97. }
  98. }
  99. }
  100. return result;
  101. }
  102. static long long int update_totals(TreeNode& node)
  103. {
  104. long long int result = 0;
  105. if (node.m_children) {
  106. for (auto& child : *node.m_children) {
  107. result += update_totals(child);
  108. }
  109. node.m_area = result;
  110. } else {
  111. result = node.m_area;
  112. }
  113. return result;
  114. }
  115. static NonnullRefPtr<GUI::Window> create_progress_window()
  116. {
  117. auto window = GUI::Window::construct();
  118. window->set_title(APP_NAME);
  119. window->set_resizable(false);
  120. window->set_closeable(false);
  121. window->resize(240, 50);
  122. window->center_on_screen();
  123. auto& main_widget = window->set_main_widget<GUI::Widget>();
  124. main_widget.set_fill_with_background_color(true);
  125. main_widget.set_layout<GUI::VerticalBoxLayout>();
  126. auto& label = main_widget.add<GUI::Label>("Analyzing storage space...");
  127. label.set_fixed_height(22);
  128. auto& progresslabel = main_widget.add<GUI::Label>();
  129. progresslabel.set_name("progresslabel");
  130. progresslabel.set_fixed_height(22);
  131. return window;
  132. }
  133. static void update_progress_label(GUI::Label& progresslabel, size_t files_encountered_count)
  134. {
  135. auto text = String::formatted("{} files...", files_encountered_count);
  136. progresslabel.set_text(text);
  137. Core::EventLoop::current().pump(Core::EventLoop::WaitMode::PollForEvents);
  138. }
  139. struct QueueEntry {
  140. QueueEntry(String path, TreeNode* node)
  141. : path(move(path))
  142. , node(node) {};
  143. String path;
  144. TreeNode* node { nullptr };
  145. };
  146. static void populate_filesize_tree(TreeNode& root, Vector<MountInfo>& mounts, HashMap<int, int>& error_accumulator, GUI::Label& progresslabel)
  147. {
  148. VERIFY(!root.m_name.ends_with("/"));
  149. Queue<QueueEntry> queue;
  150. queue.enqueue(QueueEntry(root.m_name, &root));
  151. size_t files_encountered_count = 0;
  152. StringBuilder builder = StringBuilder();
  153. builder.append(root.m_name);
  154. builder.append("/");
  155. MountInfo* root_mount_info = find_mount_for_path(builder.to_string(), mounts);
  156. if (!root_mount_info) {
  157. return;
  158. }
  159. while (!queue.is_empty()) {
  160. QueueEntry queue_entry = queue.dequeue();
  161. builder.clear();
  162. builder.append(queue_entry.path);
  163. builder.append("/");
  164. MountInfo* mount_info = find_mount_for_path(builder.to_string(), mounts);
  165. if (!mount_info || (mount_info != root_mount_info && mount_info->source != root_mount_info->source)) {
  166. continue;
  167. }
  168. Core::DirIterator dir_iterator(builder.to_string(), Core::DirIterator::SkipParentAndBaseDir);
  169. if (dir_iterator.has_error()) {
  170. int error_sum = error_accumulator.get(dir_iterator.error()).value_or(0);
  171. error_accumulator.set(dir_iterator.error(), error_sum + 1);
  172. } else {
  173. queue_entry.node->m_children = make<Vector<TreeNode>>();
  174. while (dir_iterator.has_next()) {
  175. queue_entry.node->m_children->append(TreeNode(dir_iterator.next_path()));
  176. }
  177. for (auto& child : *queue_entry.node->m_children) {
  178. files_encountered_count += 1;
  179. if (!(files_encountered_count % FILES_ENCOUNTERED_UPDATE_STEP_SIZE))
  180. update_progress_label(progresslabel, files_encountered_count);
  181. String& name = child.m_name;
  182. int name_len = name.length();
  183. builder.append(name);
  184. struct stat st;
  185. int stat_result = fstatat(dir_iterator.fd(), name.characters(), &st, AT_SYMLINK_NOFOLLOW);
  186. if (stat_result < 0) {
  187. int error_sum = error_accumulator.get(errno).value_or(0);
  188. error_accumulator.set(errno, error_sum + 1);
  189. } else {
  190. if (S_ISDIR(st.st_mode)) {
  191. queue.enqueue(QueueEntry(builder.to_string(), &child));
  192. } else {
  193. child.m_area = st.st_size;
  194. }
  195. }
  196. builder.trim(name_len);
  197. }
  198. }
  199. }
  200. update_totals(root);
  201. }
  202. static void analyze(RefPtr<Tree> tree, SpaceAnalyzer::TreeMapWidget& treemapwidget, GUI::Statusbar& statusbar)
  203. {
  204. statusbar.set_text("");
  205. auto progress_window = create_progress_window();
  206. progress_window->show();
  207. auto& progresslabel = *progress_window->main_widget()->find_descendant_of_type_named<GUI::Label>("progresslabel");
  208. update_progress_label(progresslabel, 0);
  209. // Build an in-memory tree mirroring the filesystem and for each node
  210. // calculate the sum of the file size for all its descendants.
  211. TreeNode* root = &tree->m_root;
  212. Vector<MountInfo> mounts;
  213. fill_mounts(mounts);
  214. HashMap<int, int> error_accumulator;
  215. populate_filesize_tree(*root, mounts, error_accumulator, progresslabel);
  216. progress_window->close();
  217. // Display an error summary in the statusbar.
  218. if (!error_accumulator.is_empty()) {
  219. StringBuilder builder;
  220. bool first = true;
  221. builder.append("Some directories were not analyzed: ");
  222. for (auto& key : error_accumulator.keys()) {
  223. if (!first) {
  224. builder.append(", ");
  225. }
  226. builder.append(strerror(key));
  227. builder.append(" (");
  228. int value = error_accumulator.get(key).value();
  229. builder.append(String::number(value));
  230. if (value == 1) {
  231. builder.append(" time");
  232. } else {
  233. builder.append(" times");
  234. }
  235. builder.append(")");
  236. first = false;
  237. }
  238. statusbar.set_text(builder.to_string());
  239. } else {
  240. statusbar.set_text("No errors");
  241. }
  242. treemapwidget.set_tree(tree);
  243. }
  244. static bool is_removable(const String& absolute_path)
  245. {
  246. VERIFY(!absolute_path.is_empty());
  247. int access_result = access(LexicalPath::dirname(absolute_path).characters(), W_OK);
  248. if (access_result != 0 && errno != EACCES)
  249. perror("access");
  250. return access_result == 0;
  251. }
  252. static String get_absolute_path_to_selected_node(const SpaceAnalyzer::TreeMapWidget& treemapwidget, bool include_last_node = true)
  253. {
  254. StringBuilder path_builder;
  255. for (size_t k = 0; k < treemapwidget.path_size() - (include_last_node ? 0 : 1); k++) {
  256. if (k != 0) {
  257. path_builder.append('/');
  258. }
  259. const SpaceAnalyzer::TreeMapNode* node = treemapwidget.path_node(k);
  260. path_builder.append(node->name());
  261. }
  262. return path_builder.build();
  263. }
  264. int main(int argc, char* argv[])
  265. {
  266. auto app = GUI::Application::construct(argc, argv);
  267. RefPtr<Tree> tree = adopt_ref(*new Tree(""));
  268. // Configure application window.
  269. auto app_icon = GUI::Icon::default_icon("app-space-analyzer");
  270. auto window = GUI::Window::construct();
  271. window->set_title(APP_NAME);
  272. window->resize(640, 480);
  273. window->set_icon(app_icon.bitmap_for_size(16));
  274. // Load widgets.
  275. auto& mainwidget = window->set_main_widget<GUI::Widget>();
  276. mainwidget.load_from_gml(space_analyzer_gml);
  277. auto& breadcrumbbar = *mainwidget.find_descendant_of_type_named<GUI::Breadcrumbbar>("breadcrumbbar");
  278. auto& treemapwidget = *mainwidget.find_descendant_of_type_named<SpaceAnalyzer::TreeMapWidget>("tree_map");
  279. auto& statusbar = *mainwidget.find_descendant_of_type_named<GUI::Statusbar>("statusbar");
  280. auto& file_menu = window->add_menu("&File");
  281. file_menu.add_action(GUI::Action::create("&Analyze", [&](auto&) {
  282. analyze(tree, treemapwidget, statusbar);
  283. }));
  284. file_menu.add_separator();
  285. file_menu.add_action(GUI::CommonActions::make_quit_action([&](auto&) {
  286. app->quit();
  287. }));
  288. auto& help_menu = window->add_menu("&Help");
  289. help_menu.add_action(GUI::CommonActions::make_about_action(APP_NAME, app_icon, window));
  290. // Configure the nodes context menu.
  291. auto open_folder_action = GUI::Action::create("Open Folder", { Mod_Ctrl, Key_O }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/open.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  292. Desktop::Launcher::open(URL::create_with_file_protocol(get_absolute_path_to_selected_node(treemapwidget)));
  293. });
  294. auto open_containing_folder_action = GUI::Action::create("Open Containing Folder", { Mod_Ctrl, Key_O }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/open.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  295. LexicalPath path { get_absolute_path_to_selected_node(treemapwidget) };
  296. Desktop::Launcher::open(URL::create_with_file_protocol(path.dirname(), path.basename()));
  297. });
  298. auto copy_path_action = GUI::Action::create("Copy Path to Clipboard", { Mod_Ctrl, Key_C }, Gfx::Bitmap::try_load_from_file("/res/icons/16x16/edit-copy.png").release_value_but_fixme_should_propagate_errors(), [&](auto&) {
  299. GUI::Clipboard::the().set_plain_text(get_absolute_path_to_selected_node(treemapwidget));
  300. });
  301. auto delete_action = GUI::CommonActions::make_delete_action([&](auto&) {
  302. String selected_node_path = get_absolute_path_to_selected_node(treemapwidget);
  303. bool try_again = true;
  304. while (try_again) {
  305. try_again = false;
  306. auto deletion_result = Core::File::remove(selected_node_path, Core::File::RecursionMode::Allowed, true);
  307. if (deletion_result.is_error()) {
  308. auto retry_message_result = GUI::MessageBox::show(window,
  309. String::formatted("Failed to delete \"{}\": {}. Retry?",
  310. deletion_result.error().file,
  311. static_cast<Error const&>(deletion_result.error())),
  312. "Deletion failed",
  313. GUI::MessageBox::Type::Error,
  314. GUI::MessageBox::InputType::YesNo);
  315. if (retry_message_result == GUI::MessageBox::ExecYes) {
  316. try_again = true;
  317. }
  318. } else {
  319. GUI::MessageBox::show(window,
  320. String::formatted("Successfully deleted \"{}\".", selected_node_path),
  321. "Deletion completed",
  322. GUI::MessageBox::Type::Information,
  323. GUI::MessageBox::InputType::OK);
  324. }
  325. }
  326. // TODO: Refreshing data always causes resetting the viewport back to "/".
  327. // It would be great if we found a way to preserve viewport across refreshes.
  328. analyze(tree, treemapwidget, statusbar);
  329. });
  330. // TODO: Both these menus could've been implemented as one, but it's impossible to change action text after it's shown once.
  331. auto folder_node_context_menu = GUI::Menu::construct();
  332. folder_node_context_menu->add_action(*open_folder_action);
  333. folder_node_context_menu->add_action(*copy_path_action);
  334. folder_node_context_menu->add_action(*delete_action);
  335. auto file_node_context_menu = GUI::Menu::construct();
  336. file_node_context_menu->add_action(*open_containing_folder_action);
  337. file_node_context_menu->add_action(*copy_path_action);
  338. file_node_context_menu->add_action(*delete_action);
  339. // Configure event handlers.
  340. breadcrumbbar.on_segment_click = [&](size_t index) {
  341. VERIFY(index < treemapwidget.path_size());
  342. treemapwidget.set_viewpoint(index);
  343. };
  344. treemapwidget.on_path_change = [&]() {
  345. StringBuilder builder;
  346. breadcrumbbar.clear_segments();
  347. for (size_t k = 0; k < treemapwidget.path_size(); k++) {
  348. if (k == 0) {
  349. breadcrumbbar.append_segment("/", GUI::FileIconProvider::icon_for_path("/").bitmap_for_size(16), "/", "/");
  350. continue;
  351. }
  352. const SpaceAnalyzer::TreeMapNode* node = treemapwidget.path_node(k);
  353. builder.append("/");
  354. builder.append(node->name());
  355. breadcrumbbar.append_segment(node->name(), GUI::FileIconProvider::icon_for_path(builder.string_view()).bitmap_for_size(16), builder.string_view(), builder.string_view());
  356. }
  357. breadcrumbbar.set_selected_segment(treemapwidget.viewpoint());
  358. };
  359. treemapwidget.on_context_menu_request = [&](const GUI::ContextMenuEvent& event) {
  360. String selected_node_path = get_absolute_path_to_selected_node(treemapwidget);
  361. if (selected_node_path.is_empty())
  362. return;
  363. delete_action->set_enabled(is_removable(selected_node_path));
  364. if (Core::File::is_directory(selected_node_path)) {
  365. folder_node_context_menu->popup(event.screen_position());
  366. } else {
  367. file_node_context_menu->popup(event.screen_position());
  368. }
  369. };
  370. // At startup automatically do an analysis of root.
  371. analyze(tree, treemapwidget, statusbar);
  372. window->show();
  373. return app->exec();
  374. }