1 /*
2 ** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
3 **
4 ** This program is free software; you can redistribute it and/or modify it
5 ** under the terms of the GNU General Public License as published by the
6 ** Free Software Foundation; either version 3, or (at your option) any
7 ** later version.
8 **
9 ** This program is distributed in the hope that it will be useful,
10 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
11 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 ** GNU General Public License for more details.
13 **
14 ** You should have received a copy of the GNU General Public License
15 ** along with this program; if not, write to the Free Software Foundation,
16 ** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 **
18 */
19 #include "mu-scanner.hh"
20 
21 #include "config.h"
22 
23 #include <chrono>
24 #include <mutex>
25 #include <atomic>
26 #include <thread>
27 #include <cstring>
28 
29 #include <sys/types.h>
30 #include <sys/stat.h>
31 #include <unistd.h>
32 
33 #include <glib.h>
34 
35 #include "utils/mu-utils.hh"
36 #include "utils/mu-error.hh"
37 
38 using namespace Mu;
39 
40 struct Scanner::Private {
PrivateScanner::Private41         Private (const std::string& root_dir,
42                  Scanner::Handler handler):
43                  root_dir_{root_dir}, handler_{handler} {
44                 if (!handler_)
45                         throw Mu::Error{Error::Code::Internal, "missing handler"};
46                 }
~PrivateScanner::Private47         ~Private() {
48                 stop();
49         }
50 
51         bool start();
52         bool stop();
53         bool process_dentry (const std::string& path, struct dirent *dentry, bool is_maildir);
54         bool process_dir (const std::string& path, bool is_maildir);
55 
56         const std::string      root_dir_;
57         const Scanner::Handler handler_;
58         std::atomic<bool>      running_{};
59         std::mutex             lock_;
60 };
61 
62 
63 static bool
is_special_dir(const struct dirent * dentry)64 is_special_dir (const struct dirent *dentry)
65 {
66         const auto d_name{dentry->d_name};
67         return d_name[0] == '\0' ||
68                 (d_name[1] == '\0' && d_name[0] == '.') ||
69                 (d_name[2] == '\0' && d_name[0] == '.' && d_name[1] == '.');
70 }
71 
72 static bool
is_new_cur(const char * dirname)73 is_new_cur (const char *dirname)
74 {
75         if (dirname[0] == 'c' && dirname[1] == 'u' && dirname[2] == 'r' && dirname[3] == '\0')
76                 return true;
77 
78        if (dirname[0] == 'n' && dirname[1] == 'e' && dirname[2] == 'w' && dirname[3] == '\0')
79                 return true;
80 
81         return false;
82 }
83 
84 bool
process_dentry(const std::string & path,struct dirent * dentry,bool is_maildir)85 Scanner::Private::process_dentry (const std::string& path, struct dirent *dentry,
86                                   bool is_maildir)
87 {
88         if (is_special_dir (dentry))
89                 return true; // ignore.
90 
91         const auto fullpath{path + "/" + dentry->d_name};
92         struct stat statbuf;
93         if (::stat(fullpath.c_str(), &statbuf) != 0) {
94                 g_warning ("failed to stat %s: %s", fullpath.c_str(), g_strerror(errno));
95                 return false;
96         }
97 
98         if (S_ISDIR(statbuf.st_mode)) {
99                 const auto new_cur = is_new_cur(dentry->d_name);
100                 const auto htype   = new_cur ?
101                         Scanner::HandleType::EnterNewCur :
102                         Scanner::HandleType::EnterDir;
103                 const auto res     = handler_(fullpath, &statbuf, htype);
104                 if (!res)
105                         return true; // skip
106 
107                 process_dir (fullpath, new_cur);
108 
109                 return handler_(fullpath, &statbuf, Scanner::HandleType::LeaveDir);
110 
111         } else if (S_ISREG(statbuf.st_mode) && is_maildir)
112                 return handler_(fullpath, &statbuf, Scanner::HandleType::File);
113 
114         g_debug ("skip %s (neither maildir-file nor directory)", fullpath.c_str());
115         return true;
116 }
117 
118 
119 bool
process_dir(const std::string & path,bool is_maildir)120 Scanner::Private::process_dir (const std::string& path, bool is_maildir)
121 {
122         const auto dir = opendir (path.c_str());
123         if (G_UNLIKELY(!dir)) {
124                 g_warning("failed to scan dir %s: %s", path.c_str(), g_strerror(errno));
125                 return false;
126         }
127 
128         // TODO: sort dentries by inode order, which makes things faster for extfs.
129         // see mu-maildir.c
130 
131         while (running_) {
132                 errno = 0;
133                 const auto dentry{readdir(dir)};
134 
135                 if (G_LIKELY(dentry)) {
136                         process_dentry (path, dentry, is_maildir);
137                         continue;
138                 }
139 
140                 if (errno != 0) {
141                         g_warning("failed to read %s: %s", path.c_str(), g_strerror(errno));
142                         continue;
143                 }
144 
145                 break;
146         }
147         closedir (dir);
148 
149         return true;
150 }
151 
152 bool
start()153 Scanner::Private::start()
154 {
155         const auto& path{root_dir_};
156         if (G_UNLIKELY(path.length() > PATH_MAX)) {
157                 g_warning("path too long");
158                 return false;
159         }
160 
161         const auto mode{F_OK | R_OK};
162         if (G_UNLIKELY(access (path.c_str(), mode) != 0)) {
163                 g_warning("'%s' is not readable: %s", path.c_str(), g_strerror (errno));
164                 return false;
165         }
166 
167         struct stat statbuf{};
168         if (G_UNLIKELY(stat (path.c_str(), &statbuf) != 0)) {
169                 g_warning("'%s' is not stat'able: %s", path.c_str(), g_strerror (errno));
170                 return false;
171         }
172 
173         if (G_UNLIKELY(!S_ISDIR (statbuf.st_mode))) {
174                 g_warning("'%s' is not a directory", path.c_str());
175                 return false;
176         }
177 
178         running_ = true;
179         g_debug ("starting scan @ %s", root_dir_.c_str());
180 
181         auto basename{g_path_get_basename(root_dir_.c_str())};
182         const auto is_maildir = (g_strcmp0(basename, "cur") == 0 ||
183                                  g_strcmp0(basename,"new") == 0);
184         g_free(basename);
185 
186         const auto start{std::chrono::steady_clock::now()};
187         process_dir(root_dir_, is_maildir);
188         const auto elapsed = std::chrono::steady_clock::now() - start;
189         g_debug ("finished scan of %s in %" G_GINT64_FORMAT " ms", root_dir_.c_str(),
190                  to_ms(elapsed));
191         running_ = false;
192 
193         return true;
194 }
195 
196 bool
stop()197 Scanner::Private::stop()
198 {
199         if (!running_)
200                 return true; // nothing to do
201 
202         g_debug ("stopping scan");
203         running_ = false;
204 
205         return true;
206 }
207 
Scanner(const std::string & root_dir,Scanner::Handler handler)208 Scanner::Scanner (const std::string& root_dir,
209                  Scanner::Handler handler):
210         priv_{std::make_unique<Private>(root_dir, handler)}
211 {}
212 
213 Scanner::~Scanner() = default;
214 
215 bool
start()216 Scanner::start()
217 {
218         {
219                 std::lock_guard<std::mutex> l(priv_->lock_);
220                 if (priv_->running_)
221                         return true; //nothing to do
222 
223                 priv_->running_ = true;
224         }
225 
226         const auto res = priv_->start();
227         priv_->running_ = false;
228 
229         return res;
230 }
231 
232 bool
stop()233 Scanner::stop()
234 {
235         std::lock_guard<std::mutex> l(priv_->lock_);
236 
237         return priv_->stop();
238 }
239 
240 bool
is_running() const241 Scanner::is_running() const
242 {
243         return priv_->running_;
244 }
245