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.plugins.local; 019 020import java.io.File; 021import java.io.IOException; 022import java.nio.file.DirectoryStream; 023import java.nio.file.Files; 024import java.nio.file.Path; 025import java.nio.file.Paths; 026import java.util.Map; 027import java.util.logging.Level; 028import java.util.logging.Logger; 029 030import org.apache.commons.io.FileUtils; 031import org.syncany.config.Config; 032import org.syncany.plugins.transfer.AbstractTransferManager; 033import org.syncany.plugins.transfer.StorageException; 034import org.syncany.plugins.transfer.StorageFileNotFoundException; 035import org.syncany.plugins.transfer.StorageMoveException; 036import org.syncany.plugins.transfer.TransferManager; 037import org.syncany.plugins.transfer.files.ActionRemoteFile; 038import org.syncany.plugins.transfer.files.CleanupRemoteFile; 039import org.syncany.plugins.transfer.files.DatabaseRemoteFile; 040import org.syncany.plugins.transfer.files.MultichunkRemoteFile; 041import org.syncany.plugins.transfer.files.RemoteFile; 042import org.syncany.plugins.transfer.files.SyncanyRemoteFile; 043import org.syncany.plugins.transfer.files.TempRemoteFile; 044import org.syncany.plugins.transfer.files.TransactionRemoteFile; 045 046import com.google.common.collect.Maps; 047 048/** 049 * Implements a {@link TransferManager} based on a local storage backend for the 050 * {@link LocalTransferPlugin}. 051 * 052 * <p>Using a {@link LocalTransferSettings}, the transfer manager is configured and uses 053 * any local folder to store the Syncany repository data. While repo and 054 * master file are stored in the given folder, databases and multichunks are stored 055 * in special sub-folders: 056 * 057 * <ul> 058 * <li>The <code>databases</code> folder keeps all the {@link DatabaseRemoteFile}s</li> 059 * <li>The <code>multichunks</code> folder keeps the actual data within the {@link MultichunkRemoteFile}s</li> 060 * </ul> 061 * 062 * <p>This plugin can be used for testing or to point to a repository 063 * on a mounted remote device or network storage such as an NFS or a 064 * Samba/NetBIOS share. 065 * 066 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 067 */ 068public class LocalTransferManager extends AbstractTransferManager { 069 private static final Logger logger = Logger.getLogger(LocalTransferManager.class.getSimpleName()); 070 071 private Path repoPath; 072 private Path multichunksPath; 073 private Path databasesPath; 074 private Path actionsPath; 075 private Path transactionsPath; 076 private Path temporaryPath; 077 078 public LocalTransferManager(LocalTransferSettings connection, Config config) { 079 super(connection, config); 080 081 this.repoPath = Paths.get(connection.getPath().toURI()); // absolute file to get abs. path! 082 this.multichunksPath = repoPath.resolve("multichunks"); 083 this.databasesPath = repoPath.resolve("databases"); 084 this.actionsPath = repoPath.resolve("actions"); 085 this.transactionsPath = repoPath.resolve("transactions"); 086 this.temporaryPath = repoPath.resolve("temporary"); 087 } 088 089 @Override 090 public void connect() throws StorageException { 091 if (repoPath == null) { 092 throw new StorageException("Repository folder '" + repoPath + "' does not exist or is not writable."); 093 } 094 } 095 096 @Override 097 public void disconnect() throws StorageException { 098 // Nothing. 099 } 100 101 @Override 102 public void init(boolean createIfRequired) throws StorageException { 103 connect(); 104 105 try { 106 if (!testTargetExists() && createIfRequired) { 107 if (!Files.exists(Files.createDirectory(repoPath))) { 108 throw new StorageException("Cannot create repository directory: " + repoPath); 109 } 110 } 111 112 if (!Files.exists(Files.createDirectory(multichunksPath))) { 113 throw new StorageException("Cannot create multichunk directory: " + multichunksPath); 114 } 115 116 if (!Files.exists(Files.createDirectory(databasesPath))) { 117 throw new StorageException("Cannot create databases directory: " + databasesPath); 118 } 119 120 if (!Files.exists(Files.createDirectory(actionsPath))) { 121 throw new StorageException("Cannot create actions directory: " + actionsPath); 122 } 123 124 if (!Files.exists(Files.createDirectory(transactionsPath))) { 125 throw new StorageException("Cannot create transactions directory: " + transactionsPath); 126 } 127 128 if (!Files.exists(Files.createDirectory(temporaryPath))) { 129 throw new StorageException("Cannot create temporary directory: " + temporaryPath); 130 } 131 } 132 catch (IOException e) { 133 throw new StorageException("Unable to create directories", e); 134 } 135 } 136 137 @Override 138 public void download(RemoteFile remoteFile, File localFile) throws StorageException { 139 connect(); 140 141 File repoFile = getRemoteFile(remoteFile); 142 143 if (!repoFile.exists()) { 144 throw new StorageFileNotFoundException("No such file in local repository: " + repoFile); 145 } 146 147 try { 148 File tempLocalFile = createTempFile("local-tm-download"); 149 tempLocalFile.deleteOnExit(); 150 151 FileUtils.copyFile(repoFile, tempLocalFile); 152 153 localFile.delete(); 154 FileUtils.moveFile(tempLocalFile, localFile); 155 tempLocalFile.delete(); 156 } 157 catch (IOException ex) { 158 throw new StorageException("Unable to copy file " + repoFile + " from local repository to " + localFile, ex); 159 } 160 } 161 162 @Override 163 public void move(RemoteFile sourceFile, RemoteFile targetFile) throws StorageException { 164 connect(); 165 166 File sourceRemoteFile = getRemoteFile(sourceFile); 167 File targetRemoteFile = getRemoteFile(targetFile); 168 169 if (!sourceRemoteFile.exists()) { 170 throw new StorageMoveException("Unable to move file " + sourceFile + " because it does not exist."); 171 } 172 173 try { 174 FileUtils.moveFile(sourceRemoteFile, targetRemoteFile); 175 } 176 catch (IOException ex) { 177 throw new StorageException("Unable to move file " + sourceRemoteFile + " to destination " + targetRemoteFile, ex); 178 } 179 } 180 181 @Override 182 public void upload(File localFile, RemoteFile remoteFile) throws StorageException { 183 connect(); 184 185 File repoFile = getRemoteFile(remoteFile); 186 File tempRepoFile = new File(getAbsoluteParentDirectory(repoFile) + File.separator + ".temp-" + repoFile.getName()); 187 188 // Do not overwrite files with same size! 189 if (repoFile.exists() && repoFile.length() == localFile.length()) { 190 return; 191 } 192 193 // No such local file 194 if (!localFile.exists()) { 195 throw new StorageException("No such file on local disk: " + localFile); 196 } 197 198 try { 199 FileUtils.copyFile(localFile, tempRepoFile); 200 FileUtils.moveFile(tempRepoFile, repoFile); 201 } 202 catch (IOException ex) { 203 throw new StorageException("Unable to copy file " + localFile + " to local repository " + repoFile, ex); 204 } 205 } 206 207 @Override 208 public boolean delete(RemoteFile remoteFile) throws StorageException { 209 connect(); 210 211 File repoFile = getRemoteFile(remoteFile); 212 213 return !repoFile.exists() || repoFile.delete(); 214 215 } 216 217 @Override 218 public <T extends RemoteFile> Map<String, T> list(Class<T> remoteFileClass) throws StorageException { 219 connect(); 220 221 Path folder = Paths.get(getRemoteFilePath(remoteFileClass)); 222 Map<String, T> files = Maps.newHashMap(); 223 224 try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(folder)) { 225 for (Path path : directoryStream) { 226 try { 227 T remoteFile = RemoteFile.createRemoteFile(path.getFileName().toString(), remoteFileClass); 228 files.put(path.getFileName().toString(), remoteFile); 229 } 230 catch (StorageException e) { 231 logger.log(Level.INFO, "Cannot create instance of " + remoteFileClass.getSimpleName() + " for file " + path 232 + "; maybe invalid file name pattern. Ignoring file."); 233 } 234 } 235 } 236 catch (IOException e) { 237 logger.log(Level.SEVERE, "Unable to list directory", e); 238 } 239 240 return files; 241 } 242 243 @Override 244 public String getRemoteFilePath(Class<? extends RemoteFile> remoteFile) { 245 if (remoteFile.equals(MultichunkRemoteFile.class)) { 246 return multichunksPath.toString(); 247 } 248 else if (remoteFile.equals(DatabaseRemoteFile.class) || remoteFile.equals(CleanupRemoteFile.class)) { 249 return databasesPath.toString(); 250 } 251 else if (remoteFile.equals(ActionRemoteFile.class)) { 252 return actionsPath.toString(); 253 } 254 else if (remoteFile.equals(TransactionRemoteFile.class)) { 255 return transactionsPath.toString(); 256 } 257 else if (remoteFile.equals(TempRemoteFile.class)) { 258 return temporaryPath.toString(); 259 } 260 else { 261 return repoPath.toString(); 262 } 263 } 264 265 private File getRemoteFile(RemoteFile remoteFile) { 266 String rootPath = getRemoteFilePath(remoteFile.getClass()); 267 return Paths.get(rootPath, remoteFile.getName()).toFile(); 268 } 269 270 public String getAbsoluteParentDirectory(File file) { 271 return file.getAbsolutePath().substring(0, file.getAbsolutePath().lastIndexOf(File.separator)); 272 } 273 274 @Override 275 public boolean testTargetCanWrite() { 276 try { 277 if (Files.isDirectory(repoPath)) { 278 Path tempFile = Files.createTempFile(repoPath, "syncany-write-test", "tmp"); 279 Files.delete(tempFile); 280 281 logger.log(Level.INFO, "testTargetCanWrite: Can write, test file created/deleted successfully."); 282 return true; 283 } 284 else { 285 logger.log(Level.INFO, "testTargetCanWrite: Can NOT write, target does not exist."); 286 return false; 287 } 288 } 289 catch (Exception e) { 290 logger.log(Level.INFO, "testTargetCanWrite: Can NOT write to target.", e); 291 return false; 292 } 293 } 294 295 @Override 296 public boolean testTargetExists() { 297 if (Files.exists(repoPath)) { 298 logger.log(Level.INFO, "testTargetExists: Target exists."); 299 return true; 300 } 301 else { 302 logger.log(Level.INFO, "testTargetExists: Target does NOT exist."); 303 return false; 304 } 305 } 306 307 @Override 308 public boolean testRepoFileExists() { 309 try { 310 File repoFile = getRemoteFile(new SyncanyRemoteFile()); 311 312 if (repoFile.exists()) { 313 logger.log(Level.INFO, "testRepoFileExists: Repo file exists, list(syncany) returned one result."); 314 return true; 315 } 316 else { 317 logger.log(Level.INFO, "testRepoFileExists: Repo file DOES NOT exist."); 318 return false; 319 } 320 } 321 catch (Exception e) { 322 logger.log(Level.INFO, "testRepoFileExists: Repo file DOES NOT exist. Exception occurred.", e); 323 return false; 324 } 325 } 326 327 @Override 328 public boolean testTargetCanCreate() { 329 if (Files.isWritable(repoPath.getParent())) { 330 logger.log(Level.INFO, "testTargetCanCreate: Can create target."); 331 return true; 332 } 333 else { 334 logger.log(Level.INFO, "testTargetCanCreate: Can NOT create target."); 335 return false; 336 } 337 } 338}