/* * Overchan Android (Meta Imageboard Client) * Copyright (C) 2014-2016 miku-nyan <https://github.com/miku-nyan> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package nya.miku.wishmaster.containers; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.text.DateFormat; import java.util.HashSet; import java.util.Locale; import java.util.Random; import java.util.Set; import nya.miku.wishmaster.api.interfaces.CancellableTask; import nya.miku.wishmaster.common.IOUtils; import nya.miku.wishmaster.common.Logger; import nya.miku.wishmaster.lib.MailDateFormat; import nya.miku.wishmaster.lib.base64.Base64; import nya.miku.wishmaster.lib.base64.Base64OutputStream; /** * Класс инкапсулирует работу с MHTML-файлом (создание/модификация).<br> * Если файл существует, все файлы, кроме отдельного списка, будут скопированы в новый веб-архив * @author miku-nyan * */ public class WriteableMHTML extends WriteableContainer { private static final String TAG = "WriteableMHTML"; private static final String MHT_FROM = "Saved by wishmaster"; private static final String MHT_SUBJ = "Saved thread"; private static final DateFormat MHT_DATE_FORMAT = new MailDateFormat(); /** файл, содержащий позиции (в байтах от начала, тип long) всех файлов в контейнере */ /*package*/ static final String METADATA_FILE = ".metadata"; /** контрольное значение метаданных */ /*package*/ static final long METADATA_MAGIC = 39; /** основной путь (URL), откуда были "загружены" все файлы */ /*package*/ static final String BASE_URL = "http://mhtml/"; private Set<String> files; private File outputFile; private FileOutputStream outputFileStream; private CountingOutputStream output; private String filePath; private String boundary; private Base64OutputStream base64Stream; private boolean base64StreamOpened; private boolean objectCancelled = false; private ByteArrayOutputStream endFileBuffer; private DataOutputStream endMetaDataStream; public WriteableMHTML(File file) throws IOException { filePath = file.getAbsolutePath(); files = new HashSet<String>(); files.add(METADATA_FILE); outputFile = new File(filePath + ".tmp"); outputFileStream = new FileOutputStream(outputFile); output = new CountingOutputStream(new BufferedOutputStream(outputFileStream)); boundary = genBoundary(); ByteArrayOutputStream buf = new ByteArrayOutputStream(); Writer w = new OutputStreamWriter(buf, "UTF-8"); w.write(String.format(Locale.US, "From: <%s>\r\n", MHT_FROM)); w.write(String.format(Locale.US, "Subject: %s\r\n", MHT_SUBJ)); w.write(String.format(Locale.US, "Date: %s\r\n", MHT_DATE_FORMAT.format(System.currentTimeMillis()))); w.write("MIME-Version: 1.0\r\n"); w.write("Content-Type: multipart/related;\r\n"); w.write("\ttype=\"text/html\";\r\n"); w.write(String.format(Locale.US, "\tboundary=\"%s\"\r\n\r\n", boundary)); w.write("This is a multi-part message in MIME format.\r\n\r\n"); w.close(); output.write(buf.toByteArray()); endFileBuffer = new ByteArrayOutputStream(); endFileBuffer.write(("--" + boundary + "\r\nContent-Type: application/octet-stream\r\n" + "Content-Transfer-Encoding: base64\r\nContent-Location: " + BASE_URL + METADATA_FILE + "\r\n\r\n").getBytes("UTF-8")); endMetaDataStream = new DataOutputStream(new Base64OutputStream(endFileBuffer, Base64.NO_CLOSE | Base64.CRLF)); endMetaDataStream.writeLong(METADATA_MAGIC); base64StreamOpened = false; } @Override public OutputStream openStream(String filename) throws IOException { if (files.contains(filename)) throw new IllegalStateException("file already exists: "+filename); if (base64StreamOpened) throw new IllegalStateException("stream is already opened"); files.add(filename); endMetaDataStream.writeLong(output.getCount()); ByteArrayOutputStream buf = new ByteArrayOutputStream(); Writer w = new OutputStreamWriter(buf, "UTF-8"); w.write("--"); w.write(boundary); w.write("\r\nContent-Type: "); String filename_l = filename.toLowerCase(Locale.US); if (filename_l.endsWith(".html") || filename_l.endsWith(".html")) { w.write("text/html;\r\n\tcharset=\"utf-8\""); } else if (filename_l.endsWith(".css")) { w.write("text/css"); } else if (filename_l.endsWith(".js")) { w.write("text/javascript"); } else if (filename_l.endsWith(".jpg") || filename_l.endsWith(".jpeg")) { w.write("image/jpeg"); } else if (filename_l.endsWith(".png")) { w.write("image/png"); } else if (filename_l.endsWith(".gif")) { w.write("image/gif"); } else if (filename_l.endsWith(".webm")) { w.write("video/webm"); } else if (filename_l.endsWith(".mp4")) { w.write("video/mp4"); } else if (filename_l.endsWith(".ogg")) { w.write("audio/ogg"); } else if (filename_l.endsWith(".mp3")) { w.write("audio/mpeg"); } else { w.write("application/octet-stream"); } w.write("\r\nContent-Transfer-Encoding: base64\r\nContent-Location: "); w.write(BASE_URL); w.write(filename); w.write("\r\n\r\n"); w.close(); output.write(buf.toByteArray()); base64Stream = new Base64OutputStream(output, Base64.NO_CLOSE | Base64.CRLF); base64StreamOpened = true; return new MHTMLOutputStream(); } @Override public void close() throws IOException { if (objectCancelled) return; try { endMetaDataStream.close(); endFileBuffer.write(("\r\n\r\n--" + boundary + "--\r\n").getBytes("UTF-8")); endFileBuffer.writeTo(output); } finally { try { output.close(); } catch (Exception e) { Logger.e(TAG, e); outputFileStream.close(); outputFile.delete(); throw e; } } File old = new File(filePath); old.delete(); if (!outputFile.renameTo(old)) throw new IOException("cannot rename temp file"); } @Override public void cancel() { try { output.close(); outputFile.delete(); } catch (Exception e) { Logger.e(TAG, e); try { outputFileStream.close(); outputFile.delete(); } catch (Exception e1) { Logger.e(TAG, e1); } } objectCancelled = true; } @Override public boolean hasFile(String arg) { return files.contains(arg); } @Override public void transfer(String[] doNotCopy, CancellableTask task) throws IOException { if (base64StreamOpened) throw new IllegalStateException("stream is already opened"); if (doNotCopy == null) doNotCopy = new String[0]; File sourceFile = new File(filePath); if (sourceFile.exists()) { InputStream in = null; try { in = new BufferedInputStream(IOUtils.modifyInputStream(new FileInputStream(sourceFile), null, task)); String srcBoundary = readBoundary(in); while (true) { if (task.isCancelled()) { cancel(); throw new InterruptedException(); } String header = readNextFileHeader(in, srcBoundary); if (header == null) break; String fnfilter = "Content-Location: " + BASE_URL; int fnpos = header.indexOf(fnfilter); if (fnpos == -1) break; else fnpos += fnfilter.length(); int fnposEnd = header.indexOf('\r', fnpos); if (fnposEnd == -1) break; String filename = header.substring(fnpos, fnposEnd); if (filename.equals(METADATA_FILE)) break; boolean skip = false; for (String exclfile : doNotCopy) { if (filename.equalsIgnoreCase(exclfile)) { skip = true; break; } } if (skip || hasFile(filename)) continue; endMetaDataStream.writeLong(output.getCount()); output.write("--".getBytes("UTF-8")); output.write(boundary.getBytes("UTF-8")); output.write(header.getBytes("UTF-8")); int r; boolean eol = false; byte[] buf = new byte[8192]; int bufpos = 0; while ((r = in.read()) != -1) { buf[bufpos++] = (byte) r; if (bufpos == 8192) { output.write(buf, 0, 8192); bufpos = 0; } if (r == '\n') { if (eol) break; eol = true; } else if (r != '\r') { eol = false; } } if (bufpos > 0) output.write(buf, 0, bufpos); files.add(filename); } } catch (Exception e) { if (e instanceof InterruptedException) throw new IOException(); Logger.e(TAG, e); if (e instanceof IOException && IOUtils.isENOSPC(e)) { cancel(); throw (IOException) e; } } finally { if (in != null) { try { in.close(); } catch (Exception e) { Logger.e(TAG, e); } } } } } private String readBoundary(InputStream input) throws IOException { if (!readFilter(input, "boundary=\"".getBytes("UTF-8"))) return null; ByteArrayOutputStream buf = new ByteArrayOutputStream(); int r; while ((r = input.read()) != -1) { if (r == '\"') break; buf.write(r); } return buf.toString("UTF-8"); } private String readNextFileHeader(InputStream input, String boundary) throws IOException { byte[] filter = ("--" + boundary).getBytes("UTF-8"); if (!readFilter(input, filter)) return null; ByteArrayOutputStream buf = new ByteArrayOutputStream(); int r; boolean eol = false; while ((r = input.read()) != -1) { buf.write(r); if (r == '\n') { if (eol) break; eol = true; } else if (r != '\r') { eol = false; } } return buf.toString("UTF-8"); } private boolean readFilter(InputStream input, byte[] filter) throws IOException { int r; int curPos = 0; boolean found = false; while ((r = input.read()) != -1) { if (r == filter[curPos]) ++curPos; else curPos = 0; if (curPos == filter.length) { found = true; break; } } return found; } private String genBoundary() { Random random = new Random(); return "----=_NextPart_000_0000_" + toHex(random.nextInt()) + "." + toHex(random.nextInt()); } private String toHex(int i) { String t = "00000000" + Integer.toHexString(i); return t.substring(t.length() - 8).toUpperCase(Locale.US); } private class MHTMLOutputStream extends OutputStream { @Override public void close() throws IOException { base64StreamOpened = false; base64Stream.close(); output.write(new byte[] { '\r', '\n', '\r', '\n' }); } @Override public void flush() throws IOException { base64Stream.flush(); } @Override public void write(int oneByte) throws IOException { base64Stream.write(oneByte); } @Override public void write(byte[] buffer) throws IOException { base64Stream.write(buffer); } @Override public void write(byte[] buffer, int offset, int count) throws IOException { base64Stream.write(buffer, offset, count); } } private class CountingOutputStream extends FilterOutputStream { private long count = 0; public CountingOutputStream(OutputStream out) { super(out); } @Override public void write(int oneByte) throws IOException { ++count; out.write(oneByte); } @Override public void write(byte[] buffer) throws IOException { count += buffer.length; out.write(buffer); } @Override public void write(byte[] buffer, int offset, int length) throws IOException { count += length; out.write(buffer, offset, length); } public long getCount() { return count; } } }