Serenity Operating System
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
27#include <AK/FileSystemPath.h>
28#include <AK/StringBuilder.h>
29#include <LibCore/DirIterator.h>
30#include <LibGUI/FileSystemModel.h>
31#include <LibGUI/Painter.h>
32#include <LibGfx/Bitmap.h>
33#include <LibThread/BackgroundAction.h>
34#include <dirent.h>
35#include <grp.h>
36#include <pwd.h>
37#include <stdio.h>
38#include <sys/stat.h>
39#include <unistd.h>
40
41namespace GUI {
42
43ModelIndex FileSystemModel::Node::index(const FileSystemModel& model, int column) const
44{
45 if (!parent)
46 return {};
47 for (size_t row = 0; row < parent->children.size(); ++row) {
48 if (&parent->children[row] == this)
49 return model.create_index(row, column, const_cast<Node*>(this));
50 }
51 ASSERT_NOT_REACHED();
52}
53
54bool 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 perror("stat/lstat");
64 return false;
65 }
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
74 if (S_ISLNK(mode)) {
75 char buffer[PATH_MAX];
76 int length = readlink(full_path.characters(), buffer, sizeof(buffer));
77 if (length < 0) {
78 perror("readlink");
79 } else {
80 ASSERT(length > 0);
81 symlink_target = String(buffer, length - 1);
82 }
83 }
84
85 return true;
86}
87
88void FileSystemModel::Node::traverse_if_needed(const FileSystemModel& model)
89{
90 if (!is_directory() || has_traversed)
91 return;
92 has_traversed = true;
93 total_size = 0;
94
95 auto full_path = this->full_path(model);
96 Core::DirIterator di(full_path, Core::DirIterator::SkipDots);
97 if (di.has_error()) {
98 fprintf(stderr, "DirIterator: %s\n", di.error_string());
99 return;
100 }
101
102 while (di.has_next()) {
103 String name = di.next_path();
104 String child_path = String::format("%s/%s", full_path.characters(), name.characters());
105 NonnullOwnPtr<Node> child = make<Node>();
106 bool ok = child->fetch_data(child_path, false);
107 if (!ok)
108 continue;
109 if (model.m_mode == DirectoriesOnly && !S_ISDIR(child->mode))
110 continue;
111 child->name = name;
112 child->parent = this;
113 total_size += child->size;
114 children.append(move(child));
115 }
116
117 if (m_watch_fd >= 0)
118 return;
119
120 m_watch_fd = watch_file(full_path.characters(), full_path.length());
121 if (m_watch_fd < 0) {
122 perror("watch_file");
123 return;
124 }
125 fcntl(m_watch_fd, F_SETFD, FD_CLOEXEC);
126 dbg() << "Watching " << full_path << " for changes, m_watch_fd = " << m_watch_fd;
127 m_notifier = Core::Notifier::construct(m_watch_fd, Core::Notifier::Event::Read);
128 m_notifier->on_ready_to_read = [this, &model] {
129 char buffer[32];
130 int rc = read(m_notifier->fd(), buffer, sizeof(buffer));
131 ASSERT(rc >= 0);
132
133 has_traversed = false;
134 mode = 0;
135 children.clear();
136 reify_if_needed(model);
137 const_cast<FileSystemModel&>(model).did_update();
138 };
139}
140
141void FileSystemModel::Node::reify_if_needed(const FileSystemModel& model)
142{
143 traverse_if_needed(model);
144 if (mode != 0)
145 return;
146 fetch_data(full_path(model), parent == nullptr);
147}
148
149String FileSystemModel::Node::full_path(const FileSystemModel& model) const
150{
151 Vector<String, 32> lineage;
152 for (auto* ancestor = parent; ancestor; ancestor = ancestor->parent) {
153 lineage.append(ancestor->name);
154 }
155 StringBuilder builder;
156 builder.append(model.root_path());
157 for (int i = lineage.size() - 1; i >= 0; --i) {
158 builder.append('/');
159 builder.append(lineage[i]);
160 }
161 builder.append('/');
162 builder.append(name);
163 return canonicalized_path(builder.to_string());
164}
165
166ModelIndex FileSystemModel::index(const StringView& path, int column) const
167{
168 FileSystemPath canonical_path(path);
169 const Node* node = m_root;
170 if (canonical_path.string() == "/")
171 return m_root->index(*this, column);
172 for (size_t i = 0; i < canonical_path.parts().size(); ++i) {
173 auto& part = canonical_path.parts()[i];
174 bool found = false;
175 for (auto& child : node->children) {
176 if (child.name == part) {
177 const_cast<Node&>(child).reify_if_needed(*this);
178 node = &child;
179 found = true;
180 if (i == canonical_path.parts().size() - 1)
181 return child.index(*this, column);
182 break;
183 }
184 }
185 if (!found)
186 return {};
187 }
188 return {};
189}
190
191String FileSystemModel::full_path(const ModelIndex& index) const
192{
193 auto& node = this->node(index);
194 const_cast<Node&>(node).reify_if_needed(*this);
195 return node.full_path(*this);
196}
197
198FileSystemModel::FileSystemModel(const StringView& root_path, Mode mode)
199 : m_root_path(canonicalized_path(root_path))
200 , m_mode(mode)
201{
202 m_directory_icon = Icon::default_icon("filetype-folder");
203 m_file_icon = Icon::default_icon("filetype-unknown");
204 m_symlink_icon = Icon::default_icon("filetype-symlink");
205 m_socket_icon = Icon::default_icon("filetype-socket");
206 m_executable_icon = Icon::default_icon("filetype-executable");
207 m_filetype_image_icon = Icon::default_icon("filetype-image");
208 m_filetype_sound_icon = Icon::default_icon("filetype-sound");
209 m_filetype_html_icon = Icon::default_icon("filetype-html");
210 m_filetype_cplusplus_icon = Icon::default_icon("filetype-cplusplus");
211 m_filetype_java_icon = Icon::default_icon("filetype-java");
212 m_filetype_javascript_icon = Icon::default_icon("filetype-javascript");
213 m_filetype_text_icon = Icon::default_icon("filetype-text");
214 m_filetype_pdf_icon = Icon::default_icon("filetype-pdf");
215 m_filetype_library_icon = Icon::default_icon("filetype-library");
216 m_filetype_object_icon = Icon::default_icon("filetype-object");
217
218 setpwent();
219 while (auto* passwd = getpwent())
220 m_user_names.set(passwd->pw_uid, passwd->pw_name);
221 endpwent();
222
223 setgrent();
224 while (auto* group = getgrent())
225 m_group_names.set(group->gr_gid, group->gr_name);
226 endgrent();
227
228 update();
229}
230
231FileSystemModel::~FileSystemModel()
232{
233}
234
235String FileSystemModel::name_for_uid(uid_t uid) const
236{
237 auto it = m_user_names.find(uid);
238 if (it == m_user_names.end())
239 return String::number(uid);
240 return (*it).value;
241}
242
243String FileSystemModel::name_for_gid(gid_t gid) const
244{
245 auto it = m_group_names.find(gid);
246 if (it == m_group_names.end())
247 return String::number(gid);
248 return (*it).value;
249}
250
251static String permission_string(mode_t mode)
252{
253 StringBuilder builder;
254 if (S_ISDIR(mode))
255 builder.append("d");
256 else if (S_ISLNK(mode))
257 builder.append("l");
258 else if (S_ISBLK(mode))
259 builder.append("b");
260 else if (S_ISCHR(mode))
261 builder.append("c");
262 else if (S_ISFIFO(mode))
263 builder.append("f");
264 else if (S_ISSOCK(mode))
265 builder.append("s");
266 else if (S_ISREG(mode))
267 builder.append("-");
268 else
269 builder.append("?");
270
271 builder.appendf("%c%c%c%c%c%c%c%c",
272 mode & S_IRUSR ? 'r' : '-',
273 mode & S_IWUSR ? 'w' : '-',
274 mode & S_ISUID ? 's' : (mode & S_IXUSR ? 'x' : '-'),
275 mode & S_IRGRP ? 'r' : '-',
276 mode & S_IWGRP ? 'w' : '-',
277 mode & S_ISGID ? 's' : (mode & S_IXGRP ? 'x' : '-'),
278 mode & S_IROTH ? 'r' : '-',
279 mode & S_IWOTH ? 'w' : '-');
280
281 if (mode & S_ISVTX)
282 builder.append("t");
283 else
284 builder.appendf("%c", mode & S_IXOTH ? 'x' : '-');
285 return builder.to_string();
286}
287
288void FileSystemModel::set_root_path(const StringView& root_path)
289{
290 m_root_path = canonicalized_path(root_path);
291
292 if (on_root_path_change)
293 on_root_path_change();
294
295 update();
296}
297
298void FileSystemModel::update()
299{
300 m_root = make<Node>();
301 m_root->reify_if_needed(*this);
302
303 did_update();
304}
305
306int FileSystemModel::row_count(const ModelIndex& index) const
307{
308 Node& node = const_cast<Node&>(this->node(index));
309 node.reify_if_needed(*this);
310 if (node.is_directory())
311 return node.children.size();
312 return 0;
313}
314
315const FileSystemModel::Node& FileSystemModel::node(const ModelIndex& index) const
316{
317 if (!index.is_valid())
318 return *m_root;
319 return *(Node*)index.internal_data();
320}
321
322ModelIndex FileSystemModel::index(int row, int column, const ModelIndex& parent) const
323{
324 if (row < 0 || column < 0)
325 return {};
326 auto& node = this->node(parent);
327 const_cast<Node&>(node).reify_if_needed(*this);
328 if (static_cast<size_t>(row) >= node.children.size())
329 return {};
330 return create_index(row, column, &node.children[row]);
331}
332
333ModelIndex FileSystemModel::parent_index(const ModelIndex& index) const
334{
335 if (!index.is_valid())
336 return {};
337 auto& node = this->node(index);
338 if (!node.parent) {
339 ASSERT(&node == m_root);
340 return {};
341 }
342 return node.parent->index(*this, index.column());
343}
344
345Variant FileSystemModel::data(const ModelIndex& index, Role role) const
346{
347 ASSERT(index.is_valid());
348 auto& node = this->node(index);
349
350 if (role == Role::Custom) {
351 // For GUI::FileSystemModel, custom role means the full path.
352 ASSERT(index.column() == Column::Name);
353 return node.full_path(*this);
354 }
355
356 if (role == Role::DragData) {
357 if (index.column() == Column::Name) {
358 StringBuilder builder;
359 builder.append("file://");
360 builder.append(node.full_path(*this));
361 return builder.to_string();
362 }
363 return {};
364 }
365
366 if (role == Role::Sort) {
367 switch (index.column()) {
368 case Column::Icon:
369 return node.is_directory() ? 0 : 1;
370 case Column::Name:
371 return node.name;
372 case Column::Size:
373 return (int)node.size;
374 case Column::Owner:
375 return name_for_uid(node.uid);
376 case Column::Group:
377 return name_for_gid(node.gid);
378 case Column::Permissions:
379 return permission_string(node.mode);
380 case Column::ModificationTime:
381 return node.mtime;
382 case Column::Inode:
383 return (int)node.inode;
384 case Column::SymlinkTarget:
385 return node.symlink_target;
386 }
387 ASSERT_NOT_REACHED();
388 }
389
390 if (role == Role::Display) {
391 switch (index.column()) {
392 case Column::Icon:
393 return icon_for(node);
394 case Column::Name:
395 return node.name;
396 case Column::Size:
397 return (int)node.size;
398 case Column::Owner:
399 return name_for_uid(node.uid);
400 case Column::Group:
401 return name_for_gid(node.gid);
402 case Column::Permissions:
403 return permission_string(node.mode);
404 case Column::ModificationTime:
405 return timestamp_string(node.mtime);
406 case Column::Inode:
407 return (int)node.inode;
408 case Column::SymlinkTarget:
409 return node.symlink_target;
410 }
411 }
412
413 if (role == Role::Icon) {
414 return icon_for(node);
415 }
416 return {};
417}
418
419Icon FileSystemModel::icon_for_file(const mode_t mode, const String& name) const
420{
421 if (S_ISDIR(mode))
422 return m_directory_icon;
423 if (S_ISLNK(mode))
424 return m_symlink_icon;
425 if (S_ISSOCK(mode))
426 return m_socket_icon;
427 if (mode & (S_IXUSR | S_IXGRP | S_IXOTH))
428 return m_executable_icon;
429 if (name.to_lowercase().ends_with(".wav"))
430 return m_filetype_sound_icon;
431 if (name.to_lowercase().ends_with(".html"))
432 return m_filetype_html_icon;
433 if (name.to_lowercase().ends_with(".png"))
434 return m_filetype_image_icon;
435 if (name.to_lowercase().ends_with(".cpp"))
436 return m_filetype_cplusplus_icon;
437 if (name.to_lowercase().ends_with(".java"))
438 return m_filetype_java_icon;
439 if (name.to_lowercase().ends_with(".js"))
440 return m_filetype_javascript_icon;
441 if (name.to_lowercase().ends_with(".txt"))
442 return m_filetype_text_icon;
443 if (name.to_lowercase().ends_with(".pdf"))
444 return m_filetype_pdf_icon;
445 if (name.to_lowercase().ends_with(".o") || name.to_lowercase().ends_with(".obj"))
446 return m_filetype_object_icon;
447 if (name.to_lowercase().ends_with(".so") || name.to_lowercase().ends_with(".a"))
448 return m_filetype_library_icon;
449
450 return m_file_icon;
451}
452
453Icon FileSystemModel::icon_for(const Node& node) const
454{
455 if (node.name.to_lowercase().ends_with(".png")) {
456 if (!node.thumbnail) {
457 if (!const_cast<FileSystemModel*>(this)->fetch_thumbnail_for(node))
458 return m_filetype_image_icon;
459 }
460 return GUI::Icon(m_filetype_image_icon.bitmap_for_size(16), *node.thumbnail);
461 }
462
463 return icon_for_file(node.mode, node.name);
464}
465
466static HashMap<String, RefPtr<Gfx::Bitmap>> s_thumbnail_cache;
467
468static RefPtr<Gfx::Bitmap> render_thumbnail(const StringView& path)
469{
470 auto png_bitmap = Gfx::Bitmap::load_from_file(path);
471 if (!png_bitmap)
472 return nullptr;
473
474 double scale = min(32 / (double)png_bitmap->width(), 32 / (double)png_bitmap->height());
475
476 auto thumbnail = Gfx::Bitmap::create(png_bitmap->format(), { 32, 32 });
477 Gfx::Rect destination = Gfx::Rect(0, 0, (int)(png_bitmap->width() * scale), (int)(png_bitmap->height() * scale));
478 destination.center_within(thumbnail->rect());
479
480 Painter painter(*thumbnail);
481 painter.draw_scaled_bitmap(destination, *png_bitmap, png_bitmap->rect());
482 return thumbnail;
483}
484
485bool FileSystemModel::fetch_thumbnail_for(const Node& node)
486{
487 // See if we already have the thumbnail
488 // we're looking for in the cache.
489 auto path = node.full_path(*this);
490 auto it = s_thumbnail_cache.find(path);
491 if (it != s_thumbnail_cache.end()) {
492 if (!(*it).value)
493 return false;
494 node.thumbnail = (*it).value;
495 return true;
496 }
497
498 // Otherwise, arrange to render the thumbnail
499 // in background and make it available later.
500
501 s_thumbnail_cache.set(path, nullptr);
502 m_thumbnail_progress_total++;
503
504 auto weak_this = make_weak_ptr();
505
506 LibThread::BackgroundAction<RefPtr<Gfx::Bitmap>>::create(
507 [path] {
508 return render_thumbnail(path);
509 },
510
511 [this, path, weak_this](auto thumbnail) {
512 s_thumbnail_cache.set(path, move(thumbnail));
513
514 // The model was destroyed, no need to update
515 // progress or call any event handlers.
516 if (weak_this.is_null())
517 return;
518
519 m_thumbnail_progress++;
520 if (on_thumbnail_progress)
521 on_thumbnail_progress(m_thumbnail_progress, m_thumbnail_progress_total);
522 if (m_thumbnail_progress == m_thumbnail_progress_total) {
523 m_thumbnail_progress = 0;
524 m_thumbnail_progress_total = 0;
525 }
526
527 did_update();
528 });
529
530 return false;
531}
532
533int FileSystemModel::column_count(const ModelIndex&) const
534{
535 return Column::__Count;
536}
537
538String FileSystemModel::column_name(int column) const
539{
540 switch (column) {
541 case Column::Icon:
542 return "";
543 case Column::Name:
544 return "Name";
545 case Column::Size:
546 return "Size";
547 case Column::Owner:
548 return "Owner";
549 case Column::Group:
550 return "Group";
551 case Column::Permissions:
552 return "Mode";
553 case Column::ModificationTime:
554 return "Modified";
555 case Column::Inode:
556 return "Inode";
557 case Column::SymlinkTarget:
558 return "Symlink target";
559 }
560 ASSERT_NOT_REACHED();
561}
562
563Model::ColumnMetadata FileSystemModel::column_metadata(int column) const
564{
565 switch (column) {
566 case Column::Icon:
567 return { 16, Gfx::TextAlignment::Center, nullptr, Model::ColumnMetadata::Sortable::False };
568 case Column::Name:
569 return { 120, Gfx::TextAlignment::CenterLeft };
570 case Column::Size:
571 return { 80, Gfx::TextAlignment::CenterRight };
572 case Column::Owner:
573 return { 50, Gfx::TextAlignment::CenterLeft };
574 case Column::Group:
575 return { 50, Gfx::TextAlignment::CenterLeft };
576 case Column::ModificationTime:
577 return { 110, Gfx::TextAlignment::CenterLeft };
578 case Column::Permissions:
579 return { 65, Gfx::TextAlignment::CenterLeft };
580 case Column::Inode:
581 return { 60, Gfx::TextAlignment::CenterRight };
582 case Column::SymlinkTarget:
583 return { 120, Gfx::TextAlignment::CenterLeft };
584 }
585 ASSERT_NOT_REACHED();
586}
587
588bool FileSystemModel::accepts_drag(const ModelIndex& index, const StringView& data_type)
589{
590 if (!index.is_valid())
591 return false;
592 if (data_type != "text/uri-list")
593 return false;
594 auto& node = this->node(index);
595 return node.is_directory();
596}
597
598}