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;
019
020import java.io.File;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.logging.Level;
027import java.util.logging.Logger;
028
029import org.syncany.config.Config;
030import org.syncany.database.FileVersion;
031import org.syncany.database.FileVersion.FileStatus;
032import org.syncany.database.FileVersionComparator;
033import org.syncany.database.FileVersionComparator.FileChange;
034import org.syncany.database.FileVersionComparator.FileVersionComparison;
035import org.syncany.database.MemoryDatabase;
036import org.syncany.database.PartialFileHistory;
037import org.syncany.database.PartialFileHistory.FileHistoryId;
038import org.syncany.database.SqlDatabase;
039import org.syncany.operations.Assembler;
040import org.syncany.operations.ChangeSet;
041import org.syncany.operations.down.actions.ChangeFileSystemAction;
042import org.syncany.operations.down.actions.DeleteFileSystemAction;
043import org.syncany.operations.down.actions.FileSystemAction;
044import org.syncany.operations.down.actions.NewFileSystemAction;
045import org.syncany.operations.down.actions.NewSymlinkFileSystemAction;
046import org.syncany.operations.down.actions.RenameFileSystemAction;
047import org.syncany.operations.down.actions.SetAttributesFileSystemAction;
048
049
050/**
051 * Implements the file synchronization algorithm in the down operation.
052 * 
053 * The algorithm compares the local file on the disk with the last local
054 * database file version and the last winning file version and determines
055 * what file system action (fsa) to apply.
056 *
057 * Input variables:
058 * - winning version
059 * - winning file (= local file of winning version)
060 * - local version
061 * - local file (= local file of local version)
062 * 
063 * Algorithm:
064 * if (has no local version) { 
065 *   compwinfwinv = compare winning file to winning version (incl. checksum!)
066 *   
067 *   if (compwinfwinv: winning file matches winning version) {
068 *     // do nothing
069 *   }
070 *   else if (compwinfwinv: new) {
071 *     add new fsa for winning version
072 *     add multichunks to download list for winning version
073 *   }
074 *   else if (compwinfwinv: deleted) {
075 *     add delete fsa for winning version
076 *   }
077 *   else if (compwinfwinv: changed link) {
078 *     add changed link fsa for winning version
079 *   } 
080 *   else if (compwinfwinv: changes attrs / modified date) { // does not(!) include "path"
081 *     add changed attrs fsa for winning version
082 *   }
083 *   else if (compwinfwinv: changed path) {
084 *     // Cannot be!
085 *   }
086 *   else { // size/checksum (path cannot be!)
087 *     add conflict fsa for winning file
088 *     add new fsa for winning version
089 *     add multichunks to download list for winning version
090 *   }
091 * }
092 * 
093 * else { // local version exists
094 *   complocflocv = compare local file to local version (incl. checksum!)
095 *   
096 *   if (complocflocv: local file matches local version) { // file as expected on disk
097 *     complocvwinv = compare local version to winning version
098 *       
099 *     if (complocvwinv: local version matches winning version) { // means: local file = local version = winning version
100 *       // Nothing to do
101 *     }
102 *     else if (complocvwinv: new) {
103 *       // Cannot be!
104 *     }
105 *     else if (complocvwinv: deleted) {
106 *       add delete fsa for winning version
107 *     }
108 *     else if (complocvwinv: changed link) {
109 *       add changed link fsa for winning version
110 *     } 
111 *     else if (complocvwinv: changes attrs / modified date / path) { // includes "path!"
112 *       add changed attrs / renamed fsa for winning version
113 *     }
114 *     else { // size/checksum 
115 *       add changed fsa for winning version (and delete local version)
116 *       add multichunks to download list for winning version
117 *     }
118 *   }
119 *   else { // local file does NOT match local version
120 *     if (local file exists) {
121 *       add conflict fsa for local version
122 *     }
123 *     
124 *     add new fsa for winning version
125 *     add multichunks to download list for winning version
126 * }
127 * 
128 */
129public class FileSystemActionReconciliator {
130        private static final Logger logger = Logger.getLogger(FileSystemActionReconciliator.class.getSimpleName());
131
132        private Config config; 
133        private ChangeSet changeSet;
134        private SqlDatabase localDatabase;
135        private FileVersionComparator fileVersionComparator;
136        private Assembler assembler;
137        
138        public FileSystemActionReconciliator(Config config, ChangeSet changeSet) {
139                this.config = config; 
140                this.changeSet = changeSet;
141                this.localDatabase = new SqlDatabase(config);
142                this.fileVersionComparator = new FileVersionComparator(config.getLocalDir(), config.getChunker().getChecksumAlgorithm());
143        }
144        
145        public List<FileSystemAction> determineFileSystemActions(MemoryDatabase winnersDatabase) throws Exception {
146                List<PartialFileHistory> localFileHistoriesWithLastVersion = localDatabase.getFileHistoriesWithLastVersion();
147                return determineFileSystemActions(winnersDatabase, false, localFileHistoriesWithLastVersion);
148        }
149
150        public List<FileSystemAction> determineFileSystemActions(MemoryDatabase winnersDatabase, boolean cleanupOccurred,
151                        List<PartialFileHistory> localFileHistoriesWithLastVersion) throws Exception {
152                this.assembler = new Assembler(config, localDatabase, winnersDatabase);
153                
154                List<FileSystemAction> fileSystemActions = new ArrayList<FileSystemAction>();
155                
156                // Load file history cache
157                logger.log(Level.INFO, "- Loading current file tree...");                                               
158                Map<FileHistoryId, FileVersion> localFileHistoryIdCache = fillFileHistoryIdCache(localFileHistoriesWithLastVersion);
159                
160                logger.log(Level.INFO, "- Determine filesystem actions ...");
161                
162                for (PartialFileHistory winningFileHistory : winnersDatabase.getFileHistories()) {
163                        // Get remote file version and content
164                        FileVersion winningLastVersion = winningFileHistory.getLastVersion();                   
165                        File winningLastFile = new File(config.getLocalDir(), winningLastVersion.getPath());
166                        
167                        // Get local file version and content
168                        FileVersion localLastVersion = localFileHistoryIdCache.get(winningFileHistory.getFileHistoryId());
169                        File localLastFile = (localLastVersion != null) ? new File(config.getLocalDir(), localLastVersion.getPath()) : null;
170                                                
171                        logger.log(Level.INFO, "  + Comparing local version: "+localLastVersion);       
172                        logger.log(Level.INFO, "    with winning version   : "+winningLastVersion);
173                        
174                        // Sync algorithm ////                  
175                        
176                        // No local file version in local database
177                        if (localLastVersion == null) {         
178                                determineActionNoLocalLastVersion(winningLastVersion, winningLastFile, winnersDatabase, fileSystemActions);
179                        }
180                        
181                        // Local version found in local database
182                        else {
183                                FileVersionComparison localFileToVersionComparison = fileVersionComparator.compare(localLastVersion, localLastFile, true);
184                                
185                                // Local file on disk as expected
186                                if (localFileToVersionComparison.areEqual()) { 
187                                        determineActionWithLocalVersionAndLocalFileAsExpected(winningLastVersion, winningLastFile, localLastVersion, localLastFile,
188                                                        winnersDatabase, fileSystemActions);
189                                }
190                                
191                                // Local file NOT what was expected
192                                else { 
193                                        determineActionWithLocalVersionAndLocalFileDiffers(winningLastVersion, winningLastFile, localLastVersion, localLastFile,
194                                                        winnersDatabase, fileSystemActions, localFileToVersionComparison);                      
195                                }
196                        }               
197                }
198                
199                // Find file histories that are in the local database and not in the
200                // winner's database. They will be assumed to be deleted.               
201                
202                if (cleanupOccurred) {
203                        logger.log(Level.INFO, "- Determine filesystem actions (for deleted histories in winner's branch)...");
204                        Map<FileHistoryId, FileVersion> winnerFileHistoryIdCache = fillFileHistoryIdCache(winnersDatabase.getFileHistories());
205        
206                        for (PartialFileHistory localFileHistoryWithLastVersion : localFileHistoriesWithLastVersion) {
207                                boolean localFileHistoryInWinnersDatabase = winnerFileHistoryIdCache.get(localFileHistoryWithLastVersion.getFileHistoryId()) != null;
208                                
209                                // If the file history is also present in the winner's database, it
210                                // has already been processed above. So we'll ignore it here.
211                                
212                                if (!localFileHistoryInWinnersDatabase) {
213                                        FileVersion localLastVersion = localFileHistoryWithLastVersion.getLastVersion();
214                                        File localLastFile = (localLastVersion != null) ? new File(config.getLocalDir(), localLastVersion.getPath()) : null;
215        
216                                        determineActionFileHistoryNotInWinnerBranch(localLastVersion, localLastFile, fileSystemActions);
217                                }
218                        }
219                }
220                        
221                return fileSystemActions;
222        }
223
224        private void determineActionNoLocalLastVersion(FileVersion winningLastVersion, File winningLastFile, MemoryDatabase winnersDatabase,
225                        List<FileSystemAction> outFileSystemActions) throws Exception {
226                
227                FileVersionComparison winningFileToVersionComparison = fileVersionComparator.compare(winningLastVersion, winningLastFile, true);
228                
229                boolean contentChanged = winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_CHECKSUM)
230                                || winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_SIZE);
231                
232                if (winningFileToVersionComparison.areEqual()) {
233                        logger.log(Level.INFO, "     -> (1) Equals: Nothing to do, winning version equals winning file: "+winningLastVersion+" AND "+winningLastFile);  
234                }
235                else if (winningFileToVersionComparison.getFileChanges().contains(FileChange.DELETED)) {                                        
236                        FileSystemAction action = new NewFileSystemAction(config, winnersDatabase, assembler, winningLastVersion);
237                        outFileSystemActions.add(action);
238                        
239                        logger.log(Level.INFO, "     -> (2) Deleted: Local file does NOT exist, but it should, winning version not known: "+winningLastVersion+" AND "+winningLastFile);
240                        logger.log(Level.INFO, "     -> "+action);
241                        
242                        changeSet.getNewFiles().add(winningLastVersion.getPath());
243                }
244                else if (winningFileToVersionComparison.getFileChanges().contains(FileChange.NEW)) {
245                        FileSystemAction action = new DeleteFileSystemAction(config, null, winningLastVersion, winnersDatabase);
246                        outFileSystemActions.add(action);
247                        
248                        logger.log(Level.INFO, "     -> (3) New: winning version was deleted, but local exists, winning version = "+winningLastVersion+" at "+winningLastFile);                                 
249                        logger.log(Level.INFO, "     -> "+action);      
250                        
251                        changeSet.getDeletedFiles().add(winningLastVersion.getPath());
252                }
253                else if (winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_LINK_TARGET)) {                                    
254                        FileSystemAction action = new NewSymlinkFileSystemAction(config, winningLastVersion, winnersDatabase);
255                        outFileSystemActions.add(action);
256
257                        logger.log(Level.INFO, "     -> (4) Changed link target: winning file has a different link target: "+winningLastVersion+" AND "+winningLastFile);
258                        logger.log(Level.INFO, "     -> "+action);
259                        
260                        changeSet.getNewFiles().add(winningLastVersion.getPath());
261                }
262                else if (!contentChanged && (winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_LAST_MOD_DATE)
263                                || winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_ATTRIBUTES))) {  
264                        
265                        FileSystemAction action = new SetAttributesFileSystemAction(config, winningLastVersion, winnersDatabase);
266                        outFileSystemActions.add(action);
267
268                        logger.log(Level.INFO, "     -> (5) Changed file attributes: winning file has different file attributes: "+winningLastVersion+" AND "+winningLastFile);
269                        logger.log(Level.INFO, "     -> "+action);
270                        
271                        changeSet.getNewFiles().add(winningLastVersion.getPath());
272                }
273                else if (winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_PATH)) {
274                        logger.log(Level.INFO, "     -> (6) Changed path: winning file has a different path: "+winningLastVersion+" AND "+winningLastFile);                                     
275                        throw new Exception("What happend here?");
276                }
277                else { // Content changed
278                        FileSystemAction action = new NewFileSystemAction(config, winnersDatabase, assembler, winningLastVersion);
279                        outFileSystemActions.add(action);
280
281                        logger.log(Level.INFO, "     -> (7) Content changed: Winning file differs from winning version: "+winningLastVersion+" AND "+winningLastFile);
282                        logger.log(Level.INFO, "     -> "+action);
283                        
284                        changeSet.getNewFiles().add(winningLastVersion.getPath());
285                }                                                       
286        }
287        
288        private void determineActionWithLocalVersionAndLocalFileAsExpected(FileVersion winningLastVersion, File winningLastFile,
289                        FileVersion localLastVersion, File localLastFile, MemoryDatabase winnersDatabase, List<FileSystemAction> fileSystemActions) {
290                
291                FileVersionComparison winningVersionToLocalVersionComparison = fileVersionComparator.compare(winningLastVersion, localLastVersion);
292                
293                boolean contentChanged = winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_CHECKSUM)
294                                || winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_SIZE);                                   
295                
296                if (winningVersionToLocalVersionComparison.areEqual()) { // Local file = local version = winning version!
297                        logger.log(Level.INFO, "     -> (8) Equals: Nothing to do, local file equals local version equals winning version: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
298                }
299                else if (winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.DELETED)) {
300                        FileSystemAction action = new ChangeFileSystemAction(config, winnersDatabase, assembler, localLastVersion, winningLastVersion);
301                        fileSystemActions.add(action);
302
303                        logger.log(Level.INFO, "     -> (9) Content changed: Local file does not exist, but it should: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
304                        logger.log(Level.INFO, "     -> "+action);
305                        
306                        changeSet.getChangedFiles().add(winningLastVersion.getPath());
307                }
308                else if (winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.NEW)) {
309                        FileSystemAction action = new DeleteFileSystemAction(config, localLastVersion, winningLastVersion, winnersDatabase);
310                        fileSystemActions.add(action);
311                        
312                        logger.log(Level.INFO, "     -> (10) Local file exists, but should not: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);                                  
313                        logger.log(Level.INFO, "     -> "+action);      
314                        
315                        changeSet.getDeletedFiles().add(winningLastVersion.getPath());
316                }
317                else if (winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_LINK_TARGET)) {                                    
318                        FileSystemAction action = new NewSymlinkFileSystemAction(config, winningLastVersion, winnersDatabase);
319                        fileSystemActions.add(action);
320
321                        logger.log(Level.INFO, "     -> (11) Changed link target: local file has a different link target: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
322                        logger.log(Level.INFO, "     -> "+action);
323                        
324                        changeSet.getNewFiles().add(winningLastVersion.getPath());
325                }
326                else if (!contentChanged && (winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_LAST_MOD_DATE)
327                                || winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_ATTRIBUTES)
328                                || winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_PATH))) {        
329                        
330                        FileSystemAction action = new RenameFileSystemAction(config, localLastVersion, winningLastVersion, winnersDatabase);
331                        fileSystemActions.add(action);
332
333                        logger.log(Level.INFO, "     -> (12) Rename / Changed file attributes: Local file has different file attributes: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
334                        logger.log(Level.INFO, "     -> "+action);
335                        
336                        changeSet.getChangedFiles().add(winningLastVersion.getPath());
337                }
338                else { // Content changed
339                        FileSystemAction action = new ChangeFileSystemAction(config, winnersDatabase, assembler, localLastVersion, winningLastVersion);
340                        fileSystemActions.add(action);
341
342                        logger.log(Level.INFO, "     -> (13) Content changed: Local file differs from winning version: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
343                        logger.log(Level.INFO, "     -> "+action);      
344                        
345                        changeSet.getChangedFiles().add(winningLastVersion.getPath());
346                }
347        }
348
349        private void determineActionWithLocalVersionAndLocalFileDiffers(FileVersion winningLastVersion, File winningLastFile,
350                        FileVersion localLastVersion, File localLastFile, MemoryDatabase winnersDatabase, List<FileSystemAction> fileSystemActions,
351                        FileVersionComparison localFileToVersionComparison) {
352
353                if (localFileToVersionComparison.getFileChanges().contains(FileChange.DELETED)) {       
354                        boolean winningLastVersionDeleted = winningLastVersion.getStatus() == FileStatus.DELETED;
355                        
356                        if (!winningLastVersionDeleted) {
357                                FileSystemAction action = new ChangeFileSystemAction(config, winnersDatabase, assembler, localLastVersion, winningLastVersion);
358                                fileSystemActions.add(action);
359                
360                                logger.log(Level.INFO, "     -> (14) Content changed: Local file does NOT exist, and winning version changed: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
361                                logger.log(Level.INFO, "     -> "+action);      
362                                
363                                changeSet.getChangedFiles().add(winningLastVersion.getPath());
364                        }
365                        else {
366                                logger.log(Level.INFO, "     -> (15) Doing nothing: Local file does NOT exist, and winning version is marked DELETED: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);                            
367                        }
368                }
369                else {
370                        FileSystemAction action = new ChangeFileSystemAction(config, winnersDatabase, assembler, winningLastVersion, localLastVersion);
371                        fileSystemActions.add(action);
372        
373                        logger.log(Level.INFO, "     -> (16) Content changed: Local file differs from last version: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
374                        logger.log(Level.INFO, "     -> "+action);      
375                        
376                        changeSet.getChangedFiles().add(winningLastVersion.getPath());
377                }
378        }
379        
380        private void determineActionFileHistoryNotInWinnerBranch(FileVersion localLastVersion, File localLastFile, List<FileSystemAction> fileSystemActions) {
381                // No local file version in local database
382                if (localLastVersion == null) {         
383                        throw new RuntimeException("This should not happen.");
384                }
385                
386                // Local version found in local database
387                else {
388                        FileSystemAction action = new DeleteFileSystemAction(config, localLastVersion, localLastVersion, null);
389                        fileSystemActions.add(action);
390                        
391                        logger.log(Level.INFO, "     -> (17) Local file exists, but not in winner branch -> File was deleted remotely: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = (none)");                                        
392                        logger.log(Level.INFO, "     -> "+action);      
393                        
394                        changeSet.getDeletedFiles().add(localLastVersion.getPath());                    
395                }                               
396        }
397
398        private Map<FileHistoryId, FileVersion> fillFileHistoryIdCache(Collection<PartialFileHistory> fileHistoriesWithLastVersion) {
399                Map<FileHistoryId, FileVersion> fileHistoryIdCache = new HashMap<FileHistoryId, FileVersion>();
400                
401                for (PartialFileHistory fileHistory : fileHistoriesWithLastVersion) {
402                        fileHistoryIdCache.put(fileHistory.getFileHistoryId(), fileHistory.getLastVersion());
403                }
404                
405                return fileHistoryIdCache;
406        }       
407}