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.util.Collection;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.logging.Level;
026import java.util.logging.Logger;
027
028import org.syncany.config.Config;
029import org.syncany.database.ChunkEntry.ChunkChecksum;
030import org.syncany.database.FileContent;
031import org.syncany.database.FileVersion;
032import org.syncany.database.MemoryDatabase;
033import org.syncany.database.MultiChunkEntry.MultiChunkId;
034import org.syncany.database.PartialFileHistory;
035import org.syncany.database.SqlDatabase;
036import org.syncany.operations.Downloader;
037import org.syncany.operations.Operation;
038import org.syncany.operations.OperationResult;
039import org.syncany.operations.down.actions.FileCreatingFileSystemAction;
040import org.syncany.operations.down.actions.FileSystemAction;
041import org.syncany.plugins.transfer.TransferManager;
042
043/**
044 * Applies a given winners database to the local directory.
045 * 
046 * <p>Steps:
047 * <ul>
048 *  <li>Determine whether the local branch needs to be updated (new database versions); if so, determine
049 *      local {@link FileSystemAction}s</li>
050 *  <li>Determine, download and decrypt required multi chunks from remote storage from file actions
051 *      (implemented in {@link #determineMultiChunksToDownload(FileVersion, MemoryDatabase) determineMultiChunksToDownload()},
052 *      and {@link Downloader#downloadAndDecryptMultiChunks(Set) downloadAndDecryptMultiChunks()})</li>
053 *  <li>Apply file system actions locally, creating conflict files where necessary if local file does
054 *      not match the expected file (implemented in {@link #applyFileSystemActions(List) applyFileSystemActions()} </li>
055 * </ul>
056 * 
057 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
058 */
059public class ApplyChangesOperation extends Operation {
060        private static final Logger logger = Logger.getLogger(DownOperation.class.getSimpleName());
061
062        private SqlDatabase localDatabase;
063        private Downloader downloader;
064
065        private MemoryDatabase winnersDatabase;
066        private DownOperationResult result;
067        
068        private boolean cleanupOccurred;
069        private List<PartialFileHistory> preDeleteFileHistoriesWithLastVersion;
070
071        public ApplyChangesOperation(Config config, SqlDatabase localDatabase, TransferManager transferManager, MemoryDatabase winnersDatabase,
072                        DownOperationResult result, boolean cleanupOccurred, List<PartialFileHistory> preDeleteFileHistoriesWithLastVersion) {
073                
074                super(config);
075                
076                this.localDatabase = localDatabase;
077                this.downloader = new Downloader(config, transferManager);
078                this.winnersDatabase = winnersDatabase;
079                this.result = result;
080                this.cleanupOccurred = cleanupOccurred;
081                this.preDeleteFileHistoriesWithLastVersion = preDeleteFileHistoriesWithLastVersion;
082        }
083
084        @Override
085        public OperationResult execute() throws Exception {
086                logger.log(Level.INFO, "Determine file system actions ...");            
087                
088                FileSystemActionReconciliator actionReconciliator = new FileSystemActionReconciliator(config, result.getChangeSet());
089                List<FileSystemAction> actions;
090                
091                if (cleanupOccurred) {
092                        actions = actionReconciliator.determineFileSystemActions(winnersDatabase, true, preDeleteFileHistoriesWithLastVersion);
093                }
094                else {
095                        actions = actionReconciliator.determineFileSystemActions(winnersDatabase);
096                }
097
098                Set<MultiChunkId> unknownMultiChunks = determineRequiredMultiChunks(actions, winnersDatabase);
099                
100                downloader.downloadAndDecryptMultiChunks(unknownMultiChunks);
101                result.getDownloadedMultiChunks().addAll(unknownMultiChunks);
102
103                applyFileSystemActions(actions);
104                
105                return null;
106        }
107        
108        /**
109         * Finds the multichunks that need to be downloaded to apply the given file system actions.
110         * The method looks at all {@link FileCreatingFileSystemAction}s and returns their multichunks. 
111         */
112        private Set<MultiChunkId> determineRequiredMultiChunks(List<FileSystemAction> actions, MemoryDatabase winnersDatabase) {
113                Set<MultiChunkId> multiChunksToDownload = new HashSet<MultiChunkId>();
114
115                for (FileSystemAction action : actions) {
116                        if (action instanceof FileCreatingFileSystemAction) { // TODO [low] This adds ALL multichunks even though some might be available locally
117                                multiChunksToDownload.addAll(determineMultiChunksToDownload(action.getFile2(), winnersDatabase));
118                        }
119                }
120
121                return multiChunksToDownload;
122        }
123        
124        /**
125         * Finds the multichunks that need to be downloaded for the given file version -- using the local 
126         * database and given winners database. Returns a set of multichunk identifiers.
127         */
128        private Collection<MultiChunkId> determineMultiChunksToDownload(FileVersion fileVersion, MemoryDatabase winnersDatabase) {
129                Set<MultiChunkId> multiChunksToDownload = new HashSet<MultiChunkId>();
130
131                // First: Check if we know this file locally!
132                List<MultiChunkId> multiChunkIds = localDatabase.getMultiChunkIds(fileVersion.getChecksum());
133                
134                if (multiChunkIds.size() > 0) {
135                        multiChunksToDownload.addAll(multiChunkIds);
136                }
137                else {
138                        // Second: We don't know it locally; must be from the winners database
139                        FileContent winningFileContent = winnersDatabase.getContent(fileVersion.getChecksum());                 
140                        boolean winningFileHasContent = winningFileContent != null;
141
142                        if (winningFileHasContent) { // File can be empty!
143                                List<ChunkChecksum> fileChunks = winningFileContent.getChunks(); 
144                                
145                                // TODO [medium] Instead of just looking for multichunks to download here, we should look for chunks in local files as well
146                                // and return the chunk positions in the local files ChunkPosition (chunk123 at file12, offset 200, size 250)
147                                
148                                Map<ChunkChecksum, MultiChunkId> checksumsWithMultiChunkIds = localDatabase.getMultiChunkIdsByChecksums(fileChunks);
149                                
150                                for (ChunkChecksum chunkChecksum : fileChunks) {
151                                        MultiChunkId multiChunkIdForChunk = checksumsWithMultiChunkIds.get(chunkChecksum);
152                                        if (multiChunkIdForChunk == null) {
153                                                multiChunkIdForChunk = winnersDatabase.getMultiChunkIdForChunk(chunkChecksum);
154                                                
155                                                if (multiChunkIdForChunk == null) {
156                                                        throw new RuntimeException("Cannot find multichunk for chunk "+chunkChecksum);  
157                                                }
158                                        }
159                                        
160                                        if (!multiChunksToDownload.contains(multiChunkIdForChunk)) {
161                                                logger.log(Level.INFO, "  + Adding multichunk " + multiChunkIdForChunk + " to download list ...");
162                                                multiChunksToDownload.add(multiChunkIdForChunk);
163                                        }
164                                }
165                        }
166                }
167                
168                return multiChunksToDownload;
169        }
170        
171        /**
172         * Applies the given file system actions in a sensible order. To do that, 
173         * the given actions are first sorted using the {@link FileSystemActionComparator} and
174         * then executed individually using {@link FileSystemAction#execute()}.
175         */
176        private void applyFileSystemActions(List<FileSystemAction> actions) throws Exception {
177                // Sort
178                FileSystemActionComparator actionComparator = new FileSystemActionComparator();
179                actionComparator.sort(actions);
180
181                logger.log(Level.FINER, "- Applying file system actions (sorted!) ...");
182
183                // Apply
184                for (FileSystemAction action : actions) {
185                        if (logger.isLoggable(Level.FINER)) {
186                                logger.log(Level.FINER, "   +  {0}", action);
187                        }
188
189                        // Execute the file system action
190                        
191                        // Note that exceptions are not caught here, to prevent 
192                        // apply-failed-delete-on-up situations.
193                        
194                        action.execute(); 
195                }
196        }
197}