Now refreshes properly the progress bar if the tty width changes.
[finddup.git] / finddup.c
1
2 /*
3  *  finddup is a simple utility to find duplicated files, files common
4  *  to several directories, or files present in one directory and not
5  *  in another one.
6  *
7  *  Copyright (c) 2010 Francois Fleuret
8  *  Written by Francois Fleuret <francois@fleuret.org>
9  *
10  *  This file is part of finddup.
11  *
12  *  finddup is free software: you can redistribute it and/or modify it
13  *  under the terms of the GNU General Public License version 3 as
14  *  published by the Free Software Foundation.
15  *
16  *  finddup is distributed in the hope that it will be useful, but WITHOUT
17  *  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
18  *  or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
19  *  License for more details.
20  *
21  *  You should have received a copy of the GNU General Public License
22  *  along with finddup.  If not, see <http://www.gnu.org/licenses/>.
23  *
24  */
25
26 #define VERSION_NUMBER "0.9"
27
28 #define _BSD_SOURCE
29
30 #include <sys/types.h>
31 #include <sys/stat.h>
32 #include <sys/param.h>
33 #include <dirent.h>
34 #include <stdlib.h>
35 #include <stdio.h>
36 #include <unistd.h>
37 #include <errno.h>
38 #include <string.h>
39 #include <sys/ioctl.h>
40 #include <locale.h>
41 #include <getopt.h>
42 #include <fcntl.h>
43 #ifdef WITH_MD5
44 #include <openssl/md5.h>
45 #endif
46
47 /* 1M really helps compared to 64k */
48 #define READ_BUFFER_SIZE (1024 * 1024)
49
50 typedef int64_t size_sum_t;
51
52 /* Yeah, global variables! */
53
54 int ignore_dotfiles = 0; /* 1 means ignore files and directories
55                             starting with a dot */
56
57 int ignore_empty_files = 0; /* 1 means ignore empty files */
58
59 int show_realpaths = 0; /* 1 means ignore files and directories
60                             starting with a dot */
61
62 int show_progress = 0; /* 1 means show a progress bar when we are in a
63                           tty */
64
65 int show_hits = 1; /* 1 means to show the files from dir2
66                       corresponding to the ones from dir1 */
67
68 int show_groups = 1; /* 1 means to show the group IDs when printing
69                         file names */
70
71 int same_inodes_are_different = 0; /* 1 means that comparison between
72                                       two files with same inode will
73                                       always be false */
74
75 #ifdef WITH_MD5
76 int use_md5 = 0; /* 1 means we keep an MD5 signature for each file */
77 #endif
78
79 /********************************************************************/
80
81 /* malloc with error checking.  */
82
83 void *safe_malloc(size_t n) {
84   void *p = malloc(n);
85   if (!p && n != 0) {
86     printf("Can not allocate memory: %s\n", strerror(errno));
87     exit(EXIT_FAILURE);
88   }
89   return p;
90 }
91
92 /********************************************************************/
93
94 int ignore_entry(const char *name) {
95   return
96     strcmp(name, ".") == 0 ||
97     strcmp(name, "..") == 0 ||
98     (ignore_dotfiles && name[0] == '.' && name[1] != '/');
99 }
100
101 /**********************************************************************/
102
103 struct file_node {
104   struct file_node *next;
105   char *name;
106   size_t size;
107   ino_t inode;
108   int group_id; /* one per identical file content */
109   int dir_id; /* 1 for DIR1, and 2 for DIR2 */
110 #ifdef WITH_MD5
111   int md5_computed;
112   unsigned char md5[MD5_DIGEST_LENGTH];
113 #endif
114 };
115
116 void file_list_delete(struct file_node *head) {
117   struct file_node *next;
118   while(head) {
119     next = head->next;
120     free(head->name);
121     free(head);
122     head = next;
123   }
124 }
125
126 int file_list_length(struct file_node *head) {
127   int l = 0;
128   while(head) {
129     l++;
130     head = head->next;
131   }
132   return l;
133 }
134
135 /**********************************************************************/
136
137 int same_content(struct file_node *f1, struct file_node *f2,
138                  char *buffer1, char *buffer2) {
139   int fd1, fd2, s1, s2;
140
141 #ifdef WITH_MD5
142   MD5_CTX c1, c2;
143
144   if(use_md5) {
145     if(f1->md5_computed && f2->md5_computed) {
146       if(!memcmp(f1->md5, f2->md5, MD5_DIGEST_LENGTH)) {
147         return 0;
148       }
149     } else {
150       if(!f1->md5_computed) {
151         MD5_Init(&c1);
152       }
153       if(!f2->md5_computed) {
154         MD5_Init(&c2);
155       }
156     }
157   }
158 #endif
159
160   fd1 = open(f1->name, O_RDONLY);
161   fd2 = open(f2->name, O_RDONLY);
162
163   if(fd1 >= 0 && fd2 >= 0) {
164     while(1) {
165       s1 = read(fd1, buffer1, READ_BUFFER_SIZE);
166       s2 = read(fd2, buffer2, READ_BUFFER_SIZE);
167
168       if(s1 < 0 || s2 < 0) {
169         close(fd1);
170         close(fd2);
171         return 0;
172       }
173
174       if(s1 == s2) {
175         if(s1 == 0) {
176           close(fd1);
177           close(fd2);
178 #ifdef WITH_MD5
179           if(use_md5) {
180             if(!f1->md5_computed) {
181               MD5_Final(f1->md5, &c1);
182               f1->md5_computed = 1;
183             }
184             if(!f2->md5_computed) {
185               MD5_Final(f2->md5, &c2);
186               f2->md5_computed = 1;
187             }
188           }
189 #endif
190           return 1;
191         } else {
192           if(memcmp(buffer1, buffer2, s1)) {
193             close(fd1);
194             close(fd2);
195             return 0;
196           }
197 #ifdef WITH_MD5
198           if(use_md5) {
199             if(!f1->md5_computed) {
200               MD5_Update(&c1, buffer1, s1);
201             }
202             if(!f2->md5_computed) {
203               MD5_Update(&c2, buffer2, s2);
204             }
205           }
206 #endif
207         }
208       } else {
209         fprintf(stderr,
210                 "Different read size without error on files of same size.\n");
211         exit(EXIT_FAILURE);
212       }
213     }
214   } else {
215
216     if(fd1 < 0) {
217       fprintf(stderr,
218               "Can not open \"%s\" error: %s\n",
219               f1->name,
220               strerror(errno));
221     }
222
223     if(fd2 < 0) {
224       fprintf(stderr,
225               "Can not open \"%s\" error: %s\n",
226               f2->name,
227               strerror(errno));
228     }
229
230     exit(EXIT_FAILURE);
231   }
232 }
233
234 int same_files(struct file_node *f1, struct file_node *f2,
235                char *buffer1, char *buffer2) {
236   if(same_inodes_are_different && f1->inode == f2->inode) {
237     return 0;
238   }
239
240   return f1->size == f2->size && same_content(f1, f2, buffer1, buffer2);
241 }
242
243 /**********************************************************************/
244
245 struct file_node *scan_directory(struct file_node *tail, const char *name) {
246   DIR *dir;
247   struct dirent *dir_e;
248   struct stat sb;
249   struct file_node *tmp;
250   char subname[PATH_MAX + 1];
251
252   if(lstat(name, &sb) != 0) {
253     fprintf(stderr, "Can not stat \"%s\": %s\n", name, strerror(errno));
254     exit(EXIT_FAILURE);
255   }
256
257   if(S_ISLNK(sb.st_mode)) {
258     return tail;
259   }
260
261   dir = opendir(name);
262
263   if(dir) {
264     while((dir_e = readdir(dir))) {
265       if(!ignore_entry(dir_e->d_name)) {
266         snprintf(subname, PATH_MAX, "%s/%s", name, dir_e->d_name);
267         tail = scan_directory(tail, subname);
268       }
269     }
270     closedir(dir);
271   } else {
272     if(S_ISREG(sb.st_mode)) {
273       if(!ignore_entry(name)) {
274         if(!ignore_empty_files || sb.st_size > 0) {
275           tmp = safe_malloc(sizeof(struct file_node));
276           tmp->next = tail;
277           tmp->name = strdup(name);
278           tmp->size = sb.st_size;
279           tmp->inode = sb.st_ino;
280           tmp->group_id = -1;
281           tmp->dir_id = -1;
282 #ifdef WITH_MD5
283           tmp->md5_computed = 0;
284 #endif
285           tail = tmp;
286         }
287       }
288     }
289   }
290
291   return tail;
292 }
293
294 void print_file(struct file_node *node) {
295   char tmp[PATH_MAX + 1];
296   if(show_realpaths) {
297     if(realpath(node->name, tmp)) {
298       if(show_groups) {
299         printf("%d %s\n", node->group_id, tmp);
300       } else {
301         printf("%s\n", tmp);
302       }
303     } else {
304       printf("Can not get the realpath of \"%s\": %s\n",
305              node->name,
306              strerror(errno));
307       exit(EXIT_FAILURE);
308     }
309   } else {
310     if(show_groups) {
311       printf("%d %s\n", node->group_id, node->name);
312     } else {
313       printf("%s\n", node->name);
314     }
315   }
316 }
317
318 int compare_nodes(const void *x1, const void *x2) {
319   const struct file_node **f1, **f2;
320
321   f1 = (const struct file_node **) x1;
322   f2 = (const struct file_node **) x2;
323
324   if((*f1)->group_id < (*f2)->group_id) {
325     return -1;
326   } else if((*f1)->group_id > (*f2)->group_id) {
327     return 1;
328   } else {
329     if((*f1)->dir_id < (*f2)->dir_id) {
330       return -1;
331     } else if((*f1)->dir_id > (*f2)->dir_id) {
332       return 1;
333     } else {
334       return 0;
335     }
336   }
337 }
338
339
340 void print_result(struct file_node *list1, struct file_node *list2) {
341   struct file_node *node1, *node2;
342   struct file_node **nodes;
343   int nb, n;
344
345   nb = 0;
346   for(node1 = list1; node1; node1 = node1->next) {
347     if(node1->group_id >= 0) { nb++; }
348   }
349
350   if(list2) {
351     for(node2 = list2; node2; node2 = node2->next) {
352       if(node2->group_id >= 0) { nb++; }
353     }
354   }
355
356   nodes = safe_malloc(nb * sizeof(struct file_node *));
357
358   n = 0;
359   for(node1 = list1; node1; node1 = node1->next) {
360     if(node1->group_id >= 0) {
361       nodes[n++] = node1;
362     }
363   }
364
365   if(list2) {
366     for(node2 = list2; node2; node2 = node2->next) {
367       if(node2->group_id >= 0) {
368         nodes[n++] = node2;
369       }
370     }
371   }
372
373   qsort(nodes, nb, sizeof(struct file_node *), compare_nodes);
374
375   for(n = 0; n < nb; n++) {
376     if(!show_groups && n > 0 && nodes[n]->group_id != nodes[n-1]->group_id) {
377       printf("\n");
378     }
379     print_file(nodes[n]);
380   }
381
382   free(nodes);
383 }
384
385 void print_progress(int max, int n, int *pp) {
386   int p, k;
387   int width, tty_width;
388   struct winsize win;
389
390   if(show_progress &&
391      isatty(STDOUT_FILENO) &&
392      !ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &win)) {
393     tty_width = win.ws_col;
394     width = tty_width - 7;
395     p = (width * n) / (max - 1);
396     if(p > *pp) {
397       for(k = 0; k < p; k++) {
398         fprintf(stderr, "+");
399       }
400       for(; k < width; k++) {
401         fprintf(stderr, "-");
402       }
403       *pp = p;
404       p = (100 * n) / (max - 1);
405       fprintf(stderr, " [%3d%%]\r", p);
406     }
407   }
408 }
409
410 void start(const char *dirname1, const char *dirname2) {
411   struct file_node *list1, *list2;
412   struct file_node *node1, *node2;
413   int not_in, found;
414   int nb_groups, nb_nodes;
415   int list1_length, previous_progress;
416
417   char *buffer1 = safe_malloc(sizeof(char) * READ_BUFFER_SIZE);
418   char *buffer2 = safe_malloc(sizeof(char) * READ_BUFFER_SIZE);
419
420   not_in = 0;
421
422   if(show_progress) {
423     fprintf(stderr, "Scanning %s ... ", dirname1);
424   }
425
426   list1 = scan_directory(0, dirname1);
427
428   if(dirname2) {
429     if(strncmp(dirname2, "not:", 4) == 0) {
430       not_in = 1;
431       /* groups are not computed in the not: mode */
432       show_groups = 0;
433       dirname2 += 4;
434     } else if(strncmp(dirname2, "and:", 4) == 0) {
435       dirname2 += 4;
436     }
437     if(show_progress) {
438       fprintf(stderr, "%s ... ", dirname2);
439     }
440     list2 = scan_directory(0, dirname2);
441   } else {
442     list2 = list1;
443   }
444
445   if(show_progress) {
446     fprintf(stderr, "done.\n");
447   }
448
449   nb_groups = 0;
450   previous_progress = -1;
451   nb_nodes = 0;
452   list1_length = file_list_length(list1);
453
454   if(not_in) {
455     for(node1 = list1; node1; node1 = node1->next) {
456       print_progress(list1_length, nb_nodes, &previous_progress);
457       nb_nodes++;
458
459       found = 0;
460
461       for(node2 = list2; !found && node2; node2 = node2->next) {
462         if(same_files(node1, node2, buffer1, buffer2)) {
463           found = 1;
464         }
465       }
466
467       if(!found) {
468         if(show_realpaths) {
469           printf("%s\n", realpath(node1->name, 0));
470         } else {
471           printf("%s\n", node1->name);
472         }
473       }
474     }
475
476   } else {
477     for(node1 = list1; node1; node1 = node1->next) {
478       print_progress(list1_length, nb_nodes, &previous_progress);
479       nb_nodes++;
480
481       for(node2 = list2; node2; node2 = node2->next) {
482         if(node1->group_id < 0 || node2->group_id < 0) {
483           if(same_files(node1, node2, buffer1, buffer2)) {
484             if(node1->group_id < 0) {
485               if(node2->group_id >= 0) {
486                 node1->group_id = node2->group_id;
487               } else {
488                 node1->group_id = nb_groups;
489                 node1->dir_id = 1;
490                 nb_groups++;
491               }
492             }
493             if(node2->group_id < 0) {
494               node2->group_id = node1->group_id;
495               node2->dir_id = 2;
496             }
497           }
498         }
499       }
500     }
501   }
502
503   if(show_progress) {
504     fprintf(stderr, "\n");
505   }
506
507   if(dirname2) {
508     print_result(list1, list2);
509     file_list_delete(list1);
510     file_list_delete(list2);
511   } else {
512     print_result(list1, 0);
513     file_list_delete(list1);
514   }
515
516   free(buffer1);
517   free(buffer2);
518 }
519
520 void usage(FILE *out) {
521   fprintf(out, "Usage: finddup [OPTION]... DIR1 [[and:|not:]DIR2]\n");
522   fprintf(out, "Version %s (%s)\n", VERSION_NUMBER, UNAME);
523   fprintf(out, "Without DIR2, lists duplicated files found in DIR1. With DIR2, lists files common to both directories. With the not: prefix, lists files found in DIR1 which do not exist in DIR2. The and: prefix is the default and should be used only if you have a directory starting with 'not:'\n");
524   fprintf(out, "\n");
525   /*            01234567890123456789012345678901234567890123456789012345678901234567890123456789*/
526   fprintf(out, "   -h, --help                 show this help\n");
527   fprintf(out, "   -d, --ignore-dots          ignore dot files and directories\n");
528   fprintf(out, "   -0, --ignore-empty         ignore empty files\n");
529   fprintf(out, "   -c, --hide-matchings       do not show which files in DIR2 corresponds to\n");
530   fprintf(out, "                              those in DIR1\n");
531   fprintf(out, "   -g, --no-group-ids         do not show the file groups\n");
532   fprintf(out, "   -p, --show-progress        show progress\n");
533   fprintf(out, "   -r, --real-paths           show the real file paths\n");
534   fprintf(out, "   -i, --same-inodes-are-different\n");
535   fprintf(out, "                              consider files with same inode as different\n");
536 #ifdef WITH_MD5
537   fprintf(out, "   -m, --md5                  use MD5 hashing\n");
538 #endif
539   fprintf(out, "\n");
540   fprintf(out, "Report bugs and comments to <francois@fleuret.org>.\n");
541 }
542
543 /**********************************************************************/
544
545 static struct option long_options[] = {
546   { "help", no_argument, 0, 'h' },
547   { "same-inodes-are-different", no_argument, 0, 'i' },
548   { "real-paths", no_argument, 0, 'r' },
549   { "hide-matchings", no_argument, 0, 'c' },
550   { "no-group-ids", no_argument, 0, 'g' },
551   { "ignore-dots", no_argument, 0, 'd' },
552   { "ignore-empty", no_argument, 0, '0' },
553   { "show-progress", no_argument, 0, 'p' },
554   { "md5", no_argument, 0, 'm' },
555   { 0, 0, 0, 0 }
556 };
557
558 int main(int argc, char **argv) {
559   int c;
560
561   setlocale (LC_ALL, "");
562
563   while ((c = getopt_long(argc, argv, "hircgd0pm",
564                           long_options, NULL)) != -1) {
565     switch (c) {
566
567     case 'h':
568       usage(stdout);
569       exit(EXIT_SUCCESS);
570
571       break;
572
573     case 'd':
574       ignore_dotfiles = 1;
575       break;
576
577     case '0':
578       ignore_empty_files = 1;
579       break;
580
581     case 'r':
582       show_realpaths = 1;
583       break;
584
585     case 'i':
586       same_inodes_are_different = 1;
587       break;
588
589     case 'g':
590       show_groups = 0;
591       break;
592
593     case 'p':
594       show_progress = 1;
595       break;
596
597     case 'c':
598       show_hits = 0;
599       break;
600
601     case 'm':
602 #ifdef WITH_MD5
603       use_md5 = 1;
604 #else
605       fprintf(stderr,
606               "finddup has not been compiled with MD5 hashing.\n");
607       usage(stderr);
608       exit(EXIT_FAILURE);
609 #endif
610       break;
611
612     default:
613       usage(stderr);
614       exit(EXIT_FAILURE);
615     }
616   }
617
618   if(optind + 2 == argc) {
619     start(argv[optind], argv[optind + 1]);
620   } else if(optind + 1 == argc) {
621     same_inodes_are_different = 1;
622     start(argv[optind], 0);
623   } else {
624     usage(stderr);
625     exit(EXIT_FAILURE);
626   }
627
628   exit(EXIT_SUCCESS);
629 }