package org.cdlib.xtf.util; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; 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. Now uses * rsync for speed and simplicity, and adds a threshold above which we avoid * per-subdirectory syncing and just do the whole thing. * * @author Martin Haye */ public class DirSync { public static final int MAX_SELECTIVE_SYNC = 500; private static final int MAX_RSYNC_BATCH = 2; 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 { // If there are no directories specified, or there are too many, or if only // the top-level directory is being sync'd, just rsync the entire source to // the dest. // if (filter == null || filter.size() > MAX_SELECTIVE_SYNC || (filter.size() == 1 && new File(filter.getTargets().get(0)).getCanonicalFile().equals(srcDir.getCanonicalFile()))) { runRsync(srcDir, dstDir, null, new String[] { "--exclude=scanDirs.list" }); } // Otherwise do a selective sync. else selectiveSync(srcDir, dstDir); // Always do the scanDirs.list file last, since it governs incremental syncing. // If it were done before other files, and the sync process aborted, we might // mistakenly think two directories were perfectly in sync when in fact they // are different. // runRsync(new File(srcDir, "scanDirs.list"), dstDir, null, null); } /** * The main workhorse of the scanner. * * @param srcDir Directory to match * @param dstDir Directory to modify * @throws IOException If anything goes wrong */ private void selectiveSync(File srcDir, File dstDir) throws IOException { // First, sync the top-level files (no sub-dirs) runRsync(srcDir, dstDir, null, new String[] { "--exclude=/*/", "--exclude=scanDirs.list" }); // Now sync the subdirectories in batches, not to exceed the batch limit if (!filter.isEmpty()) { ArrayList<String> dirBatch = new ArrayList(); String basePath = srcDir.getCanonicalPath() + "/"; for (String target : filter.getTargets()) { String targetPath = new File(target).getCanonicalPath(); assert targetPath.startsWith(basePath) : ("targetPath '" + targetPath.toString() + "' should start with basePAth '" + basePath.toString() + "'"); targetPath = targetPath.substring(basePath.length()); dirBatch.add(targetPath); if (dirBatch.size() >= MAX_RSYNC_BATCH) { runRsync(srcDir, dstDir, dirBatch, null); dirBatch.clear(); } } // Finish the last batch of subdirs (if any) if (!dirBatch.isEmpty()) runRsync(srcDir, dstDir, dirBatch, new String[] { "--exclude=scanDirs.list" }); } } /** * Run an rsync command with the standard arguments plus the * specified subdirectories and optional extra args. * * @param src Directory (or file) to match * @param dst Directory (or file) to modify * @param subDirs Sub-directories to rsync (null for all) * @throws IOException If anything goes wrong */ public void runRsync(File src, File dst, List<String> subDirs, String[] extraArgs) throws IOException { try { // First the basic arguments ArrayList<String> args = new ArrayList(6); args.add("rsync"); args.add("-av"); //args.add("--dry-run"); args.add("--delete"); // Add any extra arguments at this point, before the paths. if (extraArgs != null) { for (String extra : extraArgs) args.add(extra); } // We want to hard link dest files to the source if (src.isDirectory()) args.add("--link-dest=" + src.getAbsolutePath() + "/"); // For the source, add in the weird "./" syntax for relative syncing, e.g. // rsync --relative server.org:data/13030/pairtree_root/qt/00/./{01/d5,04/k4} data/13030/pairtree_root/qt/00/ // if (subDirs != null) { args.add("--relative"); for (String subDir : subDirs) { if (new File(src.getAbsolutePath(), subDir).canRead()) args.add(src.getAbsolutePath() + "/./" + subDir); } } else args.add(src.getAbsolutePath() + (src.isDirectory() ? "/" : "")); // Finally add the destination path args.add(dst.getAbsolutePath() + (dst.isDirectory() ? "/" : "")); // And run the command String[] argArray = args.toArray(new String[args.size()]); ProcessRunner.runAndGrab(argArray, "", 0); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (CommandFailedException e) { throw new IOException(e.getMessage()); } } }