package cloudsync.helper; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.Reader; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.FileTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import cloudsync.exceptions.FileIOException; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVRecord; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import cloudsync.connector.LocalFilesystemConnector; import cloudsync.connector.RemoteConnector; import cloudsync.exceptions.CloudsyncException; import cloudsync.model.options.FileErrorType; import cloudsync.model.options.ExistingType; import cloudsync.model.Item; import cloudsync.model.ItemType; import cloudsync.model.options.FollowLinkType; import cloudsync.model.options.PermissionType; import cloudsync.model.RemoteItem; import cloudsync.model.LocalStreamData; import cloudsync.model.RemoteStreamData; import cloudsync.model.options.SyncType; public class Handler { private final static Logger LOGGER = Logger.getLogger(Handler.class.getName()); private final String name; private final LocalFilesystemConnector localConnection; private final RemoteConnector remoteConnection; private final Crypt crypt; private final Item root; private final List<Item> duplicates; private final ExistingType existingFlag; private final FollowLinkType followlinks; private final PermissionType permissionType; private Path cacheFilePath; private Path lockFilePath; private Path pidFilePath; private boolean pidCleanup = false; private boolean isLocked = false; private final FileErrorType fileErrorBehavior; class Status { private int create = 0; private int update = 0; private int remove = 0; private int skip = 0; } public Handler(String name, final LocalFilesystemConnector localConnection, final RemoteConnector remoteConnection, final Crypt crypt, final ExistingType existingFlag, final FollowLinkType followlinks, final PermissionType permissionType, final FileErrorType fileErrorBehavior) { this.name = name; this.localConnection = localConnection; this.remoteConnection = remoteConnection; this.crypt = crypt; this.existingFlag = existingFlag; this.followlinks = followlinks; this.permissionType = permissionType; this.fileErrorBehavior = fileErrorBehavior; root = Item.getDummyRoot(); duplicates = new ArrayList<>(); } public void init(SyncType synctype, String cacheFile, String lockFile, String pidFile, boolean nocache, boolean forcestart) throws CloudsyncException { cacheFilePath = Paths.get(cacheFile.replace("{name}", name)); lockFilePath = Paths.get(lockFile.replace("{name}", name)); pidFilePath = Paths.get(pidFile.replace("{name}", name)); if (synctype.checkPID()) { if (!forcestart && Files.exists(pidFilePath, LinkOption.NOFOLLOW_LINKS)) { throw new CloudsyncException( "Other job is running or previous job has crashed. If you are sure that no other job is running use the option '--forcestart'"); } RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean(); String jvmName = bean.getName(); long pid = Long.valueOf(jvmName.split("@")[0]); try { Files.write(pidFilePath, Long.toString(pid).getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); pidCleanup = true; } catch (IOException e) { throw new CloudsyncException("Couldn't create '" + pidFilePath.toString() + "'"); } } if (Files.exists(lockFilePath, LinkOption.NOFOLLOW_LINKS)) { LOGGER.log(Level.WARNING, "Found an inconsistent cache file state. Possibly previous job has crashed or duplicate files was detected. Force a cache file rebuild."); nocache = true; } if (!nocache && Files.exists(cacheFilePath, LinkOption.NOFOLLOW_LINKS)) { LOGGER.log(Level.INFO, "load structure from cache file"); readCSVStructure(cacheFilePath); } else { LOGGER.log(Level.INFO, "load structure from remote server"); createLock(); readRemoteStructure(root); } releaseLock(); } @Override public void finalize() throws CloudsyncException { try { if (pidCleanup) Files.delete(pidFilePath); } catch (IOException e) { throw new CloudsyncException("Couldn't remove '" + pidFilePath.toString() + "'"); } } private void createLock() throws CloudsyncException { if (isLocked) return; try { if (!Files.exists(lockFilePath, LinkOption.NOFOLLOW_LINKS)) { Files.createFile(lockFilePath); } } catch (IOException e) { throw new CloudsyncException("Couldn't create '" + lockFilePath.toString() + "'"); } isLocked = true; } private void releaseLock() throws CloudsyncException { if (!isLocked || duplicates.size() > 0) return; try { Files.delete(lockFilePath); } catch (IOException e) { throw new CloudsyncException("Couldn't remove '" + lockFilePath.toString() + "'"); } try { if (root.getChildren().size() > 0) { LOGGER.log(Level.INFO, "write structure to cache file"); final PrintWriter out = new PrintWriter(cacheFilePath.toFile()); final CSVPrinter csvOut = new CSVPrinter(out, CSVFormat.EXCEL); writeStructureToCSVPrinter(csvOut, root); out.close(); } } catch (final IOException e) { throw new CloudsyncException("Can't write cache file on '" + cacheFilePath.toString() + "'", e); } isLocked = false; } private void writeStructureToCSVPrinter(final CSVPrinter out, final Item parentItem) throws IOException { for (final Item child : parentItem.getChildren().values()) { out.printRecord(Arrays.asList(child.toCSVArray())); if (child.isType(ItemType.FOLDER)) { writeStructureToCSVPrinter(out, child); } } } private void readCSVStructure(final Path cacheFilePath) throws CloudsyncException { final Map<String, Item> mapping = new HashMap<>(); mapping.put("", root); try { final Reader in = new FileReader(cacheFilePath.toFile()); final Iterable<CSVRecord> records = CSVFormat.EXCEL.parse(in); for (final CSVRecord record : records) { final Item item = Item.fromCSV(record); final String childPath = Helper.trim(record.get(0), Item.SEPARATOR); final String parentPath = childPath.length() == item.getName().length() ? "" : StringUtils.removeEnd(FilenameUtils.getPath(childPath), Item.SEPARATOR); mapping.put(childPath, item); // System.out.println(parentPath+":"+item.getName()); Item parent = mapping.get(parentPath); item.setParent(parent); parent.addChild(item); } } catch (final IOException e) { throw new CloudsyncException("Can't read cache from file '" + cacheFilePath.toString() + "'", e); } } private void readRemoteStructure(final Item parentItem) throws CloudsyncException { Map<ItemType, Integer> status = new HashMap<>(); readRemoteStructure(parentItem, status); if (status.size() > 0) LOGGER.log(Level.INFO, formatRemoteStatus(status)); } private void readRemoteStructure(final Item parentItem, Map<ItemType, Integer> status) throws CloudsyncException { final List<RemoteItem> childItems = remoteConnection.readFolder(this, parentItem); for (final RemoteItem childItem : childItems) { childItem.setParent(parentItem); final RemoteItem existingChildItem = (RemoteItem) parentItem.getChildByName(childItem.getName()); if (existingChildItem != null) { LOGGER.log(Level.WARNING, "found duplicate: '" + childItem.getPath()); String msg = ""; if (childItem.getRemoteFilesize() != null) msg += " " + childItem.getRemoteFilesize(); if (existingChildItem.getRemoteFilesize() != null) msg += " [" + existingChildItem.getRemoteFilesize() + "]"; if (!StringUtils.isEmpty(msg)) LOGGER.log(Level.WARNING, " size: " + msg); LOGGER.log(Level.WARNING, " created: " + childItem.getRemoteCreationTime() + " [" + existingChildItem.getRemoteCreationTime() + "]"); // if childItem is newer if (existingChildItem.getRemoteCreationTime().compareTo( childItem.getRemoteCreationTime() ) < 0 ) { parentItem.addChild(childItem); duplicates.add(existingChildItem); } else { duplicates.add(childItem); } putRemoteStatus(status, ItemType.DUPLICATE); } else { parentItem.addChild(childItem); } if (status.size() > 0) LOGGER.log(Level.INFO, "\r " + formatRemoteStatus(status), true); putRemoteStatus(status, childItem.getType()); if (childItem.isType(ItemType.FOLDER)) { readRemoteStructure(childItem, status); } } } private void putRemoteStatus(Map<ItemType, Integer> status, ItemType type) { Integer count = status.get(type); if (count == null) count = 0; count++; status.put(type, count); } private String formatRemoteStatus(Map<ItemType, Integer> status) { List<String> typeStatus = new ArrayList<>(); for (ItemType type : ItemType.values()) { Integer count = status.get(type); if (count == null) continue; typeStatus.add(count + " " + type.getName(count)); } String lastType = typeStatus.remove(typeStatus.size() - 1); String message = StringUtils.join(typeStatus, ", "); if (message.length() > 0) { message += " and "; } message += lastType; return "found " + message; } private void checkDuplications() throws CloudsyncException { if (duplicates.size() > 0) { String message = "found " + duplicates.size() + " duplicate item" + (duplicates.size() == 1 ? "" : "s") + ":\n\n"; final List<Item> list = new ArrayList<>(); for (final Item item : duplicates) { list.addAll(_flatRecursiveChildren(item)); } for (final Item item : list) { message += " " + item.getRemoteIdentifier() + " - " + item.getPath() + "\n"; } message += "\n try to run with '--clean=<path>'"; throw new CloudsyncException(message); } } private boolean checkPattern(String path, String[] includePatterns, String[] excludePatterns) { if (includePatterns != null) { boolean found = false; for (String includePattern : includePatterns) { if (path.matches("^" + includePattern + "$")) { found = true; break; } } if (!found) return false; } if (excludePatterns != null) { for (String excludePattern : excludePatterns) { if (path.matches("^" + excludePattern + "$")) { return false; } } } return true; } public void clean() throws CloudsyncException { if (duplicates.size() > 0) { final List<Item> list = new ArrayList<>(); for (final Item item : duplicates) { list.addAll(_flatRecursiveChildren(item)); } for (final Item item : list) { localConnection.prepareUpload(this, item, ExistingType.RENAME); LOGGER.log(Level.FINE, "restore " + item.getTypeName() + " '" + item.getPath() + "'"); localConnection.prepareParent(this, item); localConnection.upload(this, item, ExistingType.RENAME, permissionType); } Collections.reverse(list); for (final Item item : list) { LOGGER.log(Level.FINE, "clean " + item.getTypeName() + " '" + item.getPath() + "'"); remoteConnection.remove(this, item); } duplicates.clear(); releaseLock(); } } public void list(String[] includePatterns, String[] excludePatterns) throws CloudsyncException { checkDuplications(); list(includePatterns, excludePatterns, root); } private void list(String[] includePatterns, String[] excludePatterns, final Item item) { for (final Item child : item.getChildren().values()) { String path = child.getPath(); if (!checkPattern(path, includePatterns, excludePatterns)) continue; LOGGER.log(Level.INFO, path); if (child.isType(ItemType.FOLDER)) { list(includePatterns, excludePatterns, child); } } } public void restore(final boolean dryRun, String[] includePatterns, String[] excludePatterns) throws CloudsyncException { checkDuplications(); restore(dryRun, includePatterns, excludePatterns, root); } private void restore(final boolean dryRun, String[] includePatterns, String[] excludePatterns, final Item item) throws CloudsyncException { for (final Item child : item.getChildren().values()) { String path = child.getPath(); if (checkPattern(path, includePatterns, excludePatterns)) { localConnection.prepareUpload(this, child, existingFlag); LOGGER.log(Level.FINE, "restore " + child.getTypeName() + " '" + path + "'"); if (!dryRun) localConnection.upload(this, child, existingFlag, permissionType); } if (child.isType(ItemType.FOLDER)) { restore(dryRun, includePatterns, excludePatterns, child); } } } public void backup(final boolean dryRun, String[] includePatterns, String[] excludePatterns) throws CloudsyncException { checkDuplications(); final Status status = new Status(); backup(dryRun, includePatterns, excludePatterns, root, status); boolean isChanged = isLocked; releaseLock(); if (isChanged) { remoteConnection.cleanHistory(this); } final int total = status.create + status.update + status.skip; LOGGER.log(Level.INFO, "total items: " + (Integer.toString(total))); LOGGER.log(Level.INFO, "created items: " + (Integer.toString(status.create))); LOGGER.log(Level.INFO, "updated items: " + (Integer.toString(status.update))); LOGGER.log(Level.INFO, "removed items: " + (Integer.toString(status.remove))); LOGGER.log(Level.INFO, "skipped items: " + (Integer.toString(status.skip))); } private void backup(final boolean dryRun, String[] includePatterns, String[] excludePatterns, final Item remoteParentItem, final Status status) throws CloudsyncException { final Map<String, Item> unusedRemoteChildItems = remoteParentItem.getChildren(); for (File localChildFile : localConnection.readFolder(remoteParentItem)) { String filePath = localChildFile.getAbsolutePath(); if (!checkPattern(filePath, includePatterns, excludePatterns)) continue; String backupPath = filePath; Item remoteChildItem = null; try { Item localChildItem = localConnection.getItem(localChildFile, followlinks); localChildItem.setParent(remoteParentItem); backupPath = localChildItem.getPath(); remoteChildItem = remoteParentItem.getChildByName(localChildItem.getName()); if (remoteChildItem == null) { remoteChildItem = localChildItem; LOGGER.log(Level.FINE, "create " + remoteChildItem.getTypeName() + " '" + backupPath + "'"); if (!dryRun) { createLock(); remoteConnection.upload(this, remoteChildItem); } remoteParentItem.addChild(remoteChildItem); status.create++; } else { if (remoteChildItem.isTypeChanged(localChildItem)) { LOGGER.log(Level.FINE, "remove " + remoteChildItem.getTypeName() + " '" + backupPath + "'"); if (!dryRun) { createLock(); remoteConnection.remove(this, remoteChildItem); } status.remove++; remoteChildItem = localChildItem; LOGGER.log(Level.FINE, "create " + remoteChildItem.getTypeName() + " '" + backupPath + "'"); if (!dryRun) { createLock(); remoteConnection.upload(this, remoteChildItem); } remoteParentItem.addChild(remoteChildItem); status.create++; } // check filesize and modify time else if (remoteChildItem.isMetadataChanged(localChildItem)) { final boolean isFiledataChanged = localChildItem.isFiledataChanged(remoteChildItem); remoteChildItem.update(localChildItem); List<String> types = new ArrayList<>(); if (isFiledataChanged) types.add("data,attributes"); else if (!isFiledataChanged) types.add("attributes"); if (remoteChildItem.isMetadataFormatChanged()) types.add("format"); LOGGER.log(Level.FINE, "update " + remoteChildItem.getTypeName() + " '" + backupPath + "' [" + StringUtils.join(types, ",") + "]"); if (!dryRun) { createLock(); remoteConnection.update(this, remoteChildItem, isFiledataChanged); } status.update++; } else { status.skip++; } } try { // refresh Metadata Item _localChildItem = localConnection.getItem(localChildFile, followlinks); if (_localChildItem.isMetadataChanged(localChildItem)) { LOGGER.log(Level.WARNING, localChildItem.getTypeName() + " '" + backupPath + "' was changed during update."); } } catch (FileIOException e) { LOGGER.log(Level.WARNING, localChildItem.getTypeName() + " '" + backupPath + "' was removed during update."); } unusedRemoteChildItems.remove(remoteChildItem.getName()); if (remoteChildItem.isType(ItemType.FOLDER)) { backup(dryRun, includePatterns, excludePatterns, remoteChildItem, status); } } catch (FileIOException e) { status.skip++; if(FileErrorType.MESSAGE.equals( fileErrorBehavior)) { LOGGER.log(Level.SEVERE, "Skip '" + backupPath + "'. " + e.getMessage()); if( remoteChildItem != null ) { unusedRemoteChildItems.remove(remoteChildItem.getName()); } } else { throw new CloudsyncException("Skip '" + backupPath + "'", e); } } } for (final Item item : unusedRemoteChildItems.values()) { LOGGER.log(Level.FINE, "remove " + item.getTypeName() + " '" + item.getPath() + "'"); remoteParentItem.removeChild(item); if (!dryRun) { createLock(); remoteConnection.remove(this, item); } status.remove++; } } private List<Item> _flatRecursiveChildren(final Item parentItem) { final List<Item> list = new ArrayList<>(); list.add(parentItem); if (parentItem.isType(ItemType.FOLDER)) { for (final Item childItem : parentItem.getChildren().values()) { list.addAll(_flatRecursiveChildren(childItem)); } } return list; } public Item getRootItem() { return root; } public LocalStreamData getLocalProcessedBinary(final Item item) throws FileIOException { LocalStreamData data = localConnection.getFileBinary(item); if (data != null && crypt != null ) data = crypt.encryptedBinary(item.getName(), data, item); return data; } public String getLocalProcessedMetadata(final Item item) throws FileIOException { String metadata = item.getMetadata(this); return crypt != null ? crypt.encryptText(metadata) : metadata; } public String getLocalProcessedTitle(final Item item) throws FileIOException { return crypt != null ? crypt.encryptText(item.getName()) : item.getName(); } public RemoteItem initRemoteItem(String remoteIdentifier, boolean isFolder, String title, String metadata, Long remoteFilesize, FileTime remoteCreationtime) { return Item.fromMetadata(remoteIdentifier, isFolder, title, metadata, remoteFilesize, remoteCreationtime); } public RemoteStreamData getRemoteProcessedBinary(Item item) throws CloudsyncException { InputStream stream = remoteConnection.get(this, item); if( crypt != null ) { try { return new RemoteStreamData(stream,crypt.decryptData(stream)); } catch(Exception e) { if( stream != null ) IOUtils.closeQuietly(stream); throw e; } } else { return new RemoteStreamData(null,stream); } } public String getProcessedText(final String text) throws CloudsyncException { return crypt != null ? crypt.decryptText(text) : text; } }