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