notmuch new/tag: Flush all changes to database when interrupted.
[notmuch] / notmuch-new.c
1 /* notmuch - Not much of an email program, (just index and search)
2  *
3  * Copyright © 2009 Carl Worth
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see http://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "notmuch-client.h"
22
23 static volatile sig_atomic_t do_add_files_print_progress = 0;
24
25 static void
26 handle_sigalrm (unused (int signal))
27 {
28     do_add_files_print_progress = 1;
29 }
30
31 static volatile sig_atomic_t interrupted;
32
33 static void
34 handle_sigint (unused (int sig))
35 {
36     static char msg[] = "Stopping...         \n";
37     write(2, msg, sizeof(msg)-1);
38     interrupted = 1;
39 }
40
41 static void
42 tag_inbox_and_unread (notmuch_message_t *message)
43 {
44     notmuch_message_add_tag (message, "inbox");
45     notmuch_message_add_tag (message, "unread");
46 }
47
48 static void
49 add_files_print_progress (add_files_state_t *state)
50 {
51     struct timeval tv_now;
52     double elapsed_overall, rate_overall;
53
54     gettimeofday (&tv_now, NULL);
55
56     elapsed_overall = notmuch_time_elapsed (state->tv_start, tv_now);
57     rate_overall = (state->processed_files) / elapsed_overall;
58
59     printf ("Processed %d", state->processed_files);
60
61     if (state->total_files) {
62         double time_remaining;
63
64         time_remaining = ((state->total_files - state->processed_files) /
65                           rate_overall);
66         printf (" of %d files (", state->total_files);
67         notmuch_time_print_formatted_seconds (time_remaining);
68         printf (" remaining).      \r");
69     } else {
70         printf (" files (%d files/sec.)    \r", (int) rate_overall);
71     }
72
73     fflush (stdout);
74 }
75
76 /* Examine 'path' recursively as follows:
77  *
78  *   o Ask the filesystem for the mtime of 'path' (path_mtime)
79  *
80  *   o Ask the database for its timestamp of 'path' (path_dbtime)
81  *
82  *   o If 'path_mtime' > 'path_dbtime'
83  *
84  *       o For each regular file in 'path' with mtime newer than the
85  *         'path_dbtime' call add_message to add the file to the
86  *         database.
87  *
88  *       o For each sub-directory of path, recursively call into this
89  *         same function.
90  *
91  *   o Tell the database to update its time of 'path' to 'path_mtime'
92  *
93  * The 'struct stat *st' must point to a structure that has already
94  * been initialized for 'path' by calling stat().
95  */
96 static notmuch_status_t
97 add_files_recursive (notmuch_database_t *notmuch,
98                      const char *path,
99                      struct stat *st,
100                      add_files_state_t *state)
101 {
102     DIR *dir = NULL;
103     struct dirent *e, *entry = NULL;
104     int entry_length;
105     int err;
106     char *next = NULL;
107     time_t path_mtime, path_dbtime;
108     notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
109     notmuch_message_t *message = NULL;
110
111     /* If we're told to, we bail out on encountering a read-only
112      * directory, (with this being a clear clue from the user to
113      * Notmuch that new mail won't be arriving there and we need not
114      * look. */
115     if (state->ignore_read_only_directories &&
116         (st->st_mode & S_IWUSR) == 0)
117     {
118         state->saw_read_only_directory = TRUE;
119         goto DONE;
120     }
121
122     path_mtime = st->st_mtime;
123
124     path_dbtime = notmuch_database_get_timestamp (notmuch, path);
125
126     dir = opendir (path);
127     if (dir == NULL) {
128         fprintf (stderr, "Error opening directory %s: %s\n",
129                  path, strerror (errno));
130         ret = NOTMUCH_STATUS_FILE_ERROR;
131         goto DONE;
132     }
133
134     entry_length = offsetof (struct dirent, d_name) +
135         pathconf (path, _PC_NAME_MAX) + 1;
136     entry = malloc (entry_length);
137
138     while (!interrupted) {
139         err = readdir_r (dir, entry, &e);
140         if (err) {
141             fprintf (stderr, "Error reading directory: %s\n",
142                      strerror (errno));
143             ret = NOTMUCH_STATUS_FILE_ERROR;
144             goto DONE;
145         }
146
147         if (e == NULL)
148             break;
149
150         /* If this directory hasn't been modified since the last
151          * add_files, then we only need to look further for
152          * sub-directories. */
153         if (path_mtime <= path_dbtime && entry->d_type != DT_DIR)
154             continue;
155
156         /* Ignore special directories to avoid infinite recursion.
157          * Also ignore the .notmuch directory.
158          */
159         /* XXX: Eventually we'll want more sophistication to let the
160          * user specify files to be ignored. */
161         if (strcmp (entry->d_name, ".") == 0 ||
162             strcmp (entry->d_name, "..") == 0 ||
163             strcmp (entry->d_name, ".notmuch") ==0)
164         {
165             continue;
166         }
167
168         next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
169
170         if (stat (next, st)) {
171             fprintf (stderr, "Error reading %s: %s\n",
172                      next, strerror (errno));
173             ret = NOTMUCH_STATUS_FILE_ERROR;
174             continue;
175         }
176
177         if (S_ISREG (st->st_mode)) {
178             /* If the file hasn't been modified since the last
179              * add_files, then we need not look at it. */
180             if (path_dbtime == 0 || st->st_mtime > path_dbtime) {
181                 state->processed_files++;
182
183                 status = notmuch_database_add_message (notmuch, next, &message);
184                 switch (status) {
185                     /* success */
186                     case NOTMUCH_STATUS_SUCCESS:
187                         state->added_messages++;
188                         tag_inbox_and_unread (message);
189                         break;
190                     /* Non-fatal issues (go on to next file) */
191                     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
192                         /* Stay silent on this one. */
193                         break;
194                     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
195                         fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
196                                  next);
197                         break;
198                     /* Fatal issues. Don't process anymore. */
199                     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
200                     case NOTMUCH_STATUS_OUT_OF_MEMORY:
201                         fprintf (stderr, "Error: %s. Halting processing.\n",
202                                  notmuch_status_to_string (status));
203                         ret = status;
204                         goto DONE;
205                     default:
206                     case NOTMUCH_STATUS_FILE_ERROR:
207                     case NOTMUCH_STATUS_NULL_POINTER:
208                     case NOTMUCH_STATUS_TAG_TOO_LONG:
209                     case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
210                     case NOTMUCH_STATUS_LAST_STATUS:
211                         INTERNAL_ERROR ("add_message returned unexpected value: %d",  status);
212                         goto DONE;
213                 }
214
215                 if (message) {
216                     notmuch_message_destroy (message);
217                     message = NULL;
218                 }
219
220                 if (do_add_files_print_progress) {
221                     do_add_files_print_progress = 0;
222                     add_files_print_progress (state);
223                 }
224             }
225         } else if (S_ISDIR (st->st_mode)) {
226             status = add_files_recursive (notmuch, next, st, state);
227             if (status && ret == NOTMUCH_STATUS_SUCCESS)
228                 ret = status;
229         }
230
231         talloc_free (next);
232         next = NULL;
233     }
234
235     status = notmuch_database_set_timestamp (notmuch, path, path_mtime);
236     if (status && ret == NOTMUCH_STATUS_SUCCESS)
237         ret = status;
238
239   DONE:
240     if (next)
241         talloc_free (next);
242     if (entry)
243         free (entry);
244     if (dir)
245         closedir (dir);
246
247     return ret;
248 }
249
250 /* This is the top-level entry point for add_files. It does a couple
251  * of error checks, sets up the progress-printing timer and then calls
252  * into the recursive function. */
253 notmuch_status_t
254 add_files (notmuch_database_t *notmuch,
255            const char *path,
256            add_files_state_t *state)
257 {
258     struct stat st;
259     notmuch_status_t status;
260     struct sigaction action;
261     struct itimerval timerval;
262
263     if (stat (path, &st)) {
264         fprintf (stderr, "Error reading directory %s: %s\n",
265                  path, strerror (errno));
266         return NOTMUCH_STATUS_FILE_ERROR;
267     }
268
269     if (! S_ISDIR (st.st_mode)) {
270         fprintf (stderr, "Error: %s is not a directory.\n", path);
271         return NOTMUCH_STATUS_FILE_ERROR;
272     }
273
274     /* Setup our handler for SIGALRM */
275     memset (&action, 0, sizeof (struct sigaction));
276     action.sa_handler = handle_sigalrm;
277     sigemptyset (&action.sa_mask);
278     action.sa_flags = SA_RESTART;
279     sigaction (SIGALRM, &action, NULL);
280
281     /* Then start a timer to send SIGALRM once per second. */
282     timerval.it_interval.tv_sec = 1;
283     timerval.it_interval.tv_usec = 0;
284     timerval.it_value.tv_sec = 1;
285     timerval.it_value.tv_usec = 0;
286     setitimer (ITIMER_REAL, &timerval, NULL);
287
288     status = add_files_recursive (notmuch, path, &st, state);
289
290     /* Now stop the timer. */
291     timerval.it_interval.tv_sec = 0;
292     timerval.it_interval.tv_usec = 0;
293     timerval.it_value.tv_sec = 0;
294     timerval.it_value.tv_usec = 0;
295     setitimer (ITIMER_REAL, &timerval, NULL);
296
297     /* And disable the signal handler. */
298     action.sa_handler = SIG_IGN;
299     sigaction (SIGALRM, &action, NULL);
300
301     return status;
302 }
303
304 /* XXX: This should be merged with the add_files function since it
305  * shares a lot of logic with it. */
306 /* Recursively count all regular files in path and all sub-direcotries
307  * of path.  The result is added to *count (which should be
308  * initialized to zero by the top-level caller before calling
309  * count_files). */
310 static void
311 count_files (const char *path, int *count)
312 {
313     DIR *dir;
314     struct dirent *e, *entry = NULL;
315     int entry_length;
316     int err;
317     char *next;
318     struct stat st;
319
320     dir = opendir (path);
321
322     if (dir == NULL) {
323         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
324                  path, strerror (errno));
325         goto DONE;
326     }
327
328     entry_length = offsetof (struct dirent, d_name) +
329         pathconf (path, _PC_NAME_MAX) + 1;
330     entry = malloc (entry_length);
331
332     while (!interrupted) {
333         err = readdir_r (dir, entry, &e);
334         if (err) {
335             fprintf (stderr, "Error reading directory: %s\n",
336                      strerror (errno));
337             free (entry);
338             goto DONE;
339         }
340
341         if (e == NULL)
342             break;
343
344         /* Ignore special directories to avoid infinite recursion.
345          * Also ignore the .notmuch directory.
346          */
347         /* XXX: Eventually we'll want more sophistication to let the
348          * user specify files to be ignored. */
349         if (strcmp (entry->d_name, ".") == 0 ||
350             strcmp (entry->d_name, "..") == 0 ||
351             strcmp (entry->d_name, ".notmuch") == 0)
352         {
353             continue;
354         }
355
356         if (asprintf (&next, "%s/%s", path, entry->d_name) == -1) {
357             next = NULL;
358             fprintf (stderr, "Error descending from %s to %s: Out of memory\n",
359                      path, entry->d_name);
360             continue;
361         }
362
363         stat (next, &st);
364
365         if (S_ISREG (st.st_mode)) {
366             *count = *count + 1;
367             if (*count % 1000 == 0) {
368                 printf ("Found %d files so far.\r", *count);
369                 fflush (stdout);
370             }
371         } else if (S_ISDIR (st.st_mode)) {
372             count_files (next, count);
373         }
374
375         free (next);
376     }
377
378   DONE:
379     if (entry)
380         free (entry);
381
382     closedir (dir);
383 }
384
385 int
386 notmuch_new_command (void *ctx,
387                      unused (int argc), unused (char *argv[]))
388 {
389     notmuch_config_t *config;
390     notmuch_database_t *notmuch;
391     add_files_state_t add_files_state;
392     double elapsed;
393     struct timeval tv_now;
394     int ret = 0;
395     struct stat st;
396     const char *db_path;
397     char *dot_notmuch_path;
398     struct sigaction action;
399
400     /* Setup our handler for SIGINT */
401     memset (&action, 0, sizeof (struct sigaction));
402     action.sa_handler = handle_sigint;
403     sigemptyset (&action.sa_mask);
404     action.sa_flags = SA_RESTART;
405     sigaction (SIGINT, &action, NULL);
406
407     config = notmuch_config_open (ctx, NULL, NULL);
408     if (config == NULL)
409         return 1;
410
411     db_path = notmuch_config_get_database_path (config);
412
413     dot_notmuch_path = talloc_asprintf (ctx, "%s/%s", db_path, ".notmuch");
414
415     if (stat (dot_notmuch_path, &st)) {
416         int count;
417
418         count = 0;
419         count_files (db_path, &count);
420         if (interrupted)
421             return 1;
422
423         notmuch = notmuch_database_create (db_path);
424         add_files_state.ignore_read_only_directories = FALSE;
425         add_files_state.total_files = count;
426     } else {
427         notmuch = notmuch_database_open (db_path);
428         add_files_state.ignore_read_only_directories = TRUE;
429         add_files_state.total_files = 0;
430     }
431
432     if (notmuch == NULL)
433         return 1;
434
435     talloc_free (dot_notmuch_path);
436     dot_notmuch_path = NULL;
437
438     add_files_state.saw_read_only_directory = FALSE;
439     add_files_state.total_files = 0;
440     add_files_state.processed_files = 0;
441     add_files_state.added_messages = 0;
442     gettimeofday (&add_files_state.tv_start, NULL);
443
444     ret = add_files (notmuch, db_path, &add_files_state);
445
446     gettimeofday (&tv_now, NULL);
447     elapsed = notmuch_time_elapsed (add_files_state.tv_start,
448                                     tv_now);
449     if (add_files_state.processed_files) {
450         printf ("Processed %d %s in ", add_files_state.processed_files,
451                 add_files_state.processed_files == 1 ?
452                 "file" : "total files");
453         notmuch_time_print_formatted_seconds (elapsed);
454         if (elapsed > 1) {
455             printf (" (%d files/sec.).                 \n",
456                     (int) (add_files_state.processed_files / elapsed));
457         } else {
458             printf (".                    \n");
459         }
460     }
461     if (add_files_state.added_messages) {
462         printf ("Added %d new %s to the database (not much, really).\n",
463                 add_files_state.added_messages,
464                 add_files_state.added_messages == 1 ?
465                 "message" : "messages");
466     } else {
467         printf ("No new mail---and that's not much.\n");
468     }
469
470     if (elapsed > 1 && ! add_files_state.saw_read_only_directory) {
471         printf ("\nTip: If you have any sub-directories that are archives (that is,\n"
472                 "they will never receive new mail), marking these directores as\n"
473                 "read-only (chmod u-w /path/to/dir) will make \"notmuch new\"\n"
474                 "much more efficient (it won't even look in those directories).\n");
475     }
476
477     if (ret) {
478         printf ("\nNote: At least one error was encountered: %s\n",
479                 notmuch_status_to_string (ret));
480     }
481
482     notmuch_database_close (notmuch);
483
484     return ret || interrupted;
485 }