package hudson.plugins.filesystem_scm;
import java.io.*;
import java.util.*;
import org.apache.commons.io.*;
import org.apache.commons.io.filefilter.*;
/** Detect if two folders are the same or not
*
* <p>This is the core logic for detecting if we need to checkout or pollchanges</p>
*
* <p>Two methods to detect if the two folders are the same
* <ul>
* <li>check if there are new/modified files in the source folder</li>
* <li>check if there are deleted files in the source folder</li>
* </ul>
* </p>
*
* @author Sam NG
*
*/
public class FolderDiff implements Serializable {
private static final long serialVersionUID = 1L;
private String srcPath;
private String dstPath;
private boolean filterEnabled;
private boolean includeFilter;
private String[] filters;
private Set<String> allowDeleteList;
public FolderDiff() {
filterEnabled = false;
}
public void setSrcPath(String srcPath) {
this.srcPath = srcPath;
}
public void setDstPath(String dstPath) {
this.dstPath = dstPath;
}
public void setIncludeFilter(String[] filters) {
filterEnabled = true;
includeFilter = true;
this.filters = filters;
}
public void setExcludeFilter(String[] filters) {
filterEnabled = true;
includeFilter = false;
this.filters = filters;
}
public void setAllowDeleteList(Set<String> allowDeleteList) {
this.allowDeleteList = allowDeleteList;
}
/*
public boolean isModifiedSince(long time) {
//if ( hasNewOrModifiedFiles(time) ) return true;
//else if ( hasDeletedFiles(time) ) return true;
//else return false;
return true;
}*/
/**
* <p>For each file in the source folder
* <ul>
* <li>if file is not in destination, this is a new file</li>
* <li>if the destination file exists but is old, this is a modified file</li>
* </ul>
*
* <p>Note: the time parameter (1st param) is basically not used in the code.
* On Windows, the lastModifiedDate will not be updated when you copy a file to the source folder,
* until we have a way to get the "real" lastModifiedDate on Windows, we won't use this "time" field</p>
*
* @param time should be the last build time, to improve performance, we will list all files modified after "time" and check with destination
* @param breakOnceFound to improve performance, we will return once we found the 1st new or modified file
* @param testRun if true, will not sync file from source to destination, otherwise, will sync files if new or modified files found
*
* @return the list of new or modified files
*/
public List<Entry> getNewOrModifiedFiles(long time, boolean breakOnceFound, boolean testRun) {
File src = new File(srcPath);
File dst = new File(dstPath);
IOFileFilter dirFilter = HiddenFileFilter.VISIBLE;
AndFileFilter fileFilter = new AndFileFilter();
// AgeFileFilter is base on lastModifiedDate, but if you copy a file on Windows, the lastModifiedDate is not changed
// only the creation date is updated, so we can't use the following AgeFileFiilter
// fileFilter.addFileFilter(new AgeFileFilter(time, false /* accept newer */));
fileFilter.addFileFilter(HiddenFileFilter.VISIBLE);
if ( filterEnabled && null != filters && filters.length > 0 ) {
WildcardFileFilter wcf = new WildcardFileFilter(filters, IOCase.INSENSITIVE);
if ( includeFilter ) {
fileFilter.addFileFilter(wcf);
} else {
fileFilter.addFileFilter(new NotFileFilter(wcf));
}
}
Iterator<File> it = (Iterator<File>)FileUtils.iterateFiles(src, fileFilter, dirFilter);
ArrayList<Entry> list = new ArrayList<Entry>();
while( it.hasNext() ) {
File file = it.next();
try {
String relativeName = getRelativeName(file.getAbsolutePath(), src.getAbsolutePath());
boolean newOrModified = false;
// need to change dst to see if there is such a file
File tmp = new File(dst, relativeName);
if ( !tmp.exists() ) {
newOrModified = true;
list.add(new Entry(relativeName, Entry.Type.NEW));
log("New file: " + relativeName);
} else if ( FileUtils.isFileNewer(file, time) || FileUtils.isFileNewer(file, tmp) ) {
newOrModified = true;
list.add(new Entry(relativeName, Entry.Type.MODIFIED));
log("Modified file: " + relativeName);
}
if ( newOrModified ) {
if ( breakOnceFound ) return list;
if ( !testRun ) {
// FileUtils.copyFile(file, tmp);
copyFile(file, tmp);
}
}
} catch ( IOException e ) {
log(e);
}
}
return list;
}
/**
* <p>For each file in the destination folder
* <ul>
* <li>if file is not in source, and it is in the allowDeleteList, this file will be deleted in the source</li>
* </ul>
*
* <p>Note: the time parameter (1st param) is basically not used in the code.
* On Windows, the lastModifiedDate will not be updated when you copy a file to the source folder,
* until we have a way to get the "real" lastModifiedDate on Windows, we won't use this "time" field</p>
*
* @param time should be the last build time, to improve performance, we will list all files modified after "time" and check with source
* @param breakOnceFound to improve performance, we will return once we found the 1st new or modified file
* @param testRun if true, will not sync file from source to destination, otherwise, will sync files if deleted files found
*
* @return the list of deleted files
*/
public List<Entry> getDeletedFiles(long time, boolean breakOnceFound, boolean testRun) {
File src = new File(srcPath);
File dst = new File(dstPath);
IOFileFilter dirFilter = HiddenFileFilter.VISIBLE;
AndFileFilter fileFilter = new AndFileFilter();
// AgeFileFilter is base on lastModifiedDate, but if you copy a file on Windows, the lastModifiedDate is not changed
// only the creation date is updated, so we can't use the following AgeFileFiilter
//fileFilter.addFileFilter(new AgeFileFilter(time, true /* accept older */));
fileFilter.addFileFilter(HiddenFileFilter.VISIBLE);
if ( filterEnabled && null != filters && filters.length > 0 ) {
WildcardFileFilter wcf = new WildcardFileFilter(filters, IOCase.INSENSITIVE);
if ( includeFilter ) {
fileFilter.addFileFilter(wcf);
} else {
fileFilter.addFileFilter(new NotFileFilter(wcf));
}
}
Iterator<File> it = (Iterator<File>)FileUtils.iterateFiles(dst, fileFilter, dirFilter);
ArrayList<Entry> list = new ArrayList<Entry>();
while(it.hasNext()) {
File file = it.next();
try {
String relativeName = getRelativeName(file.getAbsolutePath(), dst.getAbsolutePath());
File tmp = new File(src, relativeName);
if ( !tmp.exists() && (null == allowDeleteList || allowDeleteList.contains(relativeName)) ) {
log("Deleted file: " + relativeName);
list.add(new Entry(relativeName, Entry.Type.DELETED));
if ( breakOnceFound ) return list;
if ( !testRun ) {
try {
boolean deleted = file.delete();
if ( !deleted ) {
log("file.delete() failed: " + file.getAbsolutePath());
}
} catch ( SecurityException e ) {
log("Can't delete " + file.getAbsolutePath(), e);
}
}
}
} catch ( IOException e ) {
log(e);
}
}
return list;
}
/** This function will convert e.stackTrace to String and call log(String)
*
* @param msg
* @param e
*/
protected void log(Exception e ) {
log(stackTraceToString(e));
}
/** This function will convert e.stackTrace to String and call log(String)
*
* @param msg
* @param e
*/
protected void log(String msg, Exception e) {
log(msg + "\n" + stackTraceToString(e));
}
/** Default log to System.out
*
* @param msg
*/
protected void log(String msg) {
System.out.println(msg);
}
/** Convert Exception.stackTrace to String
*
* @param e
* @return
*/
public static String stackTraceToString(Exception e ) {
StringWriter buf = new StringWriter();
PrintWriter writer = new PrintWriter(buf);
e.printStackTrace(writer);
writer.flush();
buf.flush();
return buf.toString();
}
/**
* Get the relative path of fileName and folderName
* <ul>
* <li>fileName = c:\abc\def\foo.java</li>
* <li>folderName = c:\abc</li>
* <li>relativeName = def\foo.java
* </ul>
* This function will not handle Unix/Windows path separator conversation, but will append a java.io.File.separator if folderName does not end with one
* @param fileName the full path of the file, usually file.getAbsolutePath()
* @param folderName the full path of the folder, usually dir.getAbsolutePath()
* @return the relativeName of fileNamae and folderName
* @throws IOException if fileName is not relative to folderName
*/
public static String getRelativeName(String fileName, String folderName) throws IOException {
// make sure there is an end separator after folderName
String sep = java.io.File.separator;
if ( !folderName.endsWith(sep) ) folderName += sep;
int x = fileName.indexOf(folderName);
if ( 0 != x ) throw new IOException(fileName + " is not inside " + folderName);
String relativeName = fileName.substring(folderName.length() );
return relativeName;
}
/** Copy file from source to destination (default will not copy file permission)
*
* @param src Source File
* @param dst Destination File
* @throws IOException
*/
protected void copyFile(File src, File dst) throws IOException {
FileUtils.copyFile(src, dst);
}
public static class Entry implements Serializable {
private static final long serialVersionUID = 1L;
private String filename;
private Type type;
public enum Type { MODIFIED, NEW, DELETED };
public Entry() { }
public Entry(String filename, Type type) {
this.filename = filename;
this.type = type;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public Type getType() {
return type;
}
public void setType(Type type) {
this.type = type;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((filename == null) ? 0 : filename.hashCode());
result = prime * result + ((type == null) ? 0 : type.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Entry other = (Entry) obj;
if (filename == null) {
if (other.filename != null)
return false;
} else if (!filename.equals(other.filename))
return false;
if (type == null) {
if (other.type != null)
return false;
} else if (!type.equals(other.type))
return false;
return true;
}
}
}