GFileSystemModel.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. #include <AK/FileSystemPath.h>
  2. #include <AK/StringBuilder.h>
  3. #include <LibCore/CDirIterator.h>
  4. #include <LibDraw/GraphicsBitmap.h>
  5. #include <LibGUI/GFileSystemModel.h>
  6. #include <LibGUI/GPainter.h>
  7. #include <LibThread/BackgroundAction.h>
  8. #include <dirent.h>
  9. #include <grp.h>
  10. #include <pwd.h>
  11. #include <stdio.h>
  12. #include <sys/stat.h>
  13. #include <unistd.h>
  14. GModelIndex GFileSystemModel::Node::index(const GFileSystemModel& model, int column) const
  15. {
  16. if (!parent)
  17. return {};
  18. for (int row = 0; row < parent->children.size(); ++row) {
  19. if (&parent->children[row] == this)
  20. return model.create_index(row, column, const_cast<Node*>(this));
  21. }
  22. ASSERT_NOT_REACHED();
  23. }
  24. bool GFileSystemModel::Node::fetch_data_using_lstat(const String& full_path)
  25. {
  26. struct stat st;
  27. int rc = lstat(full_path.characters(), &st);
  28. if (rc < 0) {
  29. perror("lstat");
  30. return false;
  31. }
  32. size = st.st_size;
  33. mode = st.st_mode;
  34. uid = st.st_uid;
  35. gid = st.st_gid;
  36. inode = st.st_ino;
  37. mtime = st.st_mtime;
  38. return true;
  39. }
  40. void GFileSystemModel::Node::traverse_if_needed(const GFileSystemModel& model)
  41. {
  42. if (!is_directory() || has_traversed)
  43. return;
  44. has_traversed = true;
  45. total_size = 0;
  46. auto full_path = this->full_path(model);
  47. CDirIterator di(full_path, CDirIterator::SkipDots);
  48. if (di.has_error()) {
  49. fprintf(stderr, "CDirIterator: %s\n", di.error_string());
  50. return;
  51. }
  52. while (di.has_next()) {
  53. String name = di.next_path();
  54. String child_path = String::format("%s/%s", full_path.characters(), name.characters());
  55. NonnullOwnPtr<Node> child = make<Node>();
  56. bool ok = child->fetch_data_using_lstat(child_path);
  57. if (!ok)
  58. continue;
  59. if (model.m_mode == DirectoriesOnly && !S_ISDIR(child->mode))
  60. continue;
  61. child->name = name;
  62. child->parent = this;
  63. total_size += child->size;
  64. children.append(move(child));
  65. }
  66. if (m_watch_fd >= 0)
  67. return;
  68. m_watch_fd = watch_file(full_path.characters(), full_path.length());
  69. if (m_watch_fd < 0) {
  70. perror("watch_file");
  71. return;
  72. }
  73. fcntl(m_watch_fd, F_SETFD, FD_CLOEXEC);
  74. dbg() << "Watching " << full_path << " for changes, m_watch_fd = " << m_watch_fd;
  75. m_notifier = CNotifier::construct(m_watch_fd, CNotifier::Event::Read);
  76. m_notifier->on_ready_to_read = [this, &model] {
  77. char buffer[32];
  78. int rc = read(m_notifier->fd(), buffer, sizeof(buffer));
  79. ASSERT(rc >= 0);
  80. has_traversed = false;
  81. mode = 0;
  82. children.clear();
  83. reify_if_needed(model);
  84. const_cast<GFileSystemModel&>(model).did_update();
  85. };
  86. }
  87. void GFileSystemModel::Node::reify_if_needed(const GFileSystemModel& model)
  88. {
  89. traverse_if_needed(model);
  90. if (mode != 0)
  91. return;
  92. fetch_data_using_lstat(full_path(model));
  93. }
  94. String GFileSystemModel::Node::full_path(const GFileSystemModel& model) const
  95. {
  96. Vector<String, 32> lineage;
  97. for (auto* ancestor = parent; ancestor; ancestor = ancestor->parent) {
  98. lineage.append(ancestor->name);
  99. }
  100. StringBuilder builder;
  101. builder.append(model.root_path());
  102. for (int i = lineage.size() - 1; i >= 0; --i) {
  103. builder.append('/');
  104. builder.append(lineage[i]);
  105. }
  106. builder.append('/');
  107. builder.append(name);
  108. return canonicalized_path(builder.to_string());
  109. }
  110. GModelIndex GFileSystemModel::index(const StringView& path, int column) const
  111. {
  112. FileSystemPath canonical_path(path);
  113. const Node* node = m_root;
  114. if (canonical_path.string() == "/")
  115. return m_root->index(*this, column);
  116. for (int i = 0; i < canonical_path.parts().size(); ++i) {
  117. auto& part = canonical_path.parts()[i];
  118. bool found = false;
  119. for (auto& child : node->children) {
  120. if (child.name == part) {
  121. const_cast<Node&>(child).reify_if_needed(*this);
  122. node = &child;
  123. found = true;
  124. if (i == canonical_path.parts().size() - 1)
  125. return child.index(*this, column);
  126. break;
  127. }
  128. }
  129. if (!found)
  130. return {};
  131. }
  132. return {};
  133. }
  134. String GFileSystemModel::full_path(const GModelIndex& index) const
  135. {
  136. auto& node = this->node(index);
  137. const_cast<Node&>(node).reify_if_needed(*this);
  138. return node.full_path(*this);
  139. }
  140. GFileSystemModel::GFileSystemModel(const StringView& root_path, Mode mode)
  141. : m_root_path(canonicalized_path(root_path))
  142. , m_mode(mode)
  143. {
  144. m_directory_icon = GIcon::default_icon("filetype-folder");
  145. m_file_icon = GIcon::default_icon("filetype-unknown");
  146. m_symlink_icon = GIcon::default_icon("filetype-symlink");
  147. m_socket_icon = GIcon::default_icon("filetype-socket");
  148. m_executable_icon = GIcon::default_icon("filetype-executable");
  149. m_filetype_image_icon = GIcon::default_icon("filetype-image");
  150. m_filetype_sound_icon = GIcon::default_icon("filetype-sound");
  151. m_filetype_html_icon = GIcon::default_icon("filetype-html");
  152. setpwent();
  153. while (auto* passwd = getpwent())
  154. m_user_names.set(passwd->pw_uid, passwd->pw_name);
  155. endpwent();
  156. setgrent();
  157. while (auto* group = getgrent())
  158. m_group_names.set(group->gr_gid, group->gr_name);
  159. endgrent();
  160. update();
  161. }
  162. GFileSystemModel::~GFileSystemModel()
  163. {
  164. }
  165. String GFileSystemModel::name_for_uid(uid_t uid) const
  166. {
  167. auto it = m_user_names.find(uid);
  168. if (it == m_user_names.end())
  169. return String::number(uid);
  170. return (*it).value;
  171. }
  172. String GFileSystemModel::name_for_gid(uid_t gid) const
  173. {
  174. auto it = m_user_names.find(gid);
  175. if (it == m_user_names.end())
  176. return String::number(gid);
  177. return (*it).value;
  178. }
  179. static String permission_string(mode_t mode)
  180. {
  181. StringBuilder builder;
  182. if (S_ISDIR(mode))
  183. builder.append("d");
  184. else if (S_ISLNK(mode))
  185. builder.append("l");
  186. else if (S_ISBLK(mode))
  187. builder.append("b");
  188. else if (S_ISCHR(mode))
  189. builder.append("c");
  190. else if (S_ISFIFO(mode))
  191. builder.append("f");
  192. else if (S_ISSOCK(mode))
  193. builder.append("s");
  194. else if (S_ISREG(mode))
  195. builder.append("-");
  196. else
  197. builder.append("?");
  198. builder.appendf("%c%c%c%c%c%c%c%c",
  199. mode & S_IRUSR ? 'r' : '-',
  200. mode & S_IWUSR ? 'w' : '-',
  201. mode & S_ISUID ? 's' : (mode & S_IXUSR ? 'x' : '-'),
  202. mode & S_IRGRP ? 'r' : '-',
  203. mode & S_IWGRP ? 'w' : '-',
  204. mode & S_ISGID ? 's' : (mode & S_IXGRP ? 'x' : '-'),
  205. mode & S_IROTH ? 'r' : '-',
  206. mode & S_IWOTH ? 'w' : '-');
  207. if (mode & S_ISVTX)
  208. builder.append("t");
  209. else
  210. builder.appendf("%c", mode & S_IXOTH ? 'x' : '-');
  211. return builder.to_string();
  212. }
  213. void GFileSystemModel::set_root_path(const StringView& root_path)
  214. {
  215. m_root_path = canonicalized_path(root_path);
  216. if (on_root_path_change)
  217. on_root_path_change();
  218. update();
  219. }
  220. void GFileSystemModel::update()
  221. {
  222. m_root = make<Node>();
  223. m_root->reify_if_needed(*this);
  224. did_update();
  225. }
  226. int GFileSystemModel::row_count(const GModelIndex& index) const
  227. {
  228. Node& node = const_cast<Node&>(this->node(index));
  229. node.reify_if_needed(*this);
  230. if (node.is_directory())
  231. return node.children.size();
  232. return 0;
  233. }
  234. const GFileSystemModel::Node& GFileSystemModel::node(const GModelIndex& index) const
  235. {
  236. if (!index.is_valid())
  237. return *m_root;
  238. return *(Node*)index.internal_data();
  239. }
  240. GModelIndex GFileSystemModel::index(int row, int column, const GModelIndex& parent) const
  241. {
  242. auto& node = this->node(parent);
  243. const_cast<Node&>(node).reify_if_needed(*this);
  244. if (row >= node.children.size())
  245. return {};
  246. return create_index(row, column, &node.children[row]);
  247. }
  248. GModelIndex GFileSystemModel::parent_index(const GModelIndex& index) const
  249. {
  250. if (!index.is_valid())
  251. return {};
  252. auto& node = this->node(index);
  253. if (!node.parent) {
  254. ASSERT(&node == m_root);
  255. return {};
  256. }
  257. return node.parent->index(*this, index.column());
  258. }
  259. GVariant GFileSystemModel::data(const GModelIndex& index, Role role) const
  260. {
  261. ASSERT(index.is_valid());
  262. auto& node = this->node(index);
  263. if (role == Role::Custom) {
  264. // For GFileSystemModel, custom role means the full path.
  265. ASSERT(index.column() == Column::Name);
  266. return node.full_path(*this);
  267. }
  268. if (role == Role::DragData) {
  269. if (index.column() == Column::Name) {
  270. StringBuilder builder;
  271. builder.append("file://");
  272. builder.append(node.full_path(*this));
  273. return builder.to_string();
  274. }
  275. return {};
  276. }
  277. if (role == Role::Sort) {
  278. switch (index.column()) {
  279. case Column::Icon:
  280. return node.is_directory() ? 0 : 1;
  281. case Column::Name:
  282. return node.name;
  283. case Column::Size:
  284. return (int)node.size;
  285. case Column::Owner:
  286. return name_for_uid(node.uid);
  287. case Column::Group:
  288. return name_for_gid(node.gid);
  289. case Column::Permissions:
  290. return permission_string(node.mode);
  291. case Column::ModificationTime:
  292. return node.mtime;
  293. case Column::Inode:
  294. return (int)node.inode;
  295. }
  296. ASSERT_NOT_REACHED();
  297. }
  298. if (role == Role::Display) {
  299. switch (index.column()) {
  300. case Column::Icon:
  301. return icon_for(node);
  302. case Column::Name:
  303. return node.name;
  304. case Column::Size:
  305. return (int)node.size;
  306. case Column::Owner:
  307. return name_for_uid(node.uid);
  308. case Column::Group:
  309. return name_for_gid(node.gid);
  310. case Column::Permissions:
  311. return permission_string(node.mode);
  312. case Column::ModificationTime:
  313. return timestamp_string(node.mtime);
  314. case Column::Inode:
  315. return (int)node.inode;
  316. }
  317. }
  318. if (role == Role::Icon) {
  319. return icon_for(node);
  320. }
  321. return {};
  322. }
  323. GIcon GFileSystemModel::icon_for_file(const mode_t mode, const String& name) const
  324. {
  325. if (S_ISDIR(mode))
  326. return m_directory_icon;
  327. if (S_ISLNK(mode))
  328. return m_symlink_icon;
  329. if (S_ISSOCK(mode))
  330. return m_socket_icon;
  331. if (mode & S_IXUSR)
  332. return m_executable_icon;
  333. if (name.to_lowercase().ends_with(".wav"))
  334. return m_filetype_sound_icon;
  335. if (name.to_lowercase().ends_with(".html"))
  336. return m_filetype_html_icon;
  337. if (name.to_lowercase().ends_with(".png"))
  338. return m_filetype_image_icon;
  339. return m_file_icon;
  340. }
  341. GIcon GFileSystemModel::icon_for(const Node& node) const
  342. {
  343. if (node.name.to_lowercase().ends_with(".png")) {
  344. if (!node.thumbnail) {
  345. if (!const_cast<GFileSystemModel*>(this)->fetch_thumbnail_for(node))
  346. return m_filetype_image_icon;
  347. }
  348. return GIcon(m_filetype_image_icon.bitmap_for_size(16), *node.thumbnail);
  349. }
  350. return icon_for_file(node.mode, node.name);
  351. }
  352. static HashMap<String, RefPtr<GraphicsBitmap>> s_thumbnail_cache;
  353. static RefPtr<GraphicsBitmap> render_thumbnail(const StringView& path)
  354. {
  355. auto png_bitmap = GraphicsBitmap::load_from_file(path);
  356. if (!png_bitmap)
  357. return nullptr;
  358. auto thumbnail = GraphicsBitmap::create(png_bitmap->format(), { 32, 32 });
  359. Painter painter(*thumbnail);
  360. painter.draw_scaled_bitmap(thumbnail->rect(), *png_bitmap, png_bitmap->rect());
  361. return thumbnail;
  362. }
  363. bool GFileSystemModel::fetch_thumbnail_for(const Node& node)
  364. {
  365. // See if we already have the thumbnail
  366. // we're looking for in the cache.
  367. auto path = node.full_path(*this);
  368. auto it = s_thumbnail_cache.find(path);
  369. if (it != s_thumbnail_cache.end()) {
  370. if (!(*it).value)
  371. return false;
  372. node.thumbnail = (*it).value;
  373. return true;
  374. }
  375. // Otherwise, arrange to render the thumbnail
  376. // in background and make it available later.
  377. s_thumbnail_cache.set(path, nullptr);
  378. m_thumbnail_progress_total++;
  379. auto weak_this = make_weak_ptr();
  380. LibThread::BackgroundAction<RefPtr<GraphicsBitmap>>::create(
  381. [path] {
  382. return render_thumbnail(path);
  383. },
  384. [this, path, weak_this](auto thumbnail) {
  385. s_thumbnail_cache.set(path, move(thumbnail));
  386. // The model was destroyed, no need to update
  387. // progress or call any event handlers.
  388. if (weak_this.is_null())
  389. return;
  390. m_thumbnail_progress++;
  391. if (on_thumbnail_progress)
  392. on_thumbnail_progress(m_thumbnail_progress, m_thumbnail_progress_total);
  393. if (m_thumbnail_progress == m_thumbnail_progress_total) {
  394. m_thumbnail_progress = 0;
  395. m_thumbnail_progress_total = 0;
  396. }
  397. did_update();
  398. });
  399. return false;
  400. }
  401. int GFileSystemModel::column_count(const GModelIndex&) const
  402. {
  403. return Column::__Count;
  404. }
  405. String GFileSystemModel::column_name(int column) const
  406. {
  407. switch (column) {
  408. case Column::Icon:
  409. return "";
  410. case Column::Name:
  411. return "Name";
  412. case Column::Size:
  413. return "Size";
  414. case Column::Owner:
  415. return "Owner";
  416. case Column::Group:
  417. return "Group";
  418. case Column::Permissions:
  419. return "Mode";
  420. case Column::ModificationTime:
  421. return "Modified";
  422. case Column::Inode:
  423. return "Inode";
  424. }
  425. ASSERT_NOT_REACHED();
  426. }
  427. GModel::ColumnMetadata GFileSystemModel::column_metadata(int column) const
  428. {
  429. switch (column) {
  430. case Column::Icon:
  431. return { 16, TextAlignment::Center, nullptr, GModel::ColumnMetadata::Sortable::False };
  432. case Column::Name:
  433. return { 120, TextAlignment::CenterLeft };
  434. case Column::Size:
  435. return { 80, TextAlignment::CenterRight };
  436. case Column::Owner:
  437. return { 50, TextAlignment::CenterLeft };
  438. case Column::Group:
  439. return { 50, TextAlignment::CenterLeft };
  440. case Column::ModificationTime:
  441. return { 110, TextAlignment::CenterLeft };
  442. case Column::Permissions:
  443. return { 65, TextAlignment::CenterLeft };
  444. case Column::Inode:
  445. return { 60, TextAlignment::CenterRight };
  446. }
  447. ASSERT_NOT_REACHED();
  448. }