main.cpp 16 KB

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