]> git.decadent.org.uk Git - dak.git/blob - tools/dsync-0.0/cmdline/dsync-flist.cc
LOCAL: Remove replay check
[dak.git] / tools / dsync-0.0 / cmdline / dsync-flist.cc
1 // -*- mode: cpp; mode: fold -*-
2 // Description                                                          /*{{{*/
3 // $Id: dsync-flist.cc,v 1.27 1999/12/26 06:59:00 jgg Exp $
4 /* ######################################################################
5
6    Dsync FileList is a tool to manipulate and generate the dsync file 
7    listing
8    
9    Several usefull functions are provided, the most notable is to generate
10    the file list and to dump it. There is also a function to compare the
11    file list against a local directory tree.
12    
13    ##################################################################### */
14                                                                         /*}}}*/
15 // Include files                                                        /*{{{*/
16 #ifdef __GNUG__
17 #pragma implementation "dsync-flist.h"
18 #endif 
19
20 #include "dsync-flist.h"
21 #include <dsync/cmndline.h>
22 #include <dsync/error.h>
23 #include <dsync/md5.h>
24 #include <dsync/strutl.h>
25
26 #include <config.h>
27 #include <stdio.h>
28 #include <sys/types.h>
29 #include <sys/stat.h>
30 #include <sys/ioctl.h>
31 #include <utime.h>
32 #include <unistd.h>
33 #include <termios.h>
34 #include <signal.h>
35
36 #include <iostream>
37 using namespace std;
38
39                                                                         /*}}}*/
40
41 // Externs                                                              /*{{{*/
42 ostream c0out(cout.rdbuf());
43 ostream c1out(cout.rdbuf());
44 ostream c2out(cout.rdbuf());
45 ofstream devnull("/dev/null");
46 unsigned int ScreenWidth = 80;
47                                                                         /*}}}*/
48
49 // Progress::Progress - Constructor                                     /*{{{*/
50 // ---------------------------------------------------------------------
51 /* */
52 Progress::Progress()
53 {
54    Quiet = false;
55    if (_config->FindI("quiet",0) > 0)
56       Quiet = true;
57    DirCount = 0;
58    FileCount = 0;
59    LinkCount = 0;
60    Bytes = 0;
61    CkSumBytes = 0;
62    gettimeofday(&StartTime,0);
63 }
64                                                                         /*}}}*/
65 // Progress::Done - Clear the progress meter                            /*{{{*/
66 // ---------------------------------------------------------------------
67 /* */
68 void Progress::Done()
69 {
70    if (Quiet == false)
71       c0out << '\r' << BlankLine << '\r' << flush;
72    BlankLine[0] = 0;
73 }
74                                                                         /*}}}*/
75 // Progress::ElaspedTime - Return the time that has elapsed             /*{{{*/
76 // ---------------------------------------------------------------------
77 /* Computes the time difference with maximum accuracy */
78 double Progress::ElapsedTime()
79 {
80    // Compute the CPS and elapsed time
81    struct timeval Now;
82    gettimeofday(&Now,0);
83
84    return Now.tv_sec - StartTime.tv_sec + (Now.tv_usec - 
85                                            StartTime.tv_usec)/1000000.0;
86 }
87                                                                         /*}}}*/
88 // Progress::Update - Update the meter                                  /*{{{*/
89 // ---------------------------------------------------------------------
90 /* */
91 void Progress::Update(const char *Directory)
92 {
93    LastCount = DirCount+LinkCount+FileCount;
94    
95    if (Quiet == true)
96       return;
97
98    // Put the number of files and bytes at the end of the meter
99    char S[1024];
100    if (ScreenWidth > sizeof(S)-1)
101       ScreenWidth = sizeof(S)-1;
102    
103    unsigned int Len = snprintf(S,sizeof(S),"|%lu %sb",
104                                DirCount+LinkCount+FileCount,
105                                SizeToStr(Bytes).c_str());
106    
107    memmove(S + (ScreenWidth - Len),S,Len+1);
108    memset(S,' ',ScreenWidth - Len);
109    
110    // Put the directory name at the front, possibly shortened
111    if (Directory == 0 || Directory[0] == 0)
112       S[snprintf(S,sizeof(S),"<root>")] = ' ';
113    else
114    {
115       // If the path is too long fix it and prefix it with '...'
116       if (strlen(Directory) >= ScreenWidth - Len - 1)
117       {
118          S[snprintf(S,sizeof(S),"%s",Directory + 
119                     strlen(Directory) - ScreenWidth + Len + 1)] = ' ';
120          S[0] = '.'; S[1] = '.'; S[2] = '.';
121       }
122       else
123          S[snprintf(S,sizeof(S),"%s",Directory)] = ' ';
124    }
125    
126    strcpy(LastLine,S);
127    c0out << S << '\r' << flush;
128    memset(BlankLine,' ',strlen(S));
129    BlankLine[strlen(S)] = 0;
130 }
131                                                                         /*}}}*/
132 // Progress::Stats - Show a statistics report                           /*{{{*/
133 // ---------------------------------------------------------------------
134 /* */
135 void Progress::Stats(bool CkSum)
136 {
137    // Display some interesting statistics
138    double Elapsed = ElapsedTime();
139    c1out << DirCount << " directories, " << FileCount <<
140       " files and " << LinkCount << " links (" << 
141       (DirCount+FileCount+LinkCount) << "). ";
142    if (CkSum == true)
143    {
144       if (CkSumBytes == Bytes)
145          c1out << "Total Size is " << SizeToStr(Bytes) << "b. ";
146       else
147          c1out << SizeToStr(CkSumBytes) << '/' <<
148            SizeToStr(Bytes) << "b hashed.";
149    }   
150    else
151       c1out << "Total Size is " << SizeToStr(Bytes) << "b. ";
152       
153    c1out << endl;
154    c1out << "Elapsed time " <<  TimeToStr((long)Elapsed) <<
155       " (" << SizeToStr((DirCount+FileCount+LinkCount)/Elapsed) <<
156       " files/sec) ";
157    if (CkSumBytes != 0)
158       c1out << " (" << SizeToStr(CkSumBytes/Elapsed) << "b/s hash)";
159    c1out << endl;
160 }
161                                                                         /*}}}*/
162
163 // ListGenerator::ListGenerator - Constructor                           /*{{{*/
164 // ---------------------------------------------------------------------
165 /* */
166 ListGenerator::ListGenerator()
167 {
168    Act = !_config->FindB("noact",false);
169    StripDepth = _config->FindI("FileList::CkSum-PathStrip",0);
170    Verbose = false;
171    if (_config->FindI("verbose",0) > 0)
172       Verbose = true;
173    DB = 0;
174    DBIO = 0;
175
176    // Set RSync checksum limits
177    MinRSyncSize = _config->FindI("FileList::MinRSyncSize",0);
178    if (MinRSyncSize == 0)
179       MinRSyncSize = 1;
180    if (_config->FindB("FileList::RSync-Hashes",false) == false)
181        MinRSyncSize = 0;
182        
183    // Load the rsync filter
184    if (RSyncFilter.LoadFilter(_config->Tree("FList::RSync-Filter")) == false)
185       return;
186        
187    // Load the clean filter
188    if (RemoveFilter.LoadFilter(_config->Tree("FList::Clean-Filter")) == false)
189       return;
190 }
191                                                                         /*}}}*/
192 // ListGenerator::~ListGenerator - Destructor                           /*{{{*/
193 // ---------------------------------------------------------------------
194 /* */
195 ListGenerator::~ListGenerator()
196 {
197    delete DB;
198    delete DBIO;
199 }
200                                                                         /*}}}*/
201 // ListGenerator::Visit - Collect statistics about the tree             /*{{{*/
202 // ---------------------------------------------------------------------
203 /* */
204 int ListGenerator::Visit(const char *Directory,const char *File,
205                          struct stat const &Stat)
206 {
207    if (Prog.DirCount+Prog.LinkCount+Prog.FileCount - Prog.LastCount > 100 ||
208        File == 0)
209       Prog.Update(Directory);
210    
211    // Ignore directory enters
212    if (File == 0)
213       return 0;
214    
215    // Increment our counters
216    if (S_ISDIR(Stat.st_mode) != 0)
217       Prog.DirCount++;
218    else
219    {
220       if (S_ISLNK(Stat.st_mode) != 0)
221          Prog.LinkCount++;
222       else
223          Prog.FileCount++;
224    }
225    
226    // Normal file
227    if (S_ISREG(Stat.st_mode) != 0)
228       Prog.Bytes += Stat.st_size;
229    
230    // Look for files to erase
231    if (S_ISDIR(Stat.st_mode) == 0 &&
232        RemoveFilter.Test(Directory,File) == false)
233    {
234       Prog.Hide();
235       c1out << "Unlinking " << Directory << File << endl;
236       Prog.Show();
237       
238       if (Act == true && unlink(File) != 0)
239       {
240          _error->Errno("unlink","Failed to remove %s%s",Directory,File);
241          return -1;
242       }
243       
244       return 1;
245    }
246    
247    return 0;
248 }                        
249                                                                         /*}}}*/
250 // ListGenerator::EmitMD5 - Perform md5 lookup caching                  /*{{{*/
251 // ---------------------------------------------------------------------
252 /* This looks up the file in the cache to see if it is one we already 
253    know the hash too */
254 bool ListGenerator::EmitMD5(const char *Dir,const char *File,
255                             struct stat const &St,unsigned char MD5[16],
256                             unsigned int Tag,unsigned int Flag)
257 {
258    if ((IO->Header.Flags[Tag] & Flag) != Flag)
259       return true;
260
261    // Lookup the md5 in the old file list
262    if (DB != 0 && (DBIO->Header.Flags[Tag] & Flag) == Flag)
263    {
264       // Do a lookup and make sure the timestamps match
265       dsFList List;
266       bool Hit = false;
267       const char *iDir = Dir;
268       unsigned int Strip = StripDepth;
269       while (true)
270       {  
271          if (DB->Lookup(*DBIO,iDir,File,List) == true && List.Entity != 0)
272          {
273             if ((signed)(List.Entity->ModTime + List.Head.Epoch) == St.st_mtime)
274                Hit = true;          
275             break;
276          }
277          
278          if (Strip == 0)
279             break;
280          
281          Strip--;
282          for (; *iDir != 0 && *iDir != '/'; iDir++);
283          if (*iDir == 0 || iDir[1] == 0)
284             break;
285          iDir++;
286       }
287          
288       if (Hit == true)
289       {
290          /* Both hardlinks and normal files have md5s, also check that the
291             sizes match */
292          if (List.File != 0 && List.File->Size == (unsigned)St.st_size)
293          {
294             memcpy(MD5,List.File->MD5,sizeof(List.File->MD5));
295             return true;
296          }
297       }      
298    }
299    
300    Prog.CkSumBytes += St.st_size;
301    
302    if (Verbose == true)
303    {
304       Prog.Hide();
305       c1out << "MD5 " << Dir << File << endl;
306       Prog.Show();
307    }
308    
309    return dsGenFileList::EmitMD5(Dir,File,St,MD5,Tag,Flag);
310 }
311                                                                         /*}}}*/
312 // ListGenerator::NeedsRSync - Check if a file is rsyncable             /*{{{*/
313 // ---------------------------------------------------------------------
314 /* This checks the rsync filter list and the rsync size limit*/
315 bool ListGenerator::NeedsRSync(const char *Dir,const char *File,
316                                dsFList::NormalFile &F)
317 {
318    if (MinRSyncSize == 0)
319       return false;
320    
321    if (F.Size <= MinRSyncSize)
322       return false;
323    
324    if (RSyncFilter.Test(Dir,File) == false)
325       return false;
326    
327    /* Add it to the counters, EmitMD5 will not be called if rsync checksums
328       are being built. */
329    Prog.CkSumBytes += F.Size;  
330    if (Verbose == true)
331    {
332       Prog.Hide();
333       c1out << "RSYNC " << Dir << File << endl;
334       Prog.Show();
335    }
336    
337    return true;
338 }
339                                                                         /*}}}*/
340
341 // Compare::Compare - Constructor                                       /*{{{*/
342 // ---------------------------------------------------------------------
343 /* */
344 Compare::Compare()
345 {
346    Verbose = false;
347    if (_config->FindI("verbose",0) > 0)
348       Verbose = true;
349    Act = !_config->FindB("noact",false);
350    DoDelete = _config->FindB("delete",false);
351 }
352                                                                         /*}}}*/
353 // Compare::Visit - Collect statistics about the tree                   /*{{{*/
354 // ---------------------------------------------------------------------
355 /* */
356 bool Compare::Visit(dsFList &List,string Dir)
357 {
358    if (Prog.DirCount+Prog.LinkCount+Prog.FileCount - Prog.LastCount > 100 ||
359        List.Tag == dsFList::tDirStart)
360       Prog.Update(Dir.c_str());
361    
362    // Increment our counters
363    if (List.Tag == dsFList::tDirectory)
364       Prog.DirCount++;
365    else
366    {
367       if (List.Tag == dsFList::tSymlink)
368          Prog.LinkCount++;
369
370       if (List.Tag == dsFList::tNormalFile || 
371           List.Tag == dsFList::tHardLink ||
372           List.Tag == dsFList::tDeviceSpecial)
373          Prog.FileCount++;
374    }
375    
376    // Normal file
377    if (List.File != 0)
378       Prog.Bytes += List.File->Size;
379    
380    return true;
381 }
382                                                                         /*}}}*/
383 // Compare::PrintPath - Print out a path string                         /*{{{*/
384 // ---------------------------------------------------------------------
385 /* This handles the absolute paths that can occure while processing */
386 void Compare::PrintPath(ostream &out,string Dir,string Name)
387 {
388    if (Name[0] != '/')
389       out << Dir << Name << endl;
390    else
391       out << string(Name,Base.length()) << endl;
392 }
393                                                                         /*}}}*/
394
395 // LookupPath - Find a full path within the database                    /*{{{*/
396 // ---------------------------------------------------------------------
397 /* This does the necessary path simplification and symlink resolution
398    to locate the path safely. The file must exist locally inorder to 
399    resolve the local symlinks. */
400 bool LookupPath(const char *Path,dsFList &List,dsFileListDB &DB,
401                 dsFList::IO &IO)
402 {
403    char Buffer[2024];
404    strcpy(Buffer,Path);
405       
406    if (SimplifyPath(Buffer) == false || 
407        ResolveLink(Buffer,sizeof(Buffer)) == false)
408       return false;
409    
410    // Strip off the final component name
411    char *I = Buffer + strlen(Buffer);
412    for (; I != Buffer && (*I == '/' || *I == 0); I--);
413    for (; I != Buffer && *I != '/'; I--);
414    if (I != Buffer)
415    {
416       memmove(I+1,I,strlen(I) + 1);
417       I++;
418       *I = 0;
419       I++;
420       if (DB.Lookup(IO,Buffer,I,List) == false)
421          return false;
422    }
423    else
424    {
425       if (DB.Lookup(IO,"",I,List) == false)
426          return false;
427    }
428    
429    return true;
430 }
431                                                                         /*}}}*/
432 // PrintMD5 - Prints the MD5 of a file in the form similar to md5sum    /*{{{*/
433 // ---------------------------------------------------------------------
434 /* */
435 void PrintMD5(dsFList &List,const char *Dir,const char *File = 0)
436 {
437    if (List.File == 0 || 
438        List.Head.Flags[List.Tag] & dsFList::NormalFile::FlMD5 == 0)
439       return;
440
441    char S[16*2+1];
442    for (unsigned int I = 0; I != 16; I++)
443       sprintf(S+2*I,"%02x",List.File->MD5[I]);
444    S[16*2] = 0;
445    if (File == 0)
446       cout << S << "  " << Dir << List.File->Name << endl;
447    else
448       cout << S << "  " << File << endl;
449 }
450                                                                         /*}}}*/
451
452 // DoGenerate - The Generate Command                                    /*{{{*/
453 // ---------------------------------------------------------------------
454 /* */
455 bool DoGenerate(CommandLine &CmdL)
456 {
457    ListGenerator Gen;
458    if (_error->PendingError() == true)
459       return false;
460    
461    // Load the filter list
462    if (Gen.Filter.LoadFilter(_config->Tree("FileList::Filter")) == false)
463       return false;
464
465    // Load the delay filter list
466    if (Gen.PreferFilter.LoadFilter(_config->Tree("FileList::Prefer-Filter")) == false)
467       return false;
468    
469    // Determine the ordering to use
470    string Ord = _config->Find("FileList::Order","tree");
471    if (stringcasecmp(Ord,"tree") == 0)
472       Gen.Type = dsGenFileList::Tree;
473    else
474    {
475       if (stringcasecmp(Ord,"breadth") == 0)
476          Gen.Type = dsGenFileList::Breadth;
477       else
478       {
479          if (stringcasecmp(Ord,"depth") == 0)
480             Gen.Type = dsGenFileList::Depth;
481          else
482             return _error->Error("Invalid ordering %s, must be tree, breadth or detph",Ord.c_str());
483       }      
484    }
485
486    if (CmdL.FileList[1] == 0)
487       return _error->Error("You must specify a file name");
488    
489    string List = CmdL.FileList[1];
490    
491    // Open the original file to pull cached Check Sums out of
492    if (FileExists(List) == true && 
493        _config->FindB("FileList::MD5-Hashes",false) == true)
494    {
495       Gen.DBIO = new dsMMapIO(List);
496       if (_error->PendingError() == true)
497          return false;
498       Gen.DB = new dsFileListDB;
499       if (Gen.DB->Generate(*Gen.DBIO) == false)
500          return false;
501    }   
502
503    // Sub scope to close the file
504    {      
505       FdIO IO(List + ".new",FileFd::WriteEmpty);
506       
507       // Set the flags for the list
508       if (_config->FindB("FileList::MD5-Hashes",false) == true)
509       {
510          IO.Header.Flags[dsFList::tNormalFile] |= dsFList::NormalFile::FlMD5;
511          IO.Header.Flags[dsFList::tHardLink] |= dsFList::HardLink::FlMD5;
512       }
513       if (_config->FindB("FileList::Permissions",false) == true)
514       {
515          IO.Header.Flags[dsFList::tDirectory] |= dsFList::Directory::FlPerm;
516          IO.Header.Flags[dsFList::tNormalFile] |= dsFList::NormalFile::FlPerm;
517          IO.Header.Flags[dsFList::tHardLink] |= dsFList::HardLink::FlPerm;
518       }
519       if (_config->FindB("FileList::Ownership",false) == true)
520       {
521          IO.Header.Flags[dsFList::tDirectory] |= dsFList::Directory::FlOwner;
522          IO.Header.Flags[dsFList::tNormalFile] |= dsFList::NormalFile::FlOwner;
523          IO.Header.Flags[dsFList::tSymlink] |= dsFList::Symlink::FlOwner;
524          IO.Header.Flags[dsFList::tDeviceSpecial] |= dsFList::DeviceSpecial::FlOwner;
525          IO.Header.Flags[dsFList::tHardLink] |= dsFList::HardLink::FlOwner;
526       }
527       
528       if (Gen.Go("./",IO) == false)
529          return false;
530       Gen.Prog.Done();
531       Gen.Prog.Stats(_config->FindB("FileList::MD5-Hashes",false));
532       
533       delete Gen.DB;
534       Gen.DB = 0;
535       delete Gen.DBIO;
536       Gen.DBIO = 0;
537    }
538    
539    // Just in case :>
540    if (_error->PendingError() == true)
541       return false;
542    
543    // Swap files
544    bool OldExists = FileExists(List);
545    if (OldExists == true && rename(List.c_str(),(List + "~").c_str()) != 0)
546       return _error->Errno("rename","Unable to rename %s to %s~",List.c_str(),List.c_str());
547    if (rename((List + ".new").c_str(),List.c_str()) != 0)
548       return _error->Errno("rename","Unable to rename %s.new to %s",List.c_str(),List.c_str());
549    if (OldExists == true && unlink((List + "~").c_str()) != 0)
550       return _error->Errno("unlink","Unable to unlink %s~",List.c_str());
551    
552    return true;
553 }
554                                                                         /*}}}*/
555 // DoDump - Dump the contents of a file list                            /*{{{*/
556 // ---------------------------------------------------------------------
557 /* This displays a short one line dump of each record in the file */
558 bool DoDump(CommandLine &CmdL)
559 {
560    if (CmdL.FileList[1] == 0)
561       return _error->Error("You must specify a file name");
562    
563    // Open the file
564    dsMMapIO IO(CmdL.FileList[1]);
565    if (_error->PendingError() == true)
566       return false;
567    
568    dsFList List;
569    unsigned long CountDir = 0;
570    unsigned long CountFile = 0;
571    unsigned long CountLink = 0;
572    unsigned long CountLinkReal = 0;
573    unsigned long NumFiles = 0;
574    unsigned long NumDirs = 0;
575    unsigned long NumLinks = 0;
576    double Bytes = 0;
577    
578    while (List.Step(IO) == true)
579    {
580       if (List.Print(cout) == false)
581          return false;
582
583       switch (List.Tag)
584       {
585          case dsFList::tDirMarker:
586          case dsFList::tDirStart:
587          case dsFList::tDirectory:
588          {
589             CountDir += List.Dir.Name.length();
590             if (List.Tag == dsFList::tDirectory)
591                NumDirs++;
592             break;
593          }
594
595          case dsFList::tHardLink:
596          case dsFList::tNormalFile:
597          {
598             CountFile += List.File->Name.length();
599             NumFiles++;
600             Bytes += List.File->Size;
601             break;
602          }
603          
604          case dsFList::tSymlink:
605          {
606             CountFile += List.SLink.Name.length();
607             CountLink += List.SLink.To.length();
608             
609             unsigned int Tmp = List.SLink.To.length();
610             if ((List.SLink.Compress & (1<<7)) == (1<<7))
611                Tmp -= List.SLink.Name.length();
612             Tmp -= List.SLink.Compress & 0x7F;
613             CountLinkReal += Tmp;
614             NumLinks++;
615             break;
616          }
617       }
618       if (List.Tag == dsFList::tTrailer)
619          break;
620    }
621    cout << "String Sizes: Dirs=" << CountDir << " Files=" << CountFile << 
622       " Links=" << CountLink << " (" << CountLinkReal << ")";
623    cout << " Total=" << CountDir+CountFile+CountLink << endl;
624    cout << "Entries: Dirs=" << NumDirs << " Files=" << NumFiles << 
625       " Links=" << NumLinks << " Total=" << NumDirs+NumFiles+NumLinks << endl;
626    cout << "Totals " << SizeToStr(Bytes) << "b." << endl;
627    
628    return true;
629 }
630                                                                         /*}}}*/
631 // DoMkHardLinks - Generate hardlinks for duplicated files              /*{{{*/
632 // ---------------------------------------------------------------------
633 /* This scans the archive for any duplicated files, it uses the MD5 of each
634    file and searches a map for another match then links the two */
635 struct Md5Cmp
636 {
637    unsigned char MD5[16];
638    int operator <(const Md5Cmp &rhs) const {return memcmp(MD5,rhs.MD5,sizeof(MD5)) < 0;};
639    int operator <=(const Md5Cmp &rhs) const {return memcmp(MD5,rhs.MD5,sizeof(MD5)) <= 0;};
640    int operator >=(const Md5Cmp &rhs) const {return memcmp(MD5,rhs.MD5,sizeof(MD5)) >= 0;};
641    int operator >(const Md5Cmp &rhs) const {return memcmp(MD5,rhs.MD5,sizeof(MD5)) > 0;};
642    int operator ==(const Md5Cmp &rhs) const {return memcmp(MD5,rhs.MD5,sizeof(MD5)) == 0;};
643    
644    Md5Cmp(unsigned char Md[16]) {memcpy(MD5,Md,sizeof(MD5));};
645 };
646
647 struct Location
648 {
649    string Dir;
650    string File;
651    
652    Location() {};
653    Location(string Dir,string File) : Dir(Dir), File(File) {};
654 };
655
656 bool DoMkHardLinks(CommandLine &CmdL)
657 {
658    if (CmdL.FileList[1] == 0)
659       return _error->Error("You must specify a file name");
660    
661    // Open the file
662    dsMMapIO IO(CmdL.FileList[1]);
663    if (_error->PendingError() == true)
664       return false;
665
666    dsFList List;
667    if (List.Step(IO) == false || List.Tag != dsFList::tHeader)
668       return _error->Error("Unable to read header");
669
670    // Make sure we have hashes
671    if ((IO.Header.Flags[dsFList::tNormalFile] & 
672         dsFList::NormalFile::FlMD5) == 0 ||
673        (IO.Header.Flags[dsFList::tHardLink] & 
674         dsFList::HardLink::FlMD5) == 0)
675       return _error->Error("The file list must contain MD5 hashes");
676    
677    string LastDir;
678    double Savings = 0;
679    unsigned long Hits = 0;
680    bool Act = !_config->FindB("noact",false);   
681    map<Md5Cmp,Location> Map;   
682    while (List.Step(IO) == true)
683    {
684       // Entering a new directory, just store it..
685       if (List.Tag == dsFList::tDirStart)
686       {
687          LastDir = List.Dir.Name;
688          continue;
689       }
690
691       /* Handle normal file entities. Pre-existing hard links we treat
692          exactly like a normal file, if two hard link chains are identical
693          one will be destroyed and its items placed on the other 
694          automatcially */
695       if (List.File != 0)
696       {
697          map<Md5Cmp,Location>::const_iterator I = Map.find(Md5Cmp(List.File->MD5));
698          if (I == Map.end())
699          {
700             Map[Md5Cmp(List.File->MD5)] = Location(LastDir,List.File->Name);
701             continue;
702          }
703
704          // Compute full file names for both
705          string FileA = (*I).second.Dir + (*I).second.File;
706          struct stat StA;
707          string FileB = LastDir + List.File->Name;
708          struct stat StB;
709          
710          // Stat them
711          if (lstat(FileA.c_str(),&StA) != 0)
712          {
713             _error->Warning("Unable to stat %s",FileA.c_str());
714             continue;
715          }       
716          if (lstat(FileB.c_str(),&StB) != 0)
717          {
718             _error->Warning("Unable to stat %s",FileB.c_str());
719             continue;
720          }
721          
722          // Verify they are on the same filesystem
723          if (StA.st_dev != StB.st_dev || StA.st_size != StB.st_size)
724             continue;
725          
726          // And not merged..
727          if (StA.st_ino == StB.st_ino)
728             continue;
729          
730          c1out << "Dup " << FileA << endl;
731          c1out << "    " << FileB << endl;
732       
733          // Relink the file and copy the mod time from the oldest one.
734          if (Act == true)
735          {
736             if (unlink(FileB.c_str()) != 0)
737                return _error->Errno("unlink","Failed to unlink %s",FileB.c_str());
738             if (link(FileA.c_str(),FileB.c_str()) != 0)
739                return _error->Errno("link","Failed to link %s to %s",FileA.c_str(),FileB.c_str());
740             if (StB.st_mtime > StA.st_mtime)
741             {
742                struct utimbuf Time;
743                Time.actime = Time.modtime = StB.st_mtime;
744                if (utime(FileB.c_str(),&Time) != 0)
745                   _error->Warning("Unable to set mod time for %s",FileB.c_str());              
746             }
747          }
748          
749          // Counters
750          Savings += List.File->Size;
751          Hits++;
752          
753          continue;
754       }
755       
756       if (List.Tag == dsFList::tTrailer)
757          break;
758    }
759    
760    cout << "Total space saved by merging " << 
761       SizeToStr(Savings) << "b. " << Hits << " files affected." << endl;
762    return true;
763 }
764                                                                         /*}}}*/
765 // DoLookup - Lookup a single file in the listing                       /*{{{*/
766 // ---------------------------------------------------------------------
767 /* */
768 bool DoLookup(CommandLine &CmdL)
769 {
770    if (CmdL.FileSize() < 4)
771       return _error->Error("You must specify a file name, directory name and a entry");
772    
773    // Open the file
774    dsMMapIO IO(CmdL.FileList[1]);
775    if (_error->PendingError() == true)
776       return false;
777
778    // Index it
779    dsFileListDB DB;
780    if (DB.Generate(IO) == false)
781       return false;
782
783    dsFList List;
784    if (DB.Lookup(IO,CmdL.FileList[2],CmdL.FileList[3],List) == false)
785       return _error->Error("Unable to locate item");
786    List.Print(cout);
787    return true;
788 }
789                                                                         /*}}}*/
790 // DoMD5Cache - Lookup a stream of files in the listing                 /*{{{*/
791 // ---------------------------------------------------------------------
792 /* This takes a list of files names and prints out their MD5s, if possible
793    data is used from the cache to save IO */
794 bool DoMD5Cache(CommandLine &CmdL)
795 {
796    struct timeval Start;
797    gettimeofday(&Start,0);
798    
799    if (CmdL.FileList[1] == 0)
800       return _error->Error("You must specify a file name");
801    
802    // Open the file
803    dsMMapIO IO(CmdL.FileList[1]);
804    if (_error->PendingError() == true)
805       return false;
806
807    dsFList List;
808    if (List.Step(IO) == false || List.Tag != dsFList::tHeader)
809       return _error->Error("Unable to read header");
810    
811    // Make sure we have hashes
812    if ((IO.Header.Flags[dsFList::tNormalFile] & 
813         dsFList::NormalFile::FlMD5) == 0 ||
814        (IO.Header.Flags[dsFList::tHardLink] & 
815         dsFList::HardLink::FlMD5) == 0)
816       return _error->Error("The file list must contain MD5 hashes");
817
818    // Index it
819    dsFileListDB DB;
820    if (DB.Generate(IO) == false)
821       return false;
822
823    // Counters
824    double Bytes = 0;
825    double MD5Bytes = 0;
826    unsigned long Files = 0;
827    unsigned long Errors = 0;
828
829    while (!cin == false)
830    {
831       char Buf2[200];
832       cin.getline(Buf2,sizeof(Buf2));
833       if (Buf2[0] == 0)
834          continue;
835       Files++;
836       
837       // Stat the file
838       struct stat St;
839       if (stat(Buf2,&St) != 0)
840       {
841          cout << "<ERROR> " << Buf2 << "(stat)" << endl;
842          Errors++;
843          continue;
844       }
845             
846       // Lookup in the cache and make sure the file has not changed
847       if (LookupPath(Buf2,List,DB,IO) == false ||
848           (signed)(List.Entity->ModTime + List.Head.Epoch) != St.st_mtime ||
849           (List.File != 0 && List.File->Size != (unsigned)St.st_size))
850       {
851          _error->DumpErrors();
852          
853          // Open the file and hash it
854          MD5Summation Sum;
855          FileFd Fd(Buf2,FileFd::ReadOnly);
856          if (_error->PendingError() == true)
857          {
858             cout << "<ERROR> " << Buf2 << "(open)" << endl;
859             continue;
860          }
861          
862          if (Sum.AddFD(Fd.Fd(),Fd.Size()) == false)
863          {
864             cout << "<ERROR> " << Buf2 << "(md5)" << endl;
865             continue;
866          }
867                  
868          // Store the new hash
869          List.Tag = dsFList::tNormalFile;
870          Sum.Result().Value(List.File->MD5);
871          List.File->Size = (unsigned)St.st_size;
872          
873          MD5Bytes += List.File->Size;
874       }
875
876       PrintMD5(List,0,Buf2);
877       Bytes += List.File->Size;
878    }
879
880    // Print out a summary
881    struct timeval Now;
882    gettimeofday(&Now,0);
883    double Delta = Now.tv_sec - Start.tv_sec + (Now.tv_usec - Start.tv_usec)/1000000.0;
884    cerr << Files << " files, " << SizeToStr(MD5Bytes) << "/" << 
885       SizeToStr(Bytes) << " MD5'd, " << TimeToStr((unsigned)Delta) << endl;;
886       
887    return true;
888 }
889                                                                         /*}}}*/
890 // DoMD5Dump - Dump the md5 list                                        /*{{{*/
891 // ---------------------------------------------------------------------
892 /* This displays a short one line dump of each record in the file */
893 bool DoMD5Dump(CommandLine &CmdL)
894 {
895    if (CmdL.FileList[1] == 0)
896       return _error->Error("You must specify a file name");
897    
898    // Open the file
899    dsMMapIO IO(CmdL.FileList[1]);
900    if (_error->PendingError() == true)
901       return false;
902    
903    dsFList List;
904    if (List.Step(IO) == false || List.Tag != dsFList::tHeader)
905       return _error->Error("Unable to read header");
906    
907    // Make sure we have hashes
908    if ((IO.Header.Flags[dsFList::tNormalFile] & 
909         dsFList::NormalFile::FlMD5) == 0 ||
910        (IO.Header.Flags[dsFList::tHardLink] & 
911         dsFList::HardLink::FlMD5) == 0)
912       return _error->Error("The file list must contain MD5 hashes");
913    
914    string Dir;
915    while (List.Step(IO) == true)
916    {
917       if (List.Tag == dsFList::tDirStart)
918       {
919          Dir = List.Dir.Name;
920          continue;
921       }
922       
923       PrintMD5(List,Dir.c_str());
924       
925       if (List.Tag == dsFList::tTrailer)
926          break;
927    }   
928    return true;
929 }
930                                                                         /*}}}*/
931 // DoVerify - Verify the local tree against a file list                 /*{{{*/
932 // ---------------------------------------------------------------------
933 /* */
934 bool DoVerify(CommandLine &CmdL)
935 {
936    if (CmdL.FileList[1] == 0)
937       return _error->Error("You must specify a file name");
938    
939    // Open the file
940    dsMMapIO IO(CmdL.FileList[1]);
941    if (_error->PendingError() == true)
942       return false;
943    
944    /* Set the hashing type, we can either do a full verify or only a date
945       check verify */
946    Compare Comp;
947    if (_config->FindB("FileList::MD5-Hashes",false) == true)
948       Comp.HashLevel = dsDirCompare::Md5Always;
949    else
950       Comp.HashLevel = dsDirCompare::Md5Date;
951    
952    // Scan the file list
953    if (Comp.Process(".",IO) == false)
954       return false;
955    Comp.Prog.Done();
956    
957    // Report stats
958    Comp.Prog.Stats((IO.Header.Flags[dsFList::tNormalFile] & dsFList::NormalFile::FlMD5) != 0 ||
959                    (IO.Header.Flags[dsFList::tHardLink] & dsFList::HardLink::FlMD5) != 0);
960    
961    return true;
962 }
963                                                                         /*}}}*/
964 // SigWinch - Window size change signal handler                         /*{{{*/
965 // ---------------------------------------------------------------------
966 /* */
967 void SigWinch(int)
968 {
969    // Riped from GNU ls
970 #ifdef TIOCGWINSZ
971    struct winsize ws;
972   
973    if (ioctl(1, TIOCGWINSZ, &ws) != -1 && ws.ws_col >= 5)
974       ScreenWidth = ws.ws_col - 1;
975    if (ScreenWidth > 250)
976       ScreenWidth = 250;
977 #endif
978 }
979                                                                         /*}}}*/
980 // ShowHelp - Show the help screen                                      /*{{{*/
981 // ---------------------------------------------------------------------
982 /* */
983 bool ShowHelp(CommandLine &CmdL)
984 {
985    cout << PACKAGE << ' ' << VERSION << " for " << ARCHITECTURE <<
986       " compiled on " << __DATE__ << "  " << __TIME__ << endl;
987    
988    cout << 
989       "Usage: dsync-flist [options] command [file]\n"
990       "\n"
991       "dsync-flist is a tool for manipulating dsync binary file lists.\n"
992       "It can generate the lists and check them against a tree.\n"
993       "\n"
994       "Commands:\n"
995       "   generate - Build a file list\n"
996       "   help - This help text\n"
997       "   dump - Display the contents of the list\n"
998       "   md5sums - Print out 'indices' file, suitable for use with md5sum\n"
999       "   md5cache - Print out md5sums of the files given on stdin\n"
1000       "   link-dups - Look for duplicate files\n"
1001       "   lookup - Display a single file record\n"
1002       "   verify - Compare the file list against the local directory\n"
1003       "\n"   
1004       "Options:\n"
1005       "  -h  This help text.\n"
1006       "  -q  Loggable output - no progress indicator\n"
1007       "  -qq No output except for errors\n"
1008       "  -i=? Include pattern\n"
1009       "  -e=? Exclude pattern\n"
1010       "  -c=? Read this configuration file\n"
1011       "  -o=? Set an arbitary configuration option, ie -o dir::cache=/tmp\n"
1012       "See the dsync-flist(1) and dsync.conf(5) manual\n"
1013       "pages for more information." << endl;
1014    return 100;
1015 }
1016                                                                         /*}}}*/
1017
1018 int main(int argc, const char *argv[])
1019 {
1020    CommandLine::Args Args[] = {
1021       {'h',"help","help",0},
1022       {'q',"quiet","quiet",CommandLine::IntLevel},
1023       {'q',"silent","quiet",CommandLine::IntLevel},
1024       {'i',"include","FileList::Filter:: + ",CommandLine::HasArg},
1025       {'e',"exclude","FileList::Filter:: - ",CommandLine::HasArg},
1026       {'n',"no-act","noact",0},
1027       {'v',"verbose","verbose",CommandLine::IntLevel},
1028       {0,"delete","delete",0},
1029       {0,"prefer-include","FileList::Prefer-Filter:: + ",CommandLine::HasArg},
1030       {0,"prefer-exclude","FileList::Prefer-Filter:: - ",CommandLine::HasArg},
1031       {0,"pi","FileList::Prefer-Filter:: + ",CommandLine::HasArg},
1032       {0,"pe","FileList::Prefer-Filter:: - ",CommandLine::HasArg},
1033       {0,"clean-include","FList::Clean-Filter:: + ",CommandLine::HasArg},
1034       {0,"clean-exclude","FList::Clean-Filter:: - ",CommandLine::HasArg},
1035       {0,"ci","FList::Clean-Filter:: + ",CommandLine::HasArg},
1036       {0,"ce","FList::Clean-Filter:: - ",CommandLine::HasArg},
1037       {0,"rsync-include","FList::RSync-Filter:: + ",CommandLine::HasArg},
1038       {0,"rsync-exclude","FList::RSync-Filter:: - ",CommandLine::HasArg},
1039       {0,"ri","FList::RSync-Filter:: + ",CommandLine::HasArg},
1040       {0,"re","FList::RSync-Filter:: - ",CommandLine::HasArg},
1041       {0,"md5","FileList::MD5-Hashes",0},
1042       {0,"rsync","FileList::RSync-Hashes",0},
1043       {0,"rsync-min","FileList::MinRSyncSize",CommandLine::HasArg},
1044       {0,"perm","FileList::Permissions",0},
1045       {0,"owner","FileList::Ownership",0},
1046       {0,"order","FileList::Order",CommandLine::HasArg},
1047       {'c',"config-file",0,CommandLine::ConfigFile},
1048       {'o',"option",0,CommandLine::ArbItem},
1049       {0,0,0,0}};
1050    CommandLine::Dispatch Cmds[] = {{"generate",&DoGenerate},
1051                                    {"help",&ShowHelp},
1052                                    {"dump",&DoDump},
1053                                    {"link-dups",&DoMkHardLinks},
1054                                    {"md5sums",&DoMD5Dump},
1055                                    {"md5cache",&DoMD5Cache},
1056                                    {"lookup",&DoLookup},
1057                                    {"verify",&DoVerify},
1058                                    {0,0}};
1059    CommandLine CmdL(Args,_config);
1060    if (CmdL.Parse(argc,argv) == false)
1061    {
1062       _error->DumpErrors();
1063       return 100;
1064    }
1065    
1066    // See if the help should be shown
1067    if (_config->FindB("help") == true ||
1068        CmdL.FileSize() == 0)
1069       return ShowHelp(CmdL);   
1070
1071    // Setup the output streams
1072 /*   c0out.rdbuf(cout.rdbuf());
1073    c1out.rdbuf(cout.rdbuf());
1074    c2out.rdbuf(cout.rdbuf()); */
1075    if (_config->FindI("quiet",0) > 0)
1076       c0out.rdbuf(devnull.rdbuf());
1077    if (_config->FindI("quiet",0) > 1)
1078       c1out.rdbuf(devnull.rdbuf());
1079
1080    // Setup the signals
1081    signal(SIGWINCH,SigWinch);
1082    SigWinch(0);
1083    
1084    // Match the operation
1085    CmdL.DispatchArg(Cmds);
1086    
1087    // Print any errors or warnings found during parsing
1088    if (_error->empty() == false)
1089    {
1090       
1091       bool Errors = _error->PendingError();
1092       _error->DumpErrors();
1093       return Errors == true?100:0;
1094    }
1095          
1096    return 0; 
1097 }