package com.fsck.k9.mailstore; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import timber.log.Timber; import com.fsck.k9.K9; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.RawDataBody; import com.fsck.k9.mail.internet.SizeAware; import com.fsck.k9.mailstore.util.DeferredFileOutputStream; import com.fsck.k9.mailstore.util.FileFactory; import org.apache.commons.io.IOUtils; /** This is a body where the data is memory-backed at first and switches to file-backed if it gets larger. * @see FileFactory */ public class DeferredFileBody implements RawDataBody, SizeAware { public static final int DEFAULT_MEMORY_BACKED_THRESHOLD = 1024 * 8; private final FileFactory fileFactory; private final String encoding; private final int memoryBackedThreshold; @Nullable private byte[] data; private File file; public DeferredFileBody(FileFactory fileFactory, String transferEncoding) { this(DEFAULT_MEMORY_BACKED_THRESHOLD, fileFactory, transferEncoding); } @VisibleForTesting DeferredFileBody(int memoryBackedThreshold, FileFactory fileFactory, String transferEncoding) { this.fileFactory = fileFactory; this.memoryBackedThreshold = memoryBackedThreshold; this.encoding = transferEncoding; } public OutputStream getOutputStream() throws IOException { return new DeferredFileOutputStream(memoryBackedThreshold, fileFactory) { @Override public void close() throws IOException { super.close(); if (isThresholdExceeded()) { file = getFile(); } else { data = getData(); } } }; } @Override public InputStream getInputStream() throws MessagingException { try { if (file != null) { Timber.d("Decrypted data is file-backed."); return new BufferedInputStream(new FileInputStream(file)); } if (data != null) { Timber.d("Decrypted data is memory-backed."); return new ByteArrayInputStream(data); } throw new IllegalStateException("Data must be fully written before it can be read!"); } catch (IOException ioe) { throw new MessagingException("Unable to open body", ioe); } } @Override public long getSize() { if (file != null) { return file.length(); } if (data != null) { return data.length; } throw new IllegalStateException("Data must be fully written before it can be read!"); } public File getFile() throws IOException { if (file == null) { writeMemoryToFile(); } return file; } private void writeMemoryToFile() throws IOException { if (file != null) { throw new IllegalStateException("Body is already file-backed!"); } if (data == null) { throw new IllegalStateException("Data must be fully written before it can be read!"); } Timber.d("Writing body to file for attachment access"); file = fileFactory.createFile(); FileOutputStream fos = new FileOutputStream(file); fos.write(data); fos.close(); data = null; } @Override public void setEncoding(String encoding) throws MessagingException { throw new UnsupportedOperationException("Cannot re-encode a DecryptedTempFileBody!"); } @Override public void writeTo(OutputStream out) throws IOException, MessagingException { InputStream inputStream = getInputStream(); IOUtils.copy(inputStream, out); } @Override public String getEncoding() { return encoding; } }