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}