FileSystemModel.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. /*
  2. * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
  3. * All rights reserved.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. *
  8. * 1. Redistributions of source code must retain the above copyright notice, this
  9. * list of conditions and the following disclaimer.
  10. *
  11. * 2. Redistributions in binary form must reproduce the above copyright notice,
  12. * this list of conditions and the following disclaimer in the documentation
  13. * and/or other materials provided with the distribution.
  14. *
  15. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  16. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  17. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  18. * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  19. * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  20. * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  21. * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  22. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  23. * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  24. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  25. */
  26. #include <AK/LexicalPath.h>
  27. #include <AK/QuickSort.h>
  28. #include <AK/StringBuilder.h>
  29. #include <LibCore/DirIterator.h>
  30. #include <LibCore/File.h>
  31. #include <LibCore/StandardPaths.h>
  32. #include <LibGUI/FileIconProvider.h>
  33. #include <LibGUI/FileSystemModel.h>
  34. #include <LibGUI/Painter.h>
  35. #include <LibGfx/Bitmap.h>
  36. #include <LibThread/BackgroundAction.h>
  37. #include <dirent.h>
  38. #include <grp.h>
  39. #include <pwd.h>
  40. #include <stdio.h>
  41. #include <sys/stat.h>
  42. #include <unistd.h>
  43. namespace GUI {
  44. ModelIndex FileSystemModel::Node::index(const FileSystemModel& model, int column) const
  45. {
  46. if (!parent)
  47. return {};
  48. for (size_t row = 0; row < parent->children.size(); ++row) {
  49. if (&parent->children[row] == this)
  50. return model.create_index(row, column, const_cast<Node*>(this));
  51. }
  52. ASSERT_NOT_REACHED();
  53. }
  54. bool FileSystemModel::Node::fetch_data(const String& full_path, bool is_root)
  55. {
  56. struct stat st;
  57. int rc;
  58. if (is_root)
  59. rc = stat(full_path.characters(), &st);
  60. else
  61. rc = lstat(full_path.characters(), &st);
  62. if (rc < 0) {
  63. m_error = errno;
  64. perror("stat/lstat");
  65. return false;
  66. }
  67. size = st.st_size;
  68. mode = st.st_mode;
  69. uid = st.st_uid;
  70. gid = st.st_gid;
  71. inode = st.st_ino;
  72. mtime = st.st_mtime;
  73. if (S_ISLNK(mode)) {
  74. symlink_target = Core::File::read_link(full_path);
  75. if (symlink_target.is_null())
  76. perror("readlink");
  77. }
  78. return true;
  79. }
  80. void FileSystemModel::Node::traverse_if_needed(const FileSystemModel& model)
  81. {
  82. if (!is_directory() || has_traversed)
  83. return;
  84. has_traversed = true;
  85. total_size = 0;
  86. auto full_path = this->full_path(model);
  87. Core::DirIterator di(full_path, model.should_show_dotfiles() ? Core::DirIterator::SkipParentAndBaseDir : Core::DirIterator::SkipDots);
  88. if (di.has_error()) {
  89. m_error = di.error();
  90. fprintf(stderr, "DirIterator: %s\n", di.error_string());
  91. return;
  92. }
  93. Vector<String> child_names;
  94. while (di.has_next()) {
  95. child_names.append(di.next_path());
  96. }
  97. quick_sort(child_names);
  98. for (auto& name : child_names) {
  99. String child_path = String::format("%s/%s", full_path.characters(), name.characters());
  100. NonnullOwnPtr<Node> child = make<Node>();
  101. bool ok = child->fetch_data(child_path, false);
  102. if (!ok)
  103. continue;
  104. if (model.m_mode == DirectoriesOnly && !S_ISDIR(child->mode))
  105. continue;
  106. child->name = name;
  107. child->parent = this;
  108. total_size += child->size;
  109. children.append(move(child));
  110. }
  111. if (m_watch_fd >= 0)
  112. return;
  113. m_watch_fd = watch_file(full_path.characters(), full_path.length());
  114. if (m_watch_fd < 0) {
  115. perror("watch_file");
  116. return;
  117. }
  118. fcntl(m_watch_fd, F_SETFD, FD_CLOEXEC);
  119. dbg() << "Watching " << full_path << " for changes, m_watch_fd = " << m_watch_fd;
  120. m_notifier = Core::Notifier::construct(m_watch_fd, Core::Notifier::Event::Read);
  121. m_notifier->on_ready_to_read = [this, &model] {
  122. char buffer[32];
  123. int rc = read(m_notifier->fd(), buffer, sizeof(buffer));
  124. ASSERT(rc >= 0);
  125. has_traversed = false;
  126. mode = 0;
  127. children.clear();
  128. reify_if_needed(model);
  129. const_cast<FileSystemModel&>(model).did_update();
  130. };
  131. }
  132. void FileSystemModel::Node::reify_if_needed(const FileSystemModel& model)
  133. {
  134. traverse_if_needed(model);
  135. if (mode != 0)
  136. return;
  137. fetch_data(full_path(model), parent == nullptr);
  138. }
  139. String FileSystemModel::Node::full_path(const FileSystemModel& model) const
  140. {
  141. Vector<String, 32> lineage;
  142. for (auto* ancestor = parent; ancestor; ancestor = ancestor->parent) {
  143. lineage.append(ancestor->name);
  144. }
  145. StringBuilder builder;
  146. builder.append(model.root_path());
  147. for (int i = lineage.size() - 1; i >= 0; --i) {
  148. builder.append('/');
  149. builder.append(lineage[i]);
  150. }
  151. builder.append('/');
  152. builder.append(name);
  153. return LexicalPath::canonicalized_path(builder.to_string());
  154. }
  155. ModelIndex FileSystemModel::index(const StringView& path, int column) const
  156. {
  157. LexicalPath lexical_path(path);
  158. const Node* node = m_root;
  159. if (lexical_path.string() == "/")
  160. return m_root->index(*this, column);
  161. for (size_t i = 0; i < lexical_path.parts().size(); ++i) {
  162. auto& part = lexical_path.parts()[i];
  163. bool found = false;
  164. for (auto& child : node->children) {
  165. if (child.name == part) {
  166. const_cast<Node&>(child).reify_if_needed(*this);
  167. node = &child;
  168. found = true;
  169. if (i == lexical_path.parts().size() - 1)
  170. return child.index(*this, column);
  171. break;
  172. }
  173. }
  174. if (!found)
  175. return {};
  176. }
  177. return {};
  178. }
  179. String FileSystemModel::full_path(const ModelIndex& index) const
  180. {
  181. auto& node = this->node(index);
  182. const_cast<Node&>(node).reify_if_needed(*this);
  183. return node.full_path(*this);
  184. }
  185. FileSystemModel::FileSystemModel(const StringView& root_path, Mode mode)
  186. : m_root_path(LexicalPath::canonicalized_path(root_path))
  187. , m_mode(mode)
  188. {
  189. setpwent();
  190. while (auto* passwd = getpwent())
  191. m_user_names.set(passwd->pw_uid, passwd->pw_name);
  192. endpwent();
  193. setgrent();
  194. while (auto* group = getgrent())
  195. m_group_names.set(group->gr_gid, group->gr_name);
  196. endgrent();
  197. update();
  198. }
  199. FileSystemModel::~FileSystemModel()
  200. {
  201. }
  202. String FileSystemModel::name_for_uid(uid_t uid) const
  203. {
  204. auto it = m_user_names.find(uid);
  205. if (it == m_user_names.end())
  206. return String::number(uid);
  207. return (*it).value;
  208. }
  209. String FileSystemModel::name_for_gid(gid_t gid) const
  210. {
  211. auto it = m_group_names.find(gid);
  212. if (it == m_group_names.end())
  213. return String::number(gid);
  214. return (*it).value;
  215. }
  216. static String permission_string(mode_t mode)
  217. {
  218. StringBuilder builder;
  219. if (S_ISDIR(mode))
  220. builder.append("d");
  221. else if (S_ISLNK(mode))
  222. builder.append("l");
  223. else if (S_ISBLK(mode))
  224. builder.append("b");
  225. else if (S_ISCHR(mode))
  226. builder.append("c");
  227. else if (S_ISFIFO(mode))
  228. builder.append("f");
  229. else if (S_ISSOCK(mode))
  230. builder.append("s");
  231. else if (S_ISREG(mode))
  232. builder.append("-");
  233. else
  234. builder.append("?");
  235. builder.appendf("%c%c%c%c%c%c%c%c",
  236. mode & S_IRUSR ? 'r' : '-',
  237. mode & S_IWUSR ? 'w' : '-',
  238. mode & S_ISUID ? 's' : (mode & S_IXUSR ? 'x' : '-'),
  239. mode & S_IRGRP ? 'r' : '-',
  240. mode & S_IWGRP ? 'w' : '-',
  241. mode & S_ISGID ? 's' : (mode & S_IXGRP ? 'x' : '-'),
  242. mode & S_IROTH ? 'r' : '-',
  243. mode & S_IWOTH ? 'w' : '-');
  244. if (mode & S_ISVTX)
  245. builder.append("t");
  246. else
  247. builder.appendf("%c", mode & S_IXOTH ? 'x' : '-');
  248. return builder.to_string();
  249. }
  250. void FileSystemModel::Node::set_selected(bool selected)
  251. {
  252. if (m_selected == selected)
  253. return;
  254. m_selected = selected;
  255. }
  256. void FileSystemModel::update_node_on_selection(const ModelIndex& index, const bool selected)
  257. {
  258. Node& node = const_cast<Node&>(this->node(index));
  259. node.set_selected(selected);
  260. }
  261. void FileSystemModel::set_root_path(const StringView& root_path)
  262. {
  263. m_root_path = LexicalPath::canonicalized_path(root_path);
  264. update();
  265. if (m_root->has_error()) {
  266. if (on_error)
  267. on_error(m_root->error(), m_root->error_string());
  268. } else if (on_complete) {
  269. on_complete();
  270. }
  271. }
  272. void FileSystemModel::update()
  273. {
  274. m_root = make<Node>();
  275. m_root->reify_if_needed(*this);
  276. did_update();
  277. }
  278. int FileSystemModel::row_count(const ModelIndex& index) const
  279. {
  280. Node& node = const_cast<Node&>(this->node(index));
  281. node.reify_if_needed(*this);
  282. if (node.is_directory())
  283. return node.children.size();
  284. return 0;
  285. }
  286. const FileSystemModel::Node& FileSystemModel::node(const ModelIndex& index) const
  287. {
  288. if (!index.is_valid())
  289. return *m_root;
  290. ASSERT(index.internal_data());
  291. return *(Node*)index.internal_data();
  292. }
  293. ModelIndex FileSystemModel::index(int row, int column, const ModelIndex& parent) const
  294. {
  295. if (row < 0 || column < 0)
  296. return {};
  297. auto& node = this->node(parent);
  298. const_cast<Node&>(node).reify_if_needed(*this);
  299. if (static_cast<size_t>(row) >= node.children.size())
  300. return {};
  301. return create_index(row, column, &node.children[row]);
  302. }
  303. ModelIndex FileSystemModel::parent_index(const ModelIndex& index) const
  304. {
  305. if (!index.is_valid())
  306. return {};
  307. auto& node = this->node(index);
  308. if (!node.parent) {
  309. ASSERT(&node == m_root);
  310. return {};
  311. }
  312. return node.parent->index(*this, index.column());
  313. }
  314. Variant FileSystemModel::data(const ModelIndex& index, Role role) const
  315. {
  316. ASSERT(index.is_valid());
  317. if (role == Role::TextAlignment) {
  318. switch (index.column()) {
  319. case Column::Icon:
  320. return Gfx::TextAlignment::Center;
  321. case Column::Size:
  322. case Column::Inode:
  323. return Gfx::TextAlignment::CenterRight;
  324. case Column::Name:
  325. case Column::Owner:
  326. case Column::Group:
  327. case Column::ModificationTime:
  328. case Column::Permissions:
  329. case Column::SymlinkTarget:
  330. return Gfx::TextAlignment::CenterLeft;
  331. default:
  332. ASSERT_NOT_REACHED();
  333. }
  334. }
  335. auto& node = this->node(index);
  336. if (role == Role::Custom) {
  337. // For GUI::FileSystemModel, custom role means the full path.
  338. ASSERT(index.column() == Column::Name);
  339. return node.full_path(*this);
  340. }
  341. if (role == Role::DragData) {
  342. if (index.column() == Column::Name) {
  343. StringBuilder builder;
  344. builder.append("file://");
  345. builder.append(node.full_path(*this));
  346. return builder.to_string();
  347. }
  348. return {};
  349. }
  350. if (role == Role::Sort) {
  351. switch (index.column()) {
  352. case Column::Icon:
  353. return node.is_directory() ? 0 : 1;
  354. case Column::Name:
  355. return node.name;
  356. case Column::Size:
  357. return (int)node.size;
  358. case Column::Owner:
  359. return name_for_uid(node.uid);
  360. case Column::Group:
  361. return name_for_gid(node.gid);
  362. case Column::Permissions:
  363. return permission_string(node.mode);
  364. case Column::ModificationTime:
  365. return node.mtime;
  366. case Column::Inode:
  367. return (int)node.inode;
  368. case Column::SymlinkTarget:
  369. return node.symlink_target;
  370. }
  371. ASSERT_NOT_REACHED();
  372. }
  373. if (role == Role::Display) {
  374. switch (index.column()) {
  375. case Column::Icon:
  376. return icon_for(node);
  377. case Column::Name:
  378. return node.name;
  379. case Column::Size:
  380. return (int)node.size;
  381. case Column::Owner:
  382. return name_for_uid(node.uid);
  383. case Column::Group:
  384. return name_for_gid(node.gid);
  385. case Column::Permissions:
  386. return permission_string(node.mode);
  387. case Column::ModificationTime:
  388. return timestamp_string(node.mtime);
  389. case Column::Inode:
  390. return (int)node.inode;
  391. case Column::SymlinkTarget:
  392. return node.symlink_target;
  393. }
  394. }
  395. if (role == Role::Icon) {
  396. return icon_for(node);
  397. }
  398. return {};
  399. }
  400. Icon FileSystemModel::icon_for(const Node& node) const
  401. {
  402. if (Gfx::Bitmap::is_path_a_supported_image_format(node.name.to_lowercase())) {
  403. if (!node.thumbnail) {
  404. if (!const_cast<FileSystemModel*>(this)->fetch_thumbnail_for(node))
  405. return FileIconProvider::filetype_image_icon();
  406. }
  407. return GUI::Icon(FileIconProvider::filetype_image_icon().bitmap_for_size(16), *node.thumbnail);
  408. }
  409. if (node.is_directory()) {
  410. if (node.full_path(*this) == Core::StandardPaths::home_directory()) {
  411. if (node.is_selected())
  412. return FileIconProvider::home_directory_open_icon();
  413. return FileIconProvider::home_directory_icon();
  414. }
  415. if (node.is_selected())
  416. return FileIconProvider::directory_open_icon();
  417. }
  418. return FileIconProvider::icon_for_path(node.name, node.mode);
  419. }
  420. static HashMap<String, RefPtr<Gfx::Bitmap>> s_thumbnail_cache;
  421. static RefPtr<Gfx::Bitmap> render_thumbnail(const StringView& path)
  422. {
  423. auto png_bitmap = Gfx::Bitmap::load_from_file(path);
  424. if (!png_bitmap)
  425. return nullptr;
  426. double scale = min(32 / (double)png_bitmap->width(), 32 / (double)png_bitmap->height());
  427. auto thumbnail = Gfx::Bitmap::create(png_bitmap->format(), { 32, 32 });
  428. Gfx::IntRect destination = Gfx::IntRect(0, 0, (int)(png_bitmap->width() * scale), (int)(png_bitmap->height() * scale));
  429. destination.center_within(thumbnail->rect());
  430. Painter painter(*thumbnail);
  431. painter.draw_scaled_bitmap(destination, *png_bitmap, png_bitmap->rect());
  432. return thumbnail;
  433. }
  434. bool FileSystemModel::fetch_thumbnail_for(const Node& node)
  435. {
  436. // See if we already have the thumbnail
  437. // we're looking for in the cache.
  438. auto path = node.full_path(*this);
  439. auto it = s_thumbnail_cache.find(path);
  440. if (it != s_thumbnail_cache.end()) {
  441. if (!(*it).value)
  442. return false;
  443. node.thumbnail = (*it).value;
  444. return true;
  445. }
  446. // Otherwise, arrange to render the thumbnail
  447. // in background and make it available later.
  448. s_thumbnail_cache.set(path, nullptr);
  449. m_thumbnail_progress_total++;
  450. auto weak_this = make_weak_ptr();
  451. LibThread::BackgroundAction<RefPtr<Gfx::Bitmap>>::create(
  452. [path] {
  453. return render_thumbnail(path);
  454. },
  455. [this, path, weak_this](auto thumbnail) {
  456. s_thumbnail_cache.set(path, move(thumbnail));
  457. // The model was destroyed, no need to update
  458. // progress or call any event handlers.
  459. if (weak_this.is_null())
  460. return;
  461. m_thumbnail_progress++;
  462. if (on_thumbnail_progress)
  463. on_thumbnail_progress(m_thumbnail_progress, m_thumbnail_progress_total);
  464. if (m_thumbnail_progress == m_thumbnail_progress_total) {
  465. m_thumbnail_progress = 0;
  466. m_thumbnail_progress_total = 0;
  467. }
  468. did_update();
  469. });
  470. return false;
  471. }
  472. int FileSystemModel::column_count(const ModelIndex&) const
  473. {
  474. return Column::__Count;
  475. }
  476. String FileSystemModel::column_name(int column) const
  477. {
  478. switch (column) {
  479. case Column::Icon:
  480. return "";
  481. case Column::Name:
  482. return "Name";
  483. case Column::Size:
  484. return "Size";
  485. case Column::Owner:
  486. return "Owner";
  487. case Column::Group:
  488. return "Group";
  489. case Column::Permissions:
  490. return "Mode";
  491. case Column::ModificationTime:
  492. return "Modified";
  493. case Column::Inode:
  494. return "Inode";
  495. case Column::SymlinkTarget:
  496. return "Symlink target";
  497. }
  498. ASSERT_NOT_REACHED();
  499. }
  500. bool FileSystemModel::accepts_drag(const ModelIndex& index, const StringView& data_type)
  501. {
  502. if (!index.is_valid())
  503. return false;
  504. if (data_type != "text/uri-list")
  505. return false;
  506. auto& node = this->node(index);
  507. return node.is_directory();
  508. }
  509. void FileSystemModel::set_should_show_dotfiles(bool show)
  510. {
  511. if (m_should_show_dotfiles == show)
  512. return;
  513. m_should_show_dotfiles = show;
  514. update();
  515. }
  516. }