001/* A program to copy directory trees
002 *
003 * Copyright (C) 2015 Sidney Marshall (swm@cs.rit.edu)
004 *
005 * This program is free software: you can redistribute it and/or
006 * modify it under the terms of the GNU General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but
011 * WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program.  If not, see
017 * <http://www.gnu.org/licenses/>.
018 */
019
020import java.io.FileOutputStream;
021import java.io.File;
022import java.io.FileInputStream;
023import java.io.IOException;
024import java.util.Arrays;
025import java.util.Comparator;
026
027/**
028 * This class copies files and directories from one directory to
029 * another. It can check for differences either by file date or by a
030 * byte by byte file compare.
031 *
032 * Without the -m option it uses byte by byte file compare and does
033 * not remove target files or directories that are not in the source
034 * directory.
035 *
036 * With the -m option it uses file dates to determine differences and
037 * does remove files target files that do not exist in the source
038 * directory. It also sets the dates in the target directory to match
039 * the dates in the source directioy.
040 */
041class Copyfiles {
042  //static byte xbuf[] = new byte[1048576]; // not thread safe
043  //static byte ybuf[] = new byte[1048576]; // not thread safe
044
045  /**
046   * Does a byte for byte comparison of two files. Returns false on
047   * any error.
048   *
049   * @param x first file to compare
050   * @param y second file to compare
051   * @return true if the files are exactly the same
052   */
053  static boolean equalFile(File x, File y) {
054    if(x.length() != y.length()) return false;
055    FileInputStream strmx = null;
056    FileInputStream strmy = null;
057    byte xbuf[] = new byte[1048576];
058    byte ybuf[] = new byte[1048576];
059    try {
060      strmx = new FileInputStream(x);
061      strmy = new FileInputStream(y);
062      int xlen;
063      int ylen;
064      Arrays.fill(xbuf, (byte)0);
065      Arrays.fill(ybuf, (byte)0);
066      while(true) {
067        xlen = strmx.read(xbuf);
068        ylen = strmy.read(ybuf);
069        if(xlen != ylen) return false;
070        if(xlen < 0) return true;
071        if(!Arrays.equals(xbuf, ybuf)) return false;
072      }
073    } catch(IOException e) {
074      return false;
075    } finally {
076      if(strmx != null) try { strmx.close();} catch(IOException e) {}
077      if(strmy != null) try { strmy.close();} catch(IOException e) {}
078    }
079  }
080
081  /**
082   * Compare modification times and return true if less than 2 seconds
083   * different. This is to compensate for various precisions in time
084   * stamps between opreating systems.
085   *
086   * @param x first file to compare
087   * @param y second file to compare
088   * @return true if the modification times are within 2 seconds of each other
089   */
090  static boolean equalDate(File x, File y) {
091    return Math.abs(x.lastModified() - y.lastModified()) <= 2000;
092  }
093
094  /**
095   * Copies a file creating or replacing the destination file as
096   * necessary.
097   *
098   * @param from source file of copy
099   * @param to destination file of copy
100   * @return true if no errors
101   */
102  static boolean copyFile(File from, File to) {
103    boolean status = true;
104    FileInputStream in = null;
105    FileOutputStream out = null;
106    byte xbuf[] = new byte[1048576];
107    try {
108      in = new FileInputStream(from);
109      out = new FileOutputStream(to);
110      int len;
111      while((len = in.read(xbuf)) > 0) {
112        out.write(xbuf, 0, len);
113      }
114    } catch(IOException e) {
115      status = false;
116    } finally {
117      if(in != null) {
118        try {
119          in.close();
120        } catch(IOException e) { status = false; }
121      }
122      if(out != null) {
123        try {
124          out.close();
125        } catch(IOException e) { status = false; }
126      }
127    }
128    return status;
129  }
130
131  /**
132   * Return a relative path relative to the given absolute path. If
133   * the prefix is not a prefix of the absolute path just return the
134   * unmodified absolute path.
135   *
136   * @param prefix the prefix to be stripped off of the path
137   * @param absolute the path to be stripped
138   * @return the stripped path
139   */
140  static String relativePath(String prefix, String absolute) {
141    int index = absolute.indexOf(prefix);
142    if(index == 0 && prefix.length() != absolute.length()) {
143      String suffix = absolute.substring(prefix.length());
144      if(suffix.charAt(0) == File.separatorChar) {
145        suffix = suffix.substring(1);
146      }
147      return suffix;
148    }
149    return absolute;
150  }
151
152  /**
153   * A comparator on File comparing base names.
154   */
155  static Comparator<File> fileComp = new Comparator<File>() {
156      public int compare(File f1, File f2) {
157        return f1.getName().compareTo(f2.getName());
158      }
159    };
160
161  String fromBase;
162  String toBase;
163
164  /**
165   * Make two directories equal by recursively copying files with
166   * differing content. The file dates are not copied.
167   *
168   * @param from the source directory for the copy
169   * @param to the destination directory for the copy
170   */
171  Copyfiles(File from, File to) {
172    fromBase = from.getAbsolutePath();
173    toBase = to.getAbsolutePath();
174    copyDirectories(from, to);
175  }
176
177  /**
178   * Mirror two directories - both arguments must be directories and
179   * the destination directory must exist. Dates are not preserved.
180   *
181   * flag -t update timestamps
182   * flag -d delete if not in source directory
183   *
184   * Currently updates timestamps and deletes if not in source
185   * directory as there are no flags yet.
186   *
187   * 4 cases file, directory, unknown, missing
188   *
189   * @param fromDir the source directory for the copy
190   * @param toDir the destination directory for the copy
191   */
192  void copyDirectories(File fromDir, File toDir) {
193    File[] fromFiles = fromDir.listFiles();
194    Arrays.sort(fromFiles, fileComp); // sort by getName()
195    File[] toFiles = toDir.listFiles();
196    Arrays.sort(toFiles, fileComp); // sort by getName()
197    int i = 0, j = 0;
198    int flen = fromFiles.length, tlen = toFiles.length;
199    while(i < flen || j < tlen) {
200      int c = i == flen ? 1 : j == tlen ? -1
201        : fromFiles[i].getName().compareTo(toFiles[j].getName());
202      if(c < 0) {
203        // directory entry only in fromDir
204        String fromPath = fromFiles[i].getAbsolutePath();
205        File fromFile = fromFiles[i].getAbsoluteFile();
206        String name = relativePath(fromBase, fromPath);
207        File toFile = new File(toDir, fromFiles[i].getName());
208        // create empty file/directory and continue
209        if(fromFiles[i].isDirectory()) {
210          System.out.println("Creating directory: " + name);
211          toFile.mkdir();
212          // update time stamp ///////////////////////
213          copyDirectories(fromFiles[i], toFile);
214        } else if(fromFiles[i].isFile()) {
215          // print message and copy
216          System.out.println("Creating: " + name);
217          if(!copyFile(fromFile, toFile)) {
218            System.out.println("Copying " + name + " failed.");
219          }
220          // update timestamp
221          if(!toFile.setLastModified(fromFiles[i].lastModified())) {
222            System.out.println("Setting last modified on " + toFile + " failed.");
223          }
224        } else {
225          System.out.println("Unknown file type: " + name);
226        }
227        i++;
228      } else if(c > 0) {
229        File toFile = new File(toDir, toFiles[j].getName());
230        String toPath = toFile.getAbsolutePath();
231        String name = relativePath(toBase, toPath);
232        // directory entry only in toDir - print error and continue
233        System.out.println("Only in destination directory: " + name);
234        // remove directory if flag set /////////////////////
235        j++;
236      } else {
237        File fromFile = new File(fromDir, fromFiles[i].getName());
238        File toFile = new File(toDir, toFiles[j].getName());
239        String toPath = toFile.getAbsolutePath();
240        String name = relativePath(toBase, toPath);
241        if(fromFile.isFile() && toFile.isFile()) {
242          // both files
243          // if both dates and contents differ
244          //   copy and set date
245          // }
246          // x.length() != y.length()
247          if(!equalDate(fromFile, toFile) && !equalFile(fromFile, toFile)) {
248            // print message and copy
249            System.out.println("Updating: " + name);
250            if(!copyFile(fromFile, toFile)) {
251              System.out.println("Copying " + name + " failed.");
252            }
253            // update timestamp ///////////////////////////
254            if(!toFile.setLastModified(fromFile.lastModified())) {
255              System.out.println("Setting last modified on " + toFile + " failed.");
256            }
257          }
258        } else if(fromFile.isDirectory() && toFile.isDirectory()) {
259          // both directories
260          copyDirectories(fromFile, toFile);
261        } else {
262          System.out.println("Incompatable file types: " + name);
263          // if flag remove and copy ///////////////////////////
264        }
265        i++;
266        j++;
267      }
268    }
269  }
270
271  /**
272   * Mirror two directories - both arguments must be directories and
273   * the destination directory must exist. Dates are preserved.
274   * Equality is checked by comparing dates on files.
275   *
276   * 4 cases file, directory, unknown, missing
277   *
278   * @param fromDir the source directory for the copy
279   * @param toDir the destination directory for the copy
280   */
281  static void mirrorDirectories(File fromDir, File toDir) {
282    File[] fromFiles = fromDir.listFiles();
283    Arrays.sort(fromFiles, fileComp); // sort by getName()
284    File[] toFiles = toDir.listFiles();
285    Arrays.sort(toFiles, fileComp); // sort by getName()
286    int i = 0, j = 0;
287    int flen = fromFiles.length, tlen = toFiles.length;
288    while(i < flen || j < tlen) {
289      int c = i == flen ? 1 : j == tlen ? -1
290        : fromFiles[i].getName().compareTo(toFiles[j].getName());
291      if(c < 0) {
292        // directory entry only in fromDir
293        File toFile = new File(toDir, fromFiles[i].getName());
294        // create empty file/directory and continue
295        if(fromFiles[i].isDirectory()) {
296          System.out.println("Creating directory: " + toFile);
297          toFile.mkdir();
298          // update time stamp ///////////////////////
299          mirrorDirectories(fromFiles[i], toFile);
300        } else if(fromFiles[i].isFile()) {
301          // print message and copy
302          System.out.println("Creating: " + toFile);
303          if(!copyFile(fromFiles[i], toFile)) {
304            System.out.println("Copying " + toFile + " failed.");
305          }
306          if(!toFile.setLastModified(fromFiles[i].lastModified())) {              
307            System.out.println("Setting last modified on " + toFile + " failed.");
308          }
309        } else {
310          System.out.println("Unknown file type: " + fromFiles[i]);
311        }
312        i++;
313      } else if(c > 0) {
314        // directory entry only in toDir - print error and continue
315        System.out.println("Only in destination directory: " + toFiles[j]);
316        if(!delete(toFiles[j])) {
317          System.out.println("Deleting " + toFiles[j] + " failed.");
318        }
319        // remove directory if flag set /////////////////////
320        j++;
321      } else {
322        if(fromFiles[i].isFile() && toFiles[j].isFile()) {
323          // both files
324          if(fromFiles[i].lastModified() != toFiles[j].lastModified()) {
325            // print message and copy
326            System.out.println("Updating: " + toFiles[j]);
327            if(!copyFile(fromFiles[i], toFiles[j])) {
328              System.out.println("Copying " + toFiles[j] + " failed.");
329            }
330            // update timestamp ///////////////////////////
331            try {
332              if(!toFiles[j].setLastModified(fromFiles[i].lastModified())) {
333                System.out.println("Setting last modified on " + toFiles[j] + " failed.");
334              }
335            }
336            catch(Exception e) {
337              System.out.println("Setting last modified on " + toFiles[j] + " failed.");
338              System.out.println("Date was: " + fromFiles[i].lastModified());
339              e.printStackTrace();
340            }
341          }
342        } else if(fromFiles[i].isDirectory() && toFiles[j].isDirectory()) {
343          // both directories
344          mirrorDirectories(fromFiles[i], toFiles[j]);
345        } else {
346          System.out.println("Incompatable file types: " + fromFiles[i]);
347          ///////////// should delete and copy
348        }
349        i++;
350        j++;
351      }
352    }
353  }
354
355  /**
356   * This method deletes the specified directory. If given a
357   * directory it recursively deletes its contents and then the
358   * directory. Returns true if successful, otherwise false.
359   *
360   * @param file the file or directory to be deleted
361   * @return true if file is a directory and there are no errors
362   */
363  static boolean delete(File file) {
364    if(file.isDirectory()) {
365      boolean status = true;
366      File[] files = file.listFiles();
367      for(int i = 0; i < files.length; i++) {
368        status &= delete(files[i]);
369      }
370      status &= file.delete();
371      return status;
372    } else if(file.isFile()) return file.delete();
373    else return false;
374  }
375
376  /**
377   * Copies or mirrors a directory tree.
378   *
379   * java [-m] sourceDirectory destinationDirectory
380   *
381   * Without the -m flag does a byte for byte compare and does not
382   * delete files in the destination directory.
383   *
384   * With the -m flag only checks file dates for differences and
385   * deletes destination files and directories that do not appear in
386   * the source directory hierarchy.
387   *
388   * Note: file syntax is c:/ etc.
389   *
390   * @param args the command line arguments.
391   */
392  public static void main(String...args) {
393    if(args.length == 3 && args[0].equals("-m")) {
394      mirrorDirectories(new File(args[1]), new File(args[2]));
395    } else {
396      File from = new File(args[0]);
397      File to = new File(args[1]);
398      Copyfiles update = new Copyfiles(from, to);
399      //update.mirrorDirectories(from, to);
400    }
401  }
402} // class CopyFiles