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;
019
020import java.io.File;
021import java.io.FileOutputStream;
022import java.io.InputStream;
023import java.security.MessageDigest;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.logging.Level;
027import java.util.logging.Logger;
028
029import org.apache.commons.io.FileUtils;
030import org.syncany.chunk.Chunker;
031import org.syncany.chunk.Deduper;
032import org.syncany.chunk.MultiChunk;
033import org.syncany.chunk.MultiChunker;
034import org.syncany.config.Config;
035import org.syncany.database.ChunkEntry.ChunkChecksum;
036import org.syncany.database.FileContent;
037import org.syncany.database.FileVersion;
038import org.syncany.database.MemoryDatabase;
039import org.syncany.database.MultiChunkEntry.MultiChunkId;
040import org.syncany.database.SqlDatabase;
041import org.syncany.util.StringUtil;
042
043/**
044 * The assembler re-assembles files broken down through the deduplication
045 * mechanisms of the {@link Deduper} and its corresponding classes (chunker,
046 * multichunker, etc.).
047 * 
048 * <p>It uses the local {@link SqlDatabase} and an optional {@link MemoryDatabase}
049 * to perform file checksum and chunk checksum lookups.   
050 * 
051 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
052 */
053public class Assembler {
054        private static final Logger logger = Logger.getLogger(Assembler.class.getSimpleName());
055        
056        private Config config;
057        private SqlDatabase localDatabase;
058        private MemoryDatabase memoryDatabase;
059        
060        public Assembler(Config config, SqlDatabase localDatabase) {
061                this(config, localDatabase, null);
062        }
063        
064        public Assembler(Config config, SqlDatabase localDatabase, MemoryDatabase memoryDatabase) {
065                this.config = config;
066                this.localDatabase = localDatabase;
067                this.memoryDatabase = memoryDatabase;
068        }
069
070        /**
071         * Assembles the given file version to the local cache and returns a reference
072         * to the cached file after successfully assembling the file. 
073         */
074        public File assembleToCache(FileVersion fileVersion) throws Exception {
075                File reconstructedFileInCache = config.getCache().createTempFile("reconstructedFileVersion");
076                logger.log(Level.INFO, "     - Creating file " + fileVersion.getPath() + " to " + reconstructedFileInCache + " ...");
077
078                FileContent fileContent = localDatabase.getFileContent(fileVersion.getChecksum(), true);
079
080                if (fileContent == null && memoryDatabase != null) {
081                        fileContent = memoryDatabase.getContent(fileVersion.getChecksum());
082                }
083                
084                // Check consistency!
085                if (fileContent == null && fileVersion.getChecksum() != null) {
086                        throw new Exception("Cannot determine file content for checksum "+fileVersion.getChecksum());
087                }
088
089                // Create empty file
090                if (fileContent == null) {
091                        FileUtils.touch(reconstructedFileInCache);      
092                        return reconstructedFileInCache;
093                }
094                                
095                // Create non-empty file
096                Chunker chunker = config.getChunker();
097                MultiChunker multiChunker = config.getMultiChunker();
098                
099                FileOutputStream reconstructedFileOutputStream = new FileOutputStream(reconstructedFileInCache);                
100                MessageDigest reconstructedFileChecksum = MessageDigest.getInstance(chunker.getChecksumAlgorithm());
101                
102                if (fileContent != null) { // File can be empty!
103                        Collection<ChunkChecksum> fileChunks = fileContent.getChunks();
104
105                        for (ChunkChecksum chunkChecksum : fileChunks) {
106                                MultiChunkId multiChunkIdForChunk = localDatabase.getMultiChunkId(chunkChecksum);
107
108                                if (multiChunkIdForChunk == null && memoryDatabase != null) {
109                                        multiChunkIdForChunk = memoryDatabase.getMultiChunkIdForChunk(chunkChecksum);
110                                }
111
112                                File decryptedMultiChunkFile = config.getCache().getDecryptedMultiChunkFile(multiChunkIdForChunk);
113
114                                MultiChunk multiChunk = multiChunker.createMultiChunk(decryptedMultiChunkFile);
115                                InputStream chunkInputStream = multiChunk.getChunkInputStream(chunkChecksum.getBytes());
116
117                                byte[] buffer = new byte[4096];
118                                int read = 0;
119
120                                while (-1 != (read = chunkInputStream.read(buffer))) {
121                                        reconstructedFileChecksum.update(buffer, 0, read);
122                                        reconstructedFileOutputStream.write(buffer, 0, read);
123                                }
124
125                                chunkInputStream.close();
126                                multiChunk.close();
127                        }
128                }
129
130                reconstructedFileOutputStream.close();
131
132                // Validate checksum
133                byte[] reconstructedFileExpectedChecksum = fileContent.getChecksum().getBytes();
134                byte[] reconstructedFileActualChecksum = reconstructedFileChecksum.digest();
135                
136                if (!Arrays.equals(reconstructedFileActualChecksum, reconstructedFileExpectedChecksum)) {
137                        throw new Exception("Checksums do not match: expected " + StringUtil.toHex(reconstructedFileExpectedChecksum) + " != actual "
138                                        + StringUtil.toHex(reconstructedFileActualChecksum));
139                }
140                
141                return reconstructedFileInCache;
142        }       
143}