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