package jdrivesync.sync; import com.google.api.client.util.DateTime; import jdrivesync.cli.Options; import jdrivesync.exception.JDriveSyncException; import jdrivesync.fs.FileSystemAdapter; import jdrivesync.fs.FileSystemWalker; import jdrivesync.gdrive.GoogleDriveAdapter; import jdrivesync.gdrive.GoogleDriveWalker; import jdrivesync.logging.LoggerFactory; import jdrivesync.model.SyncDirectory; import jdrivesync.model.SyncFile; import jdrivesync.model.SyncItem; import jdrivesync.report.ReportEntry; import jdrivesync.report.ReportFactory; import jdrivesync.walker.WalkerVisitor; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; public class Synchronization { private static final Logger LOGGER = LoggerFactory.getLogger(); public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); private GoogleDriveAdapter googleDriveAdapter; private FileSystemAdapter fileSystemAdapter; private final Options options; public Synchronization(GoogleDriveAdapter googleDriveAdapter, FileSystemAdapter fileSystemAdapter, Options options) { this.googleDriveAdapter = googleDriveAdapter; this.fileSystemAdapter = fileSystemAdapter; this.options = options; } public void syncUp(final Options options) { FileSystemWalker fileSystemWalker = new FileSystemWalker(options, fileSystemAdapter); fileSystemWalker.walk(new WalkerVisitor() { @Override public WalkerVisitorResult visitDirectory(SyncDirectory syncDirectory) { try { WalkerVisitorResult result = WalkerVisitorResult.Continue; LOGGER.log(Level.FINE, "visitDirectory (absolute path: '" + syncDirectory.getLocalFile().get().getAbsolutePath() + "';relative path: '" + syncDirectory.getPath() + "')"); String parentId = determineParentId(syncDirectory); if (parentId != null) { List<com.google.api.services.drive.model.File> children = googleDriveAdapter.listChildren(parentId); for (com.google.api.services.drive.model.File remoteChild : children) { try { String title = remoteChild.getTitle(); SyncItem syncItemFound = null; Iterator<SyncItem> childrenIterator = syncDirectory.getChildrenIterator(); while (childrenIterator.hasNext()) { SyncItem syncItem = childrenIterator.next(); String name = syncItem.getLocalFile().get().getName(); if (title.equals(name)) { syncItemFound = processRemoteChildFound(remoteChild, syncItem); break; } } if (syncItemFound == null) { processRemoteChildNotFound(remoteChild, syncDirectory); } } catch (Exception e) { LOGGER.log(Level.WARNING, "Skipping file/directory '" + syncDirectory.getPath() + "' because an exception occurred: " + e.getMessage(), e); ReportFactory.getInstance(options).log(new ReportEntry(syncDirectory.getPath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped, e.getMessage())); } } processLocalFilesWithoutRemoteFile(syncDirectory); } else { result = WalkerVisitorResult.SkipSubtree; } return result; } catch (Exception e) { if (e instanceof JDriveSyncException) { JDriveSyncException jDriveSyncException = (JDriveSyncException) e; if (jDriveSyncException.getReason() == JDriveSyncException.Reason.InvalidRemoteRootDirectory) { throw jDriveSyncException; } } LOGGER.log(Level.WARNING, "Skipping directory '" + syncDirectory.getPath() + "' because an exception occurred: " + e.getMessage(), e); ReportFactory.getInstance(options).log(new ReportEntry(syncDirectory.getPath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped, e.getMessage())); return WalkerVisitorResult.SkipSubtree; } } private void processLocalFilesWithoutRemoteFile(SyncDirectory syncDirectory) { Iterator<SyncItem> childrenIterator = syncDirectory.getChildrenIterator(); while (childrenIterator.hasNext()) { SyncItem syncItem = childrenIterator.next(); try { if (!syncItem.getRemoteFile().isPresent()) { if (syncItem instanceof SyncFile) { LOGGER.log(Level.FINE, "Storing new file '" + syncItem.getPath() + "'."); googleDriveAdapter.store((SyncFile) syncItem); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Created)); } else if (syncItem instanceof SyncDirectory) { LOGGER.log(Level.FINE, "Storing new directory '" + syncItem.getPath() + "'."); googleDriveAdapter.store((SyncDirectory) syncItem); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Created)); } else { LOGGER.log(Level.FINE, "Type of syncItem is not supported: " + syncItem.getClass().getName()); } } } catch (Exception e) { LOGGER.log(Level.WARNING, "Skipping file/directory '" + syncItem.getPath() + "' because an exception occurred: " + e.getMessage(), e); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped, e.getMessage())); } } } private void processRemoteChildNotFound(com.google.api.services.drive.model.File remoteChild, SyncDirectory syncDirectory) { LOGGER.log(Level.FINE, "Deleting remote file/directory '" + remoteChild.getTitle() + "' because locally it does not exist any more."); if (googleDriveAdapter.isDirectory(remoteChild)) { googleDriveAdapter.deleteDirectory(remoteChild); ReportFactory.getInstance(options).log(new ReportEntry(syncDirectory.getPath() + "/" + remoteChild.getTitle(), ReportEntry.Status.Synchronized, ReportEntry.Action.Deleted)); } else { if (!googleDriveAdapter.isGoogleAppsDocument(remoteChild)) { googleDriveAdapter.deleteFile(remoteChild); ReportFactory.getInstance(options).log(new ReportEntry(syncDirectory.getPath() + "/" + remoteChild.getTitle(), ReportEntry.Status.Synchronized, ReportEntry.Action.Deleted)); } } } private SyncItem processRemoteChildFound(com.google.api.services.drive.model.File remoteChild, SyncItem syncItem) throws IOException { SyncItem syncItemFound = null; if (googleDriveAdapter.isDirectory(remoteChild)) { if (!(syncItem instanceof SyncDirectory)) { LOGGER.log(Level.FINE, "Deleting remote directory '" + remoteChild.getTitle() + "' because locally it is a file (" + syncItem.getPath() + ")."); googleDriveAdapter.deleteDirectory(remoteChild); if (syncItem instanceof SyncFile) { SyncFile syncFile = (SyncFile) syncItem; googleDriveAdapter.store(syncFile); syncItem.setRemoteFile(Optional.of(remoteChild)); syncItemFound = syncItem; ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Created)); } } else { syncItem.setRemoteFile(Optional.of(remoteChild)); syncItemFound = syncItem; ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Unchanged)); } } else { if (syncItem instanceof SyncDirectory) { if (!googleDriveAdapter.isGoogleAppsDocument(remoteChild)) { LOGGER.log(Level.FINE, "Deleting remote file '" + remoteChild.getTitle() + "' because locally it is a directory (" + syncItem.getPath() + ")."); googleDriveAdapter.deleteFile(remoteChild); googleDriveAdapter.store((SyncDirectory) syncItem); syncItem.setRemoteFile(Optional.of(remoteChild)); syncItemFound = syncItem; ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Created)); } } else { syncItem.setRemoteFile(Optional.of(remoteChild)); syncItemFound = syncItem; File localFile = syncItemFound.getLocalFile().get(); if (options.isUseChecksum()) { performChecksumCheck(syncItemFound, localFile, false); } else { BasicFileAttributes attr = Files.readAttributes(localFile.toPath(), BasicFileAttributes.class); FileTime modifiedDateLocal = attr.lastModifiedTime(); DateTime modifiedDateRemote = remoteChild.getModifiedDate(); long sizeLocal = attr.size(); Long sizeRemote = remoteChild.getFileSize() == null ? 0L : remoteChild.getFileSize(); if (!datesAreEqual(modifiedDateLocal.toMillis(), modifiedDateRemote.getValue(), syncItem)) { LOGGER.log(Level.FINE, "Last modification dates are not equal for file '" + syncItemFound.getPath() + "' (local: " + DATE_FORMAT.format(new Date(modifiedDateLocal.toMillis())) + "; remote: " + DATE_FORMAT.format(new Date(modifiedDateRemote.getValue())) + "). Checking MD5 checksums."); performChecksumCheck(syncItemFound, localFile, true); } else if(sizeLocal != sizeRemote) { LOGGER.log(Level.FINE, "File sizes are not equal for file '" + syncItemFound.getPath() + "' (local: " + sizeLocal + "; remote: " + sizeRemote + "). Checking MD5 checksums."); performChecksumCheck(syncItemFound, localFile, true); } else { LOGGER.log(Level.FINE, "Last modification dates and sizes are equal for file '" + syncItemFound.getPath() + "' (local: " + DATE_FORMAT.format(new Date(modifiedDateLocal.toMillis())) + ", " + sizeLocal + " bytes; remote: " + DATE_FORMAT.format(new Date(modifiedDateRemote.getValue())) + ", " + sizeRemote + " bytes). Not updating file."); ReportFactory.getInstance(options).log(new ReportEntry(syncItemFound.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Unchanged)); } } } } return syncItemFound; } private String determineParentId(SyncDirectory syncDirectory) { String parentId = "root"; if (syncDirectory.isRootDirectory()) { LOGGER.log(Level.FINE, "Getting remote file for root directory."); com.google.api.services.drive.model.File rootFile = googleDriveAdapter.getFile(parentId); if (options.getRemoteRootDir().isPresent()) { rootFile = getRootFileForRemotePath(rootFile, options.getRemoteRootDir().get()); parentId = rootFile.getId(); } syncDirectory.setRemoteFile(Optional.of(rootFile)); } else { Optional<com.google.api.services.drive.model.File> remoteFileOptional = syncDirectory.getRemoteFile(); if (remoteFileOptional.isPresent()) { parentId = remoteFileOptional.get().getId(); } else { googleDriveAdapter.store(syncDirectory); ReportFactory.getInstance(options).log(new ReportEntry(syncDirectory.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Created)); remoteFileOptional = syncDirectory.getRemoteFile(); if (remoteFileOptional.isPresent()) { parentId = remoteFileOptional.get().getId(); } else { LOGGER.log(Level.FINE, "Skipping directory '" + syncDirectory.getPath() + "' because remoteFile is not set."); return null; } } } return parentId; } private com.google.api.services.drive.model.File getRootFileForRemotePath(com.google.api.services.drive.model.File rootFile, String remoteRootDir) { remoteRootDir = remoteRootDir.trim(); remoteRootDir = remoteRootDir.replace("\\", "/"); if (remoteRootDir.startsWith("/")) { remoteRootDir = remoteRootDir.substring(1, remoteRootDir.length()); } LOGGER.log(Level.FINE, "Trying to find remote folder '" + remoteRootDir + "'."); String[] remoteDirectories = remoteRootDir.split("/"); com.google.api.services.drive.model.File currentRemoteDir = rootFile; for (String remoteDirectory : remoteDirectories) { if (remoteDirectory.length() == 0) { continue; } com.google.api.services.drive.model.File foundRemoteDir = null; List<com.google.api.services.drive.model.File> remoteChildren = googleDriveAdapter.listChildren(currentRemoteDir.getId()); for (com.google.api.services.drive.model.File remoteChild : remoteChildren) { if (remoteDirectory.equals(remoteChild.getTitle())) { if (googleDriveAdapter.isDirectory(remoteChild)) { foundRemoteDir = remoteChild; } else { throw new JDriveSyncException(JDriveSyncException.Reason.InvalidRemoteRootDirectory, "The remote path '" + remoteRootDir + "' does point to a file but not to a directory."); } } } if (foundRemoteDir == null) { throw new JDriveSyncException(JDriveSyncException.Reason.InvalidRemoteRootDirectory, "The remote path '" + remoteRootDir + "' does not exist."); } else { currentRemoteDir = foundRemoteDir; LOGGER.log(Level.FINE, "Found remote folder '" + remoteDirectory + "'."); } } return currentRemoteDir; } private void performChecksumCheck(SyncItem syncItemFound, File localFile, boolean updateMetadata) { com.google.api.services.drive.model.File remoteFile = syncItemFound.getRemoteFile().get(); String md5ChecksumLocal = computeMd5Checksum(localFile); String md5ChecksumRemote = remoteFile.getMd5Checksum(); if (!md5ChecksumLocal.equals(md5ChecksumRemote)) { if (!googleDriveAdapter.isGoogleAppsDocument(remoteFile)) { LOGGER.log(Level.FINE, "MD5 checksums are not equal for file '" + syncItemFound.getPath() + "' (local: " + md5ChecksumLocal + "; remote: " + md5ChecksumRemote + "). Updating file."); googleDriveAdapter.updateFile(syncItemFound); ReportFactory.getInstance(options).log(new ReportEntry(syncItemFound.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Updated)); } } else { if (!updateMetadata) { LOGGER.log(Level.FINE, "MD5 checksums are equal for file '" + syncItemFound.getPath() + "' (local: " + md5ChecksumLocal + "; remote: " + md5ChecksumRemote + "). Not updating file."); ReportFactory.getInstance(options).log(new ReportEntry(syncItemFound.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Unchanged)); } else { if (!googleDriveAdapter.isGoogleAppsDocument(remoteFile)) { LOGGER.log(Level.FINE, "MD5 checksums are equal for file '" + syncItemFound.getPath() + "' (local: " + md5ChecksumLocal + "; remote: " + md5ChecksumRemote + "). Updating metadata of remote file."); googleDriveAdapter.updateMetadata(syncItemFound); ReportFactory.getInstance(options).log(new ReportEntry(syncItemFound.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.UpdatedMetadata)); } } } } }); } private String computeMd5Checksum(File file) { try (FileInputStream fis = new FileInputStream(file)) { MessageDigest m = MessageDigest.getInstance("MD5"); byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { m.update(buffer, 0, bytesRead); } byte[] digest = m.digest(); BigInteger bigInt = new BigInteger(1, digest); String md5String = bigInt.toString(16); while (md5String.length() < 32) { md5String = "0" + md5String; } return md5String; } catch (NoSuchAlgorithmException e) { throw new JDriveSyncException(JDriveSyncException.Reason.NoSuchAlgorithmException, "Could not load MD5 implementation: " + e.getMessage(), e); } catch (Exception e) { throw new JDriveSyncException(JDriveSyncException.Reason.IOException, "Could not compute MD5 hash for file '" + file.getAbsolutePath() + "': " + e.getMessage(), e); } } private boolean datesAreEqual(long localMillis, long remoteMillis, SyncItem syncItem) { boolean equals = Math.abs(localMillis - remoteMillis) <= options.getLastModificationDateThreshold(); if (!equals) { if(LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Last modification dates for file '" + syncItem.getPath() + "' are not equal (local: " + DATE_FORMAT.format(new Date(localMillis)) + ", remote: " + DATE_FORMAT.format(new Date(remoteMillis)) + ")."); } } else { if(LOGGER.isLoggable(Level.FINER)) { LOGGER.log(Level.FINER, "Last modification dates for file '" + syncItem.getPath() + "' are equal (local: " + DATE_FORMAT.format(new Date(localMillis)) + ", remote: " + DATE_FORMAT.format(new Date(remoteMillis)) + ")."); } } return equals; } public void syncDown(Options options) { GoogleDriveWalker googleDriveWalker = new GoogleDriveWalker(options, googleDriveAdapter); googleDriveWalker.walk(new WalkerVisitor() { @Override public WalkerVisitorResult visitDirectory(SyncDirectory syncDirectory) { WalkerVisitor.WalkerVisitorResult result = WalkerVisitor.WalkerVisitorResult.Continue; LOGGER.log(Level.FINE, "visitDirectory() " + syncDirectory.getPath() + "."); if (syncDirectory.getLocalFile().isPresent()) { File localFile = syncDirectory.getLocalFile().get(); File[] files = fileSystemAdapter.listFiles(localFile); if (files != null) { for (File file : files) { SyncItem syncItemFound = null; Iterator<SyncItem> childrenIterator = syncDirectory.getChildrenIterator(); while (childrenIterator.hasNext()) { SyncItem syncItem = childrenIterator.next(); if (syncItem.getRemoteFile().isPresent() && syncItem.getRemoteFile().get().getTitle().equals(file.getName())) { syncItemFound = syncItem; result = processRemoteChildFound(file, syncItem); break; } } if (syncItemFound == null) { processRemoteChildNotFound(file); } } } processRemoteFilesWithoutLocalFile(syncDirectory); } else { LOGGER.log(Level.FINE, "Skipping directory " + syncDirectory.getPath() + " because local file is not present."); ReportFactory.getInstance(options).log(new ReportEntry(syncDirectory.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Unchanged)); } return result; } private void processRemoteFilesWithoutLocalFile(SyncDirectory syncDirectory) { if (syncDirectory.getLocalFile().isPresent()) { File fileDirectory = syncDirectory.getLocalFile().get(); Iterator<SyncItem> childrenIterator = syncDirectory.getChildrenIterator(); while (childrenIterator.hasNext()) { SyncItem syncItem = childrenIterator.next(); if (!syncItem.getLocalFile().isPresent()) { com.google.api.services.drive.model.File remoteFile = syncItem.getRemoteFile().get(); if (googleDriveAdapter.isDirectory(remoteFile)) { File newDirectory = new File(fileDirectory, remoteFile.getTitle()); try { createLocalDir(newDirectory, syncItem, remoteFile); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Created)); } catch (Exception e) { LOGGER.log(Level.WARNING, "Could not create local directory '" + newDirectory.getAbsolutePath() + "': " + e.getMessage(), e); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped, e.getMessage())); } } else { LOGGER.log(Level.FINE, "Downloading file '" + syncItem.getPath() + "'."); try { InputStream stream = googleDriveAdapter.downloadFile(syncItem); fileSystemAdapter.storeFile(stream, syncItem); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Created)); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failed to store file '" + syncItem.getPath() + "': " + e.getMessage()); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped)); } } } } } else { LOGGER.log(Level.FINE, "Cannot process missing local files because local directory '" + syncDirectory.getPath() + "' is missing."); ReportFactory.getInstance(options).log(new ReportEntry(syncDirectory.getPath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped)); } } private void processRemoteChildNotFound(File file) { if (fileSystemAdapter.isDirectory(file)) { LOGGER.log(Level.FINE, "Deleting local directory '" + file.getAbsolutePath() + "' because it does not exist remote."); try { fileSystemAdapter.deleteDirectorySubtree(file.toPath()); ReportFactory.getInstance(options).log(new ReportEntry(file.getAbsolutePath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Deleted)); } catch (Exception e) { LOGGER.log(Level.WARNING, "Could not delete directory '" + file.getAbsolutePath() + "': " + e.getMessage(), e); ReportFactory.getInstance(options).log(new ReportEntry(file.getAbsolutePath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped, e.getMessage())); } } else { LOGGER.log(Level.FINE, "Deleting local file '" + file.getAbsolutePath() + "' because it does not exist remote."); boolean deleted = fileSystemAdapter.delete(file); if (!deleted) { LOGGER.log(Level.WARNING, "Could not delete file '" + file.getAbsolutePath() + "':."); ReportFactory.getInstance(options).log(new ReportEntry(file.getAbsolutePath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped, "Could not delete file.")); } else { ReportFactory.getInstance(options).log(new ReportEntry(file.getAbsolutePath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Deleted)); } } } private WalkerVisitor.WalkerVisitorResult processRemoteChildFound(File file, SyncItem syncItem) { com.google.api.services.drive.model.File remoteFile = syncItem.getRemoteFile().get(); if (googleDriveAdapter.isDirectory(remoteFile)) { if (fileSystemAdapter.isDirectory(file)) { syncItem.setLocalFile(Optional.of(file)); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Unchanged)); } else { LOGGER.log(Level.FINE, "Deleting local file '" + file.getAbsolutePath() + " because remote it is a directory."); boolean deleted = fileSystemAdapter.delete(file); if (!deleted) { ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped, "Deleting local file failed.")); throw new JDriveSyncException(JDriveSyncException.Reason.IOException, "Could not delete local directory '" + file.getAbsolutePath() + "'."); } try { createLocalDir(file, syncItem, remoteFile); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Created)); } catch (Exception e) { LOGGER.log(Level.WARNING, "Skipping directory '" + syncItem.getPath() + "' because creation of local directory failed: " + e.getMessage(), e); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped, e.getMessage())); return WalkerVisitorResult.SkipSubtree; } } } else { if (fileSystemAdapter.isDirectory(file)) { LOGGER.log(Level.FINE, "Deleting local directory '" + syncItem.getPath() + "' because remote it is a file."); boolean deleted = fileSystemAdapter.delete(file); if (!deleted) { ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped, "Deleting local file failed.")); throw new JDriveSyncException(JDriveSyncException.Reason.IOException, "Could not delete local directory '" + file.getAbsolutePath() + "'."); } LOGGER.log(Level.FINE, "Downloading file '" + syncItem.getPath() + "'."); try { InputStream stream = googleDriveAdapter.downloadFile(syncItem); fileSystemAdapter.storeFile(stream, syncItem); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Created)); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failed to store file '" + syncItem.getPath() + "': " + e.getMessage()); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped)); } } else { if (options.isUseChecksum()) { performChecksumCheck(file, syncItem, remoteFile, false); } else { DateTime remoteFileModifiedDate = remoteFile.getModifiedDate(); try { BasicFileAttributes attr = fileSystemAdapter.readAttributes(file); FileTime localLastModifiedTime = attr.lastModifiedTime(); long sizeLocal = attr.size(); long sizeRemote = remoteFile.getFileSize() == null ? 0L : remoteFile.getFileSize(); if (!datesAreEqual(localLastModifiedTime.toMillis(), remoteFileModifiedDate.getValue(), syncItem)) { LOGGER.log(Level.FINE, "Last modification dates are not equal for file '" + syncItem.getPath() + "' (local: " + DATE_FORMAT.format(new Date(localLastModifiedTime.toMillis())) + "; remote: " + DATE_FORMAT.format(new Date(remoteFileModifiedDate.getValue())) + "). Checking MD5 checksums."); performChecksumCheck(file, syncItem, remoteFile, true); } else if(sizeLocal != sizeRemote) { LOGGER.log(Level.FINE, "File sizes are not equal for file '" + syncItem.getPath() + "' (local: " + sizeLocal + "; remote: " + sizeRemote + "). Checking MD5 checksums."); performChecksumCheck(file, syncItem, remoteFile, true); } else { syncItem.setLocalFile(Optional.of(file)); LOGGER.log(Level.FINE, "Last modification dates and sizes are equal for file '" + syncItem.getPath() + "' (local: " + DATE_FORMAT.format(new Date(localLastModifiedTime.toMillis())) + ", " + sizeLocal + " bytes; remote: " + DATE_FORMAT.format(new Date(remoteFileModifiedDate.getValue())) + ", " + sizeRemote + " bytes). Not updating file."); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Unchanged)); } } catch (Exception e) { LOGGER.log(Level.FINE, "Skipping file '" + syncItem.getPath() + " because reading local file attributes failed: " + e.getMessage(), e); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped, e.getMessage())); } } } } return WalkerVisitorResult.Continue; } private void createLocalDir(File newDirectory, SyncItem syncItem, com.google.api.services.drive.model.File remoteFile) throws IOException { LOGGER.log(Level.FINE, "Creating local directory '" + syncItem.getPath() + "'."); Path newLocalDir = fileSystemAdapter.createDirectory(newDirectory); DateTime lastModifiedDateTime = remoteFile.getModifiedDate(); fileSystemAdapter.setLastModifiedTime(newLocalDir.toFile(), lastModifiedDateTime.getValue()); syncItem.setLocalFile(Optional.of(newLocalDir.toFile())); } private void performChecksumCheck(File file, SyncItem syncItem, com.google.api.services.drive.model.File remoteFile, boolean updateMetadata) { String remoteFileMd5Checksum = remoteFile.getMd5Checksum(); String localFileMd5Checksum = computeMd5Checksum(file); if (remoteFileMd5Checksum.equals(localFileMd5Checksum)) { syncItem.setLocalFile(Optional.of(file)); if (!updateMetadata) { LOGGER.log(Level.FINE, "Not downloading file '" + syncItem.getPath() + "' because MD5 checksums are equal (local: " + localFileMd5Checksum + ", remote: " + remoteFileMd5Checksum + ")."); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Unchanged)); } else { try { fileSystemAdapter.setLastModifiedTime(file, remoteFile.getModifiedDate().getValue()); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.UpdatedMetadata)); } catch (Exception e) { LOGGER.log(Level.WARNING, "Could not update last modification date of local file '" + file.getAbsolutePath() + "':" + e.getMessage(), e); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped, e.getMessage())); } } } else { LOGGER.log(Level.FINE, "Downloading file '" + syncItem.getPath() + "' because MD5 checksums are not equal (local: " + localFileMd5Checksum + ", remote: " + remoteFileMd5Checksum + ")."); try { InputStream stream = googleDriveAdapter.downloadFile(syncItem); fileSystemAdapter.storeFile(stream, syncItem); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Synchronized, ReportEntry.Action.Updated)); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failed to store file '" + syncItem.getPath() + "': " + e.getMessage()); ReportFactory.getInstance(options).log(new ReportEntry(syncItem.getPath(), ReportEntry.Status.Error, ReportEntry.Action.Skipped)); } } } }); } }