package org.cdlib.xtf.util;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import org.cdlib.xtf.util.ProcessRunner.CommandFailedException;
/**
* Copyright (c) 2009, Regents of the University of California
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the University of California nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* Routines to synchronize one directory hierarchy to match another.
*
* @author Martin Haye
*/
public class DirSync
{
private StringBuffer linkCmds;
private ArrayList<File> fixDirs = new ArrayList();
private ArrayList<Long> fixTimes = new ArrayList();
private SubDirFilter filter;
/**
* Initialize a directory syncer with no sub-directory filter
* (all sub-directories will be scanned.)
*/
public DirSync() {
this(null);
}
/**
* Initialize with a sub-directory filter.
*/
public DirSync(SubDirFilter filter) {
this.filter = filter;
}
/**
* Sync the files from source to dest.
*
* @param srcDir Directory to match
* @param dstDir Directory to modify
* @throws IOException If anything goes wrong
*/
public void syncDirs(File srcDir, File dstDir)
throws IOException
{
linkCmds = new StringBuffer();
syncDirs(srcDir, dstDir, 0);
flushLinks();
}
/**
* The main workhorse of the scanner.
*
* @param srcDir Directory to match
* @param dstDir Directory to modify
* @param level 0,1,2... during recursive scan
* @throws IOException If anything goes wrong
*/
private void syncDirs(File srcDir, File dstDir, int level) throws IOException
{
String[] srcList = readSortedDir(srcDir);
String[] dstList = readSortedDir(dstDir);
int srcEnd = srcList.length;
int dstEnd = dstList.length;
int s = 0;
int d = 0;
while (s < srcEnd || d < dstEnd)
{
File srcFile = null;
File dstFile = null;
long direction; // if < 0 copy-src; if == 0 skip; if > 0 delete-dst
// Decide what to do. First, if we still have source files...
if (s < srcEnd)
{
srcFile = new File(srcDir, srcList[s]);
// And we still have dest files...
if (d < dstEnd)
{
dstFile = new File(dstDir, dstList[d]);
// Compare the name. If no match, do the lesser first
direction = srcList[s].compareTo(dstList[d]);
// If same name...
if (direction == 0)
{
// For directories, keep scanning down.
if (srcFile.isDirectory() && dstFile.isDirectory())
direction = 0;
else
{
// For files, delete dest if older than src; skip if same mod time.
direction = srcFile.lastModified() - dstFile.lastModified();
// Special case: skip if source older than dest.
if (direction < 0)
direction = 0;
}
}
}
// Ran out of dest files. Copy src.
else
direction = -1;
}
// Ran out of src files. Copy dst.
else {
dstFile = new File(dstDir, dstList[d]);
direction = 1;
}
// Now act on the decision made above.
if (direction < 0) {
File newFile = new File(dstDir, srcList[s]);
if (srcFile.isDirectory())
copyDir(srcFile, newFile, level+1);
else
copyFile(srcFile, newFile);
s++;
}
else if (direction > 0) {
if (dstFile.isDirectory())
deleteDir(dstFile);
else
deleteFile(dstFile);
d++;
}
else {
if (srcFile.isDirectory())
copyDir(srcFile, dstFile, level+1);
else
skipFile(srcFile, dstFile);
s++;
d++;
}
}
}
private void skipFile(File srcFile, File dstFile) {
//System.out.format("Skip file %s == %s\n", srcFile.toString(), dstFile.toString());
}
private void deleteFile(File file) throws IOException {
//System.out.format("Delete file %s\n", file.toString());
if (!file.delete())
throw new IOException("Sync error: cannot delete file '" + file.toString() + "'");
}
private void deleteDir(File dir) throws IOException {
//System.out.format("Delete dir %s\n", dir.toString());
if (filter == null || filter.approve(dir.toString()))
Path.deleteDir(dir);
}
private void copyFile(File srcFile, File dstFile) {
//System.out.format("Copy file %s to %s\n", srcFile.toString(), dstFile.toString());
linkCmds.append("link('" + srcFile.toString() + "', '" + dstFile.toString() + "') or die;\n");
}
private void copyDir(File srcDir, File dstDir, int level) throws IOException {
//System.out.format("Copy dir %s to %s\n", srcDir.toString(), dstDir.toString());
if (filter == null || filter.approve(srcDir.toString())) {
if (!dstDir.exists() && !dstDir.mkdir())
throw new IOException("Error creating directory '" + dstDir + "'");
syncDirs(srcDir, dstDir, level);
if (srcDir.lastModified() != dstDir.lastModified()) {
fixDirs.add(dstDir);
fixTimes.add(srcDir.lastModified());
}
}
//else
// System.out.format("Skip dir %s\n", srcDir.toString());
}
/**
* Create all the hard links that we determined need to be made. Since
* Java (as of 1.6) has no ability to create these, we call out and have
* Perl do it for us.
*
* @throws IOException If something goes wrong.
*/
private void flushLinks() throws IOException
{
// No links? Nothing to do.
if (linkCmds.length() == 0)
return;
// Fire up Perl and run the script to make the links.
try {
String[] args = new String[1];
args[0] = "perl";
//System.out.println("Link cmds:\n" + linkCmds.toString());
ProcessRunner.runAndGrab(args, linkCmds.toString(), 0);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (CommandFailedException e) {
throw new IOException(e.getMessage());
}
// Now that we've copied all the files, fix up the directory mod times.
assert fixDirs.size() == fixTimes.size(); // Should have added in parallel
for (int i = 0; i < fixDirs.size(); i++) {
File dstDir = fixDirs.get(i);
long time = fixTimes.get(i);
if (!dstDir.setLastModified(time))
throw new IOException("Error setting last modified date on dir '" + dstDir + "'");
}
}
/**
* Get a sorted list of the files (and subdirectories) within a directory.
*/
private static String[] readSortedDir(File dir)
{
String[] list = dir.list();
Collections.sort(Arrays.asList(list));
String prev = null;
for (String s : list) {
assert prev == null || s.compareTo(prev) >= 0;
prev = s;
}
return list;
}
}