/** * DeployMan # Thomas Uhrig (Stuttgart, 2014) # www.tuhrig.de */ package de.tuhrig.deployman.repo; import static de.tuhrig.deployman.DeployMan.REPO_BUCKET; import static de.tuhrig.deployman.DeployMan.REPO_LOCALE; import static de.tuhrig.deployman.DeployMan.SLASH; import static de.tuhrig.deployman.DeployMan.getUserProperty; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.vfs.FileChangeEvent; import org.apache.commons.vfs.FileListener; import org.apache.commons.vfs.FileObject; import org.apache.commons.vfs.FileSystemManager; import org.apache.commons.vfs.VFS; import org.apache.commons.vfs.impl.DefaultFileMonitor; import com.amazonaws.AmazonClientException; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.ListObjectsRequest; import com.amazonaws.services.s3.model.ObjectListing; import com.amazonaws.services.s3.model.S3ObjectSummary; import de.tuhrig.deployman.aws.S3; import de.tuhrig.deployman.console.Console; import de.tuhrig.deployman.console.TerminalConsole; /** * @author tuhrig */ public class FileMonitor { private final AmazonS3 client; private final String locale; private final String bucket; private Console console; private boolean logging; public FileMonitor() { this(true); } public FileMonitor(boolean logging) { this(getUserProperty(REPO_BUCKET), getUserProperty(REPO_LOCALE), logging); } public FileMonitor(String bucket, String locale, boolean logging) { this.client = new S3().getClient(); this.bucket = bucket; this.locale = locale; this.logging = logging; this.console = new Console(new TerminalConsole(logging)); } public void monitor(String prefix) { prefix = addSlash(prefix); File root = new File(this.locale + SLASH + prefix); this.console.write("Monitor " + root); //$NON-NLS-1$ this.console.newLine(); final String finalPrefix = prefix; try { FileSystemManager manager = VFS.getManager(); FileObject file = manager.toFileObject(root); DefaultFileMonitor monitor = new DefaultFileMonitor(new FileListener() { @Override public void fileDeleted(FileChangeEvent arg0) throws Exception { sync(finalPrefix); } @Override public void fileCreated(FileChangeEvent arg0) throws Exception { sync(finalPrefix); } @Override public void fileChanged(FileChangeEvent arg0) throws Exception { sync(finalPrefix); } }); monitor.setDelay(3000); monitor.setRecursive(true); monitor.addFile(file); monitor.start(); } catch (IOException e) { this.console.write("Cannot monitor " + prefix); //$NON-NLS-1$ e.printStackTrace(); } } public void sync(String prefix) { prefix = addSlash(prefix); File root = new File(this.locale + SLASH + prefix); this.console.write("Sync " + root + " to " + this.bucket); //$NON-NLS-1$ //$NON-NLS-2$ try { Map<String, Md5FilePair> localFiles = getLocalFiles(root, prefix); Map<String, String> remoteFiles = getRemoteFiles(this.client, this.bucket); for (Entry<String, Md5FilePair> entry : localFiles.entrySet()) { String key = entry.getKey(); Md5FilePair value = entry.getValue(); if (!remoteFiles.containsKey(key)) { if (this.logging) { this.console.write("Add " + key); //$NON-NLS-1$ new RemoteRepository().uploadFile(value.file, key); } else new RemoteRepository().uploadFileSilently(value.file, key); } else if (!remoteFiles.get(key).equals(value.md5)) { if (this.logging) { this.console.write("Update " + key); //$NON-NLS-1$ new RemoteRepository().uploadFile(value.file, key); } else new RemoteRepository().uploadFileSilently(value.file, key); } } /* * Delete missing files */ for (Map.Entry<String, String> entry : remoteFiles.entrySet()) { String key = entry.getKey(); if (key.startsWith(prefix) && !localFiles.containsKey(key)) { this.console.write("Delete " + key); //$NON-NLS-1$ this.client.deleteObject(this.bucket, key); } } this.console.write("Sync done"); //$NON-NLS-1$ this.console.newLine(); } catch (IOException | AmazonClientException | InterruptedException e) { this.console.write("Cannot sync"); //$NON-NLS-1$ e.printStackTrace(); } } private Map<String, String> getRemoteFiles(AmazonS3 client, String bucketName) { this.console.write("Get remote files..."); //$NON-NLS-1$ ObjectListing listing; Map<String, String> files = new TreeMap<>(); ListObjectsRequest listObjectsRequest = new ListObjectsRequest().withBucketName(bucketName); do { listing = client.listObjects(listObjectsRequest); for (S3ObjectSummary object : listing.getObjectSummaries()) files.put(object.getKey(), object.getETag()); } while (listing.isTruncated()); this.console.write("Found " + files.size() + " remote files"); //$NON-NLS-1$ //$NON-NLS-2$ return files; } private Map<String, Md5FilePair> getLocalFiles(File root, String prefix) throws IOException { this.console.write("Get locale files..."); //$NON-NLS-1$ Map<String, Md5FilePair> files = new TreeMap<>(); getLocalFilesRecursive(root, files, prefix); this.console.write("Found " + files.size() + " locale files"); //$NON-NLS-1$ //$NON-NLS-2$ return files; } private void getLocalFilesRecursive(File folder, Map<String, Md5FilePair> list, String prefix) throws IOException { for (File file : folder.listFiles()) { if (file.isFile()) { String key = prefix + file.getName(); list.put(key, new Md5FilePair(file)); } else if (file.isDirectory()) { getLocalFilesRecursive(file, list, prefix + file.getName() + SLASH); } } } private static String getMD5Checksum(File file) throws IOException { FileInputStream fis = new FileInputStream(file); return DigestUtils.md5Hex(fis); } private String addSlash(String prefix) { if (!prefix.endsWith(SLASH)) return prefix += SLASH; return prefix; } /** * A locale helper class to put a file, togehter with its MD5 hash into a key-value map. */ private static class Md5FilePair { final File file; final String md5; Md5FilePair(File file) throws IOException { this.file = file; this.md5 = getMD5Checksum(file); } } }