/* * Copyright (C) 2009-2015 FBReader.ORG Limited <contact@fbreader.org> * * 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 2 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ package org.geometerplus.android.fbreader.httpd; import java.io.*; import java.util.Map; import fi.iki.elonen.NanoHTTPD; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import org.geometerplus.zlibrary.core.filesystem.ZLFile; import org.geometerplus.zlibrary.core.image.*; import org.geometerplus.zlibrary.core.util.MimeType; import org.geometerplus.zlibrary.core.util.SliceInputStream; import org.geometerplus.zlibrary.ui.android.image.ZLBitmapImage; import org.geometerplus.fbreader.Paths; import org.geometerplus.fbreader.book.CoverUtil; import org.geometerplus.fbreader.formats.PluginCollection; import org.geometerplus.fbreader.formats.PluginImage; public class DataServer extends NanoHTTPD { private final DataService myService; DataServer(DataService service, int port) { super(port); myService = service; } @Override public Response serve(String uri, Method method, Map<String,String> headers, Map<String,String> params, Map<String,String> files) { if (uri.startsWith("/cover/")) { return serveCover(uri, method, headers, params, files); } else if (uri.startsWith("/video")) { return serveVideo(uri, method, headers, params, files); } else { return notFound(uri); } } private Response serveCover(String uri, Method method, Map<String,String> headers, Map<String,String> params, Map<String,String> files) { try { final ZLImage image = CoverUtil.getCover( DataUtil.fileFromEncodedPath(uri.substring(7)), PluginCollection.Instance(Paths.systemInfo(myService)) ); if (image instanceof ZLFileImageProxy) { final ZLFileImageProxy proxy = (ZLFileImageProxy)image; proxy.synchronize(); final ZLStreamImage realImage = proxy.getRealImage(); if (realImage == null) { return notFound(uri); } InputStream stream = realImage.inputStream(); if (stream == null) { return notFound(uri); } final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; try { BitmapFactory.decodeStream(stream, null, options); } catch (Exception e) { return notFound(uri); } if (options.outWidth <= 0 || options.outHeight <= 0) { return notFound(uri); } stream.close(); stream = realImage.inputStream(); if (stream == null) { return notFound(uri); } final Response res = new Response(Response.Status.OK, MimeType.IMAGE_PNG.toString(), stream); res.addHeader("X-Width", String.valueOf(options.outWidth)); res.addHeader("X-Height", String.valueOf(options.outHeight)); return res; } else if (image instanceof PluginImage) { final PluginImage pluginImage = (PluginImage)image; if (pluginImage.isSynchronized()) { try { final Bitmap bitmap = ((ZLBitmapImage)pluginImage.getRealImage()).getBitmap(); final ByteArrayOutputStream os = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 85, os); final InputStream is = new ByteArrayInputStream(os.toByteArray()); final Response res = new Response(Response.Status.OK, MimeType.IMAGE_JPEG.toString(), is); res.addHeader("X-Width", String.valueOf(bitmap.getWidth())); res.addHeader("X-Height", String.valueOf(bitmap.getHeight())); return res; } catch (Throwable t) { return noContent(uri); } } else { myService.ImageSynchronizer.synchronize(pluginImage, null); return noContent(uri); } } else { return notFound(uri); } } catch (Throwable t) { return forbidden(uri, t); } } private Response serveVideo(String uri, Method method, Map<String,String> headers, Map<String,String> params, Map<String,String> files) { String mime = null; for (MimeType mimeType : MimeType.TYPES_VIDEO) { final String m = mimeType.toString(); if (uri.startsWith("/" + m + "/")) { mime = m; break; } } if (mime == null) { return notFound(uri); } try { return serveFile(DataUtil.fileFromEncodedPath(uri.substring(mime.length() + 2)), mime, headers); } catch (Exception e) { return forbidden(uri, e); } } private static final String BYTES_PREFIX = "bytes="; private Response serveFile(ZLFile file, String mime, Map<String,String> headers) throws IOException { final Response res; final InputStream baseStream = file.getInputStream(); final int fileLength = baseStream.available(); final String etag = '"' + Integer.toHexString(file.getPath().hashCode()) + '"'; final String range = headers.get("range"); if (range == null || !range.startsWith(BYTES_PREFIX)) { if (etag.equals(headers.get("if-none-match"))) res = new Response(Response.Status.NOT_MODIFIED, mime, ""); else { res = new Response(Response.Status.OK, mime, baseStream); res.addHeader("ETag", etag); } } else { int start = 0; int end = -1; final String bytes = range.substring(BYTES_PREFIX.length()); final int minus = bytes.indexOf('-'); if (minus > 0) { try { start = Integer.parseInt(bytes.substring(0, minus)); final String endString = bytes.substring(minus + 1).trim(); if (!"".equals(endString)) { end = Integer.parseInt(endString); } } catch (NumberFormatException e) { } } if (start >= fileLength) { res = new Response( Response.Status.RANGE_NOT_SATISFIABLE, MimeType.TEXT_PLAIN.toString(), "" ); res.addHeader("ETag", etag); res.addHeader("Content-Range", "bytes 0-0/" + fileLength); } else { if (end == -1 || end >= fileLength) { end = fileLength - 1; } res = new Response( Response.Status.PARTIAL_CONTENT, mime, new SliceInputStream(baseStream, start, end - start + 1) ); res.addHeader("ETag", etag); res.addHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength); } } res.addHeader("Accept-Ranges", "bytes"); return res; } private Response notFound(String uri) { return new Response( Response.Status.NOT_FOUND, MimeType.TEXT_HTML.toString(), "<html><body><h1>Not found: " + uri + "</h1></body></html>" ); } private Response noContent(String uri) { return new Response( Response.Status.NO_CONTENT, MimeType.TEXT_HTML.toString(), "<html><body><h1>No content: " + uri + "</h1></body></html>" ); } private Response forbidden(String uri, Throwable t) { t.printStackTrace(); return new Response( Response.Status.FORBIDDEN, MimeType.TEXT_HTML.toString(), "<html><body><h1>" + t.getMessage() + "</h1>\n(" + uri + ")</body></html>" ); } }