Prints the number of files when scanning is done.
[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, list2_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   list1_length = file_list_length(list1);
429
430   if(dirname2) {
431     if(strncmp(dirname2, "not:", 4) == 0) {
432       not_in = 1;
433       /* groups are not computed in the not: mode */
434       show_groups = 0;
435       dirname2 += 4;
436     } else if(strncmp(dirname2, "and:", 4) == 0) {
437       dirname2 += 4;
438     }
439     if(show_progress) {
440       fprintf(stderr, "%s ... ", dirname2);
441     }
442     list2 = scan_directory(0, dirname2);
443   } else {
444     list2 = list1;
445   }
446
447   if(show_progress) {
448     fprintf(stderr, "done.\n");
449     fprintf(stderr,
450             "%s: %d file%s.\n",
451             dirname1, list1_length, (list1_length > 1 ? "s" : ""));
452     if(dirname2) {
453       list2_length = file_list_length(list2);
454       fprintf(stderr,
455               "%s: %d file%s.\n",
456               dirname2, list2_length, (list2_length > 1 ? "s" : ""));
457     }
458     fprintf(stderr, "Now looking for identical files.\n");
459   }
460
461   nb_groups = 0;
462   previous_progress = -1;
463   nb_nodes = 0;
464
465   if(not_in) {
466     for(node1 = list1; node1; node1 = node1->next) {
467       print_progress(list1_length, nb_nodes, &previous_progress);
468       nb_nodes++;
469
470       found = 0;
471
472       for(node2 = list2; !found && node2; node2 = node2->next) {
473         if(same_files(node1, node2, buffer1, buffer2)) {
474           found = 1;
475         }
476       }
477
478       if(!found) {
479         if(show_realpaths) {
480           printf("%s\n", realpath(node1->name, 0));
481         } else {
482           printf("%s\n", node1->name);
483         }
484       }
485     }
486
487   } else {
488     for(node1 = list1; node1; node1 = node1->next) {
489       print_progress(list1_length, nb_nodes, &previous_progress);
490       nb_nodes++;
491
492       for(node2 = list2; node2; node2 = node2->next) {
493         if(node1->group_id < 0 || node2->group_id < 0) {
494           if(same_files(node1, node2, buffer1, buffer2)) {
495             if(node1->group_id < 0) {
496               if(node2->group_id >= 0) {
497                 node1->group_id = node2->group_id;
498               } else {
499                 node1->group_id = nb_groups;
500                 node1->dir_id = 1;
501                 nb_groups++;
502               }
503             }
504             if(node2->group_id < 0) {
505               node2->group_id = node1->group_id;
506               node2->dir_id = 2;
507             }
508           }
509         }
510       }
511     }
512   }
513
514   if(show_progress) {
515     fprintf(stderr, "\n");
516   }
517
518   if(dirname2) {
519     print_result(list1, list2);
520     file_list_delete(list1);
521     file_list_delete(list2);
522   } else {
523     print_result(list1, 0);
524     file_list_delete(list1);
525   }
526
527   free(buffer1);
528   free(buffer2);
529 }
530
531 void usage(FILE *out) {
532   fprintf(out, "Usage: finddup [OPTION]... DIR1 [[and:|not:]DIR2]\n");
533   fprintf(out, "Version %s (%s)\n", VERSION_NUMBER, UNAME);
534   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");
535   fprintf(out, "\n");
536   /*            01234567890123456789012345678901234567890123456789012345678901234567890123456789*/
537   fprintf(out, "   -h, --help                 show this help\n");
538   fprintf(out, "   -d, --ignore-dots          ignore dot files and directories\n");
539   fprintf(out, "   -0, --ignore-empty         ignore empty files\n");
540   fprintf(out, "   -c, --hide-matchings       do not show which files in DIR2 corresponds to\n");
541   fprintf(out, "                              those in DIR1\n");
542   fprintf(out, "   -g, --no-group-ids         do not show the file groups\n");
543   fprintf(out, "   -p, --show-progress        show progress\n");
544   fprintf(out, "   -r, --real-paths           show the real file paths\n");
545   fprintf(out, "   -i, --same-inodes-are-different\n");
546   fprintf(out, "                              consider files with same inode as different\n");
547 #ifdef WITH_MD5
548   fprintf(out, "   -m, --md5                  use MD5 hashing\n");
549 #endif
550   fprintf(out, "\n");
551   fprintf(out, "Report bugs and comments to <francois@fleuret.org>.\n");
552 }
553
554 /**********************************************************************/
555
556 static struct option long_options[] = {
557   { "help", no_argument, 0, 'h' },
558   { "same-inodes-are-different", no_argument, 0, 'i' },
559   { "real-paths", no_argument, 0, 'r' },
560   { "hide-matchings", no_argument, 0, 'c' },
561   { "no-group-ids", no_argument, 0, 'g' },
562   { "ignore-dots", no_argument, 0, 'd' },
563   { "ignore-empty", no_argument, 0, '0' },
564   { "show-progress", no_argument, 0, 'p' },
565   { "md5", no_argument, 0, 'm' },
566   { 0, 0, 0, 0 }
567 };
568
569 int main(int argc, char **argv) {
570   int c;
571
572   setlocale (LC_ALL, "");
573
574   while ((c = getopt_long(argc, argv, "hircgd0pm",
575                           long_options, NULL)) != -1) {
576     switch (c) {
577
578     case 'h':
579       usage(stdout);
580       exit(EXIT_SUCCESS);
581
582       break;
583
584     case 'd':
585       ignore_dotfiles = 1;
586       break;
587
588     case '0':
589       ignore_empty_files = 1;
590       break;
591
592     case 'r':
593       show_realpaths = 1;
594       break;
595
596     case 'i':
597       same_inodes_are_different = 1;
598       break;
599
600     case 'g':
601       show_groups = 0;
602       break;
603
604     case 'p':
605       show_progress = 1;
606       break;
607
608     case 'c':
609       show_hits = 0;
610       break;
611
612     case 'm':
613 #ifdef WITH_MD5
614       use_md5 = 1;
615 #else
616       fprintf(stderr,
617               "finddup has not been compiled with MD5 hashing.\n");
618       usage(stderr);
619       exit(EXIT_FAILURE);
620 #endif
621       break;
622
623     default:
624       usage(stderr);
625       exit(EXIT_FAILURE);
626     }
627   }
628
629   if(optind + 2 == argc) {
630     start(argv[optind], argv[optind + 1]);
631   } else if(optind + 1 == argc) {
632     same_inodes_are_different = 1;
633     start(argv[optind], 0);
634   } else {
635     usage(stderr);
636     exit(EXIT_FAILURE);
637   }
638
639   exit(EXIT_SUCCESS);
640 }