001/* 002 * Syncany, www.syncany.org 003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation, either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018package org.syncany.operations.down.actions; 019 020import java.io.File; 021import java.io.FileNotFoundException; 022import java.io.IOException; 023import java.nio.file.Files; 024import java.nio.file.Path; 025import java.nio.file.Paths; 026import java.nio.file.attribute.DosFileAttributes; 027import java.nio.file.attribute.FileTime; 028import java.nio.file.attribute.PosixFilePermission; 029import java.nio.file.attribute.PosixFilePermissions; 030import java.text.SimpleDateFormat; 031import java.util.Date; 032import java.util.Set; 033import java.util.logging.Level; 034import java.util.logging.Logger; 035 036import org.apache.commons.io.FileExistsException; 037import org.apache.commons.io.FileUtils; 038import org.syncany.config.Config; 039import org.syncany.database.FileVersion; 040import org.syncany.database.FileVersion.FileType; 041import org.syncany.database.FileVersionComparator; 042import org.syncany.database.FileVersionComparator.FileChange; 043import org.syncany.database.FileVersionComparator.FileVersionComparison; 044import org.syncany.database.MemoryDatabase; 045import org.syncany.util.CollectionUtil; 046import org.syncany.util.EnvironmentUtil; 047import org.syncany.util.FileUtil; 048import org.syncany.util.NormalizedPath; 049 050/** 051 * File system actions perform operations on the local disk -- creating, updating and 052 * deleting files. Given an expected and a new {@link FileVersion} (namely file1 and file2), 053 * the concrete implementation of a file system action performs an action on the file. 054 * 055 * <p>Implementations of this class treat file1 and file2 differently, depending on what 056 * action they implement. 057 * 058 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 059 */ 060public abstract class FileSystemAction { 061 protected static final Logger logger = Logger.getLogger(FileSystemAction.class.getSimpleName()); 062 063 protected Config config; 064 protected MemoryDatabase winningDatabase; 065 protected FileVersion fileVersion1; 066 protected FileVersion fileVersion2; 067 protected FileVersionComparator fileVersionHelper; 068 069 public FileSystemAction(Config config, MemoryDatabase winningDatabase, FileVersion file1, FileVersion file2) { 070 this.config = config; 071 this.winningDatabase = winningDatabase; 072 this.fileVersion1 = file1; 073 this.fileVersion2 = file2; 074 this.fileVersionHelper = new FileVersionComparator(config.getLocalDir(), config.getChunker().getChecksumAlgorithm()); 075 } 076 077 public FileVersion getFile1() { 078 return fileVersion1; 079 } 080 081 public FileVersion getFile2() { 082 return fileVersion2; 083 } 084 085 public FileType getType() { 086 if (fileVersion1 != null) { 087 return fileVersion1.getType(); 088 } 089 else { 090 return fileVersion2.getType(); 091 } 092 } 093 094 protected void createSymlink(FileVersion reconstructedFileVersion) throws Exception { 095 File reconstructedFileAtFinalLocation = getAbsolutePathFile(reconstructedFileVersion.getPath()); 096 097 if (EnvironmentUtil.symlinksSupported()) { 098 // Make directory if it does not exist 099 File reconstructedFileParentDir = reconstructedFileAtFinalLocation.getParentFile(); 100 101 if (!FileUtil.exists(reconstructedFileParentDir)) { 102 logger.log(Level.INFO, " - Parent folder does not exist, creating " + reconstructedFileParentDir + " ..."); 103 reconstructedFileParentDir.mkdirs(); 104 } 105 106 // Make link 107 logger.log(Level.INFO, 108 " - Creating symlink at " + reconstructedFileAtFinalLocation + " (target: " + reconstructedFileVersion.getLinkTarget() 109 + ") ..."); 110 FileUtil.createSymlink(reconstructedFileVersion.getLinkTarget(), reconstructedFileAtFinalLocation); 111 } 112 else { 113 logger.log(Level.INFO, " - Skipping symlink (not supported) at " + reconstructedFileAtFinalLocation + " (target: " 114 + reconstructedFileVersion.getLinkTarget() + ") ..."); 115 } 116 } 117 118 protected void setLastModified(FileVersion reconstructedFileVersion) { 119 File reconstructedFilesAtFinalLocation = getAbsolutePathFile(reconstructedFileVersion.getPath()); 120 setLastModified(reconstructedFileVersion, reconstructedFilesAtFinalLocation); 121 } 122 123 protected void setLastModified(FileVersion reconstructedFileVersion, File reconstructedFilesAtFinalLocation) { 124 // Using Files.setLastModifiedTime() instead of File.setLastModified() 125 // due to pre-1970 issue. See #374 for details. 126 127 try { 128 FileTime newLastModifiedTime = FileTime.fromMillis(reconstructedFileVersion.getLastModified().getTime()); 129 Files.setLastModifiedTime(reconstructedFilesAtFinalLocation.toPath(), newLastModifiedTime); 130 } 131 catch (IOException e) { 132 logger.log(Level.WARNING, "Warning: Could not set last modified date for file " + reconstructedFilesAtFinalLocation + "; Ignoring error.", e); 133 } 134 } 135 136 protected void moveToConflictFile(FileVersion targetFileVersion) throws IOException { 137 NormalizedPath targetConflictingFile = new NormalizedPath(config.getLocalDir(), targetFileVersion.getPath()); 138 moveToConflictFile(targetConflictingFile); 139 } 140 141 protected void moveToConflictFile(NormalizedPath conflictingPath) throws IOException { 142 if (!FileUtil.exists(conflictingPath.toFile())) { 143 logger.log(Level.INFO, " - Creation of conflict file not necessary. Locally conflicting file vanished from " + conflictingPath); 144 return; 145 } 146 147 int attempts = 0; 148 149 while (attempts++ < 10) { 150 NormalizedPath conflictedCopyPath = null; 151 152 try { 153 conflictedCopyPath = findConflictFilename(conflictingPath); 154 logger.log(Level.INFO, " - Local version conflicts, moving local file " + conflictingPath + " to " + conflictedCopyPath + " ..."); 155 156 if (conflictingPath.toFile().isDirectory()) { 157 FileUtils.moveDirectory(conflictingPath.toFile(), conflictedCopyPath.toFile()); 158 } 159 else { 160 FileUtils.moveFile(conflictingPath.toFile(), conflictedCopyPath.toFile()); 161 } 162 163 // Success! 164 break; 165 } 166 catch (FileExistsException e) { 167 logger.log(Level.SEVERE, " - Cannot create conflict file; attempt = " + attempts + " for file: " + conflictedCopyPath, e); 168 } 169 catch (FileNotFoundException e) { 170 logger.log(Level.INFO, " - Conflict file vanished. Don't care!", e); 171 } 172 catch (Exception e) { 173 throw new RuntimeException("What to do here?", e); 174 } 175 } 176 } 177 178 protected File moveFileToFinalLocation(File reconstructedFileInCache, FileVersion targetFileVersion) throws IOException { 179 NormalizedPath originalPath = new NormalizedPath(config.getLocalDir(), targetFileVersion.getPath()); 180 NormalizedPath targetPath = originalPath; 181 182 try { 183 // Clean filename 184 if (targetPath.hasIllegalChars()) { 185 targetPath = targetPath.toCreatable("filename conflict", true); 186 } 187 188 // Try creating folder 189 createFolder(targetPath.getParent()); 190 } 191 catch (Exception e) { 192 throw new RuntimeException("What to do here?!"); 193 } 194 195 // Try moving file to final destination 196 try { 197 FileUtils.moveFile(reconstructedFileInCache, targetPath.toFile()); 198 } 199 catch (FileExistsException e) { 200 logger.log(Level.FINE, "File already existed", e); 201 moveToConflictFile(targetPath); 202 } 203 catch (Exception e) { 204 throw new RuntimeException("What to do here?!"); 205 } 206 207 return targetPath.toFile(); 208 } 209 210 protected void createFolder(NormalizedPath targetDir) throws Exception { 211 if (!FileUtil.exists(targetDir.toFile())) { 212 logger.log(Level.INFO, " - Creating folder at " + targetDir.toFile() + " ..."); 213 boolean targetDirCreated = targetDir.toFile().mkdirs(); 214 215 if (!targetDirCreated) { 216 throw new Exception("Cannot create target dir: " + targetDir); 217 } 218 } 219 else if (!FileUtil.isDirectory(targetDir.toFile())) { 220 logger.log(Level.INFO, " - Expected a folder at " + targetDir.toFile() + " ..."); 221 moveToConflictFile(targetDir); 222 } 223 } 224 225 private NormalizedPath findConflictFilename(NormalizedPath conflictingPath) throws Exception { 226 String conflictUserName = (config.getDisplayName() != null) ? config.getDisplayName() : config.getMachineName(); 227 boolean conflictUserNameEndsWithS = conflictUserName.endsWith("s"); 228 String conflictDate = new SimpleDateFormat("d MMM yy, h-mm a").format(new Date()); 229 230 String conflictFilenameSuffix; 231 232 if (conflictUserNameEndsWithS) { 233 conflictFilenameSuffix = String.format("%s' conflicted copy, %s", conflictUserName, conflictDate); 234 } 235 else { 236 conflictFilenameSuffix = String.format("%s's conflicted copy, %s", conflictUserName, conflictDate); 237 } 238 239 return conflictingPath.withSuffix(conflictFilenameSuffix, false); 240 } 241 242 protected void setFileAttributes(FileVersion reconstructedFileVersion) throws IOException { 243 File reconstructedFilesAtFinalLocation = getAbsolutePathFile(reconstructedFileVersion.getPath()); 244 setFileAttributes(reconstructedFileVersion, reconstructedFilesAtFinalLocation); 245 } 246 247 protected void setFileAttributes(FileVersion reconstructedFileVersion, File reconstructedFilesAtFinalLocation) throws IOException { 248 if (EnvironmentUtil.isWindows()) { 249 if (reconstructedFileVersion.getDosAttributes() != null) { 250 logger.log(Level.INFO, " - Setting DOS attributes: " + reconstructedFileVersion.getDosAttributes() + " ..."); 251 252 DosFileAttributes dosAttrs = FileUtil.dosAttrsFromString(reconstructedFileVersion.getDosAttributes()); 253 Path filePath = Paths.get(reconstructedFilesAtFinalLocation.getAbsolutePath()); 254 255 try { 256 Files.setAttribute(filePath, "dos:readonly", dosAttrs.isReadOnly()); 257 Files.setAttribute(filePath, "dos:hidden", dosAttrs.isHidden()); 258 Files.setAttribute(filePath, "dos:archive", dosAttrs.isArchive()); 259 Files.setAttribute(filePath, "dos:system", dosAttrs.isSystem()); 260 } 261 catch (IOException e) { 262 logger.log(Level.WARNING, " - WARNING: Cannot set file attributes for " + filePath, e); 263 } 264 } 265 } 266 else if (EnvironmentUtil.isUnixLikeOperatingSystem()) { 267 if (reconstructedFileVersion.getPosixPermissions() != null) { 268 logger.log(Level.INFO, " - Setting POSIX permissions: " + reconstructedFileVersion.getPosixPermissions() + " ..."); 269 270 Set<PosixFilePermission> posixPerms = PosixFilePermissions.fromString(reconstructedFileVersion.getPosixPermissions()); 271 272 Path filePath = Paths.get(reconstructedFilesAtFinalLocation.getAbsolutePath()); 273 274 try { 275 Files.setPosixFilePermissions(filePath, posixPerms); 276 } 277 catch (IOException e) { 278 logger.log(Level.WARNING, " - WARNING: Cannot set file permissions for " + filePath, e); 279 } 280 } 281 } 282 } 283 284 protected boolean fileAsExpected(FileVersion expectedLocalFileVersion) { 285 return fileAsExpected(expectedLocalFileVersion, new FileChange[] {}); 286 } 287 288 protected boolean fileAsExpected(FileVersion expectedLocalFileVersion, FileChange... allowedFileChanges) { 289 FileVersionComparison fileVersionComparison = fileChanges(expectedLocalFileVersion); 290 291 if (fileVersionComparison.areEqual()) { 292 return true; 293 } 294 else if (allowedFileChanges.length > 0) { 295 return CollectionUtil.containsOnly(fileVersionComparison.getFileChanges(), allowedFileChanges); 296 } 297 else { 298 return false; 299 } 300 } 301 302 protected FileVersionComparison fileChanges(FileVersion expectedLocalFileVersion) { 303 File actualLocalFile = getAbsolutePathFile(expectedLocalFileVersion.getPath()); 304 FileVersionComparison fileVersionComparison = fileVersionHelper.compare(expectedLocalFileVersion, actualLocalFile, true); 305 306 return fileVersionComparison; 307 } 308 309 protected boolean fileExists(FileVersion expectedLocalFileVersion) { 310 File actualLocalFile = getAbsolutePathFile(expectedLocalFileVersion.getPath()); 311 return FileUtil.exists(actualLocalFile); 312 } 313 314 protected void deleteFile(FileVersion deleteFileVersion) { 315 File fromFileOnDisk = getAbsolutePathFile(deleteFileVersion.getPath()); 316 fromFileOnDisk.delete(); 317 } 318 319 protected File getAbsolutePathFile(String relativePath) { 320 return new File(config.getLocalDir(), relativePath); // TODO [medium] This does not work for 'some\file' on windows! 321 } 322 323 public abstract FileSystemActionResult execute() throws Exception; 324}