package net.flibusta.servlet;
import net.flibusta.concurrent.LockManager;
import net.flibusta.converter.ConversionService;
import net.flibusta.converter.ConversionServiceFactory;
import net.flibusta.download.DownloadService;
import net.flibusta.persistence.dao.BookDao;
import net.flibusta.persistence.dao.UrlDao;
import net.flibusta.persistence.dao.UrlInfo;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URI;
import java.net.URL;
@Controller
public class ConverterController implements SingleUrlConverter {
Logger logger = Logger.getLogger(ConverterController.class);
public static final String PARAM_URL = "url";
public static final String PARAM_SOURCE_MD5 = "md5";
public static final String PARAM_OUT_FORMAT = "out";
public static final String PARAM_SOURCE_FORMAT = "src";
public static final String DEFAULT_OUT_FORMAT = "mobi";
public static final String DEFAULT_SRC_FORMAT = "fb2";
@Autowired
private UrlDao urlDao;
@Autowired
private BookDao bookDao;
@Autowired
private ConversionServiceFactory conversionServiceFactory;
@Autowired
private DownloadService downloadService;
@Autowired
private LockManager lockManager;
private String staticRedirectUrlPrefix = null;
private Boolean useXAccelRerirect = false;
@Override
@RequestMapping(value = "/convert", method = {RequestMethod.GET, RequestMethod.HEAD})
public void convert(@RequestParam(PARAM_URL) String sourceUrl,
@RequestParam(value = PARAM_SOURCE_MD5, required = false) String sourceMd5,
@RequestParam(value = PARAM_OUT_FORMAT, required = false) String outputFormat,
@RequestParam(value = PARAM_SOURCE_FORMAT, required = false) String sourceFormat,
HttpServletResponse response) throws Exception {
if (sourceUrl == null || sourceUrl.length() == 0) {
throw new Exception("Required parameter missing: " + PARAM_URL);
}
if (outputFormat == null) {
outputFormat = DEFAULT_OUT_FORMAT;
}
String bookId;
lockManager.lock(sourceUrl);
try {
// System.out.println("locked " + Thread.currentThread().getName() + " " + sourceUrl);
UrlInfo urlInfo = urlDao.findUrlInfo(sourceUrl);
if (urlInfo == null) {
if (sourceMd5 != null && sourceMd5.length() == 32) {
urlInfo = new UrlInfo();
urlInfo.setBookId(sourceMd5);
urlInfo.setSourceFormat(sourceFormat);
} else if (sourceUrl.contains("flibusta.net/b/")) { // guess book id and source format from url
URI uri = new URI(sourceUrl);
String path = uri.getPath();
String[] pathElements = path.split("/");
if (pathElements.length == 4 && pathElements[2].length() == 32) {
urlInfo = new UrlInfo();
urlInfo.setBookId(pathElements[2]);
String format = "download".equals(pathElements[3]) ? "fb2" : pathElements[3];
urlInfo.setSourceFormat(format);
urlDao.addUrlReference(sourceUrl, urlInfo.getBookId(), urlInfo.getSourceFormat());
logger.debug("guessed bookId=" + urlInfo.getBookId() + " format=" + urlInfo.getSourceFormat() + " from url=" + sourceUrl);
}
}
}
if (urlInfo != null) {
// source book already downloaded
bookId = urlInfo.getBookId();
File book = bookDao.findBook(bookId, outputFormat);
if (book != null) {
// book already converted
redirectToFile(bookId, book, outputFormat, response);
return;
}
// book downloaded but not converted yet
if (bookDao.findBook(bookId, urlInfo.getSourceFormat()) == null) { // just safety check for lost files
urlDao.removeUrlReference(sourceUrl);
bookId = downloadBook(sourceUrl, sourceFormat);
}
} else {
// source book not downloaded yet
bookId = downloadBook(sourceUrl, sourceFormat);
}
} finally {
lockManager.unlock(sourceUrl);
// System.out.println("unlocked " + Thread.currentThread().getName() + " " + sourceUrl);
}
logger.info("Start conversion url=" + sourceUrl + " bookId=" + bookId + " format=" + outputFormat);
makeConversion(bookId, outputFormat, response);
}
@RequestMapping(value = "/clean", method = RequestMethod.GET)
public void clean(@RequestParam(PARAM_URL) String sourceUrl, HttpServletResponse response) throws Exception {
if (sourceUrl == null || sourceUrl.length() == 0) {
throw new Exception("Required parameter missing: " + PARAM_URL);
}
lockManager.lock(sourceUrl);
try {
UrlInfo urlInfo = urlDao.findUrlInfo(sourceUrl);
if (urlInfo != null) {
String bookId = urlInfo.getBookId();
lockManager.lock(bookId);
try {
bookDao.deleteBook(bookId);
} finally {
lockManager.unlock(bookId);
}
urlDao.removeUrlReference(sourceUrl);
}
} finally {
lockManager.unlock(sourceUrl);
}
response.setStatus(HttpStatus.OK.value());
response.setHeader("Content-Type", "text/plain");
PrintWriter writer = response.getWriter();
writer.write("OK");
}
@ExceptionHandler(Exception.class)
public void handleException(Exception e, HttpServletResponse response) {
logger.error("Exception sent by ConverterController: " + e.getMessage(), e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("text/plain");
try {
PrintWriter writer = response.getWriter();
writer.println("Internal server error: " + e.getMessage());
} catch (IOException e1) {
logger.error(e);
}
}
private String downloadBook(String sourceUrl, String sourceFormat) throws Exception {
File sourceFile = downloadService.fetch(new URL(sourceUrl));
if (sourceFile == null) {
throw new Exception("Can't download book from url " + sourceUrl);
}
String bookId = calculateBookId(sourceFile);
if (sourceFormat == null) {
try {
sourceFormat = resolveSourceFormat(sourceFile);
} catch (Exception e) {
logger.error("book " + bookId + " Error: " + e.getMessage());
throw e;
}
}
urlDao.addUrlReference(sourceUrl, bookId, sourceFormat);
if (bookDao.findBook(bookId, sourceFormat) == null) {
bookDao.addBook(bookId, sourceFormat, sourceFile);
} else {
// this book already downloaded from other source
sourceFile.delete();
}
return bookId;
}
private void makeConversion(String bookId, String outputFormat, HttpServletResponse response) throws Exception {
File convertedFile;
lockManager.lock(bookId);
try {
convertedFile = bookDao.findBook(bookId, outputFormat);
if (convertedFile == null) {
ConversionService conversionService = conversionServiceFactory.getConversionService(outputFormat);
convertedFile = conversionService.convert(bookId);
}
} finally {
lockManager.unlock(bookId);
}
if (convertedFile != null && convertedFile.exists()) {
logger.debug("converted bookId=" + bookId + " to format=" + outputFormat);
redirectToFile(bookId, convertedFile, outputFormat, response);
} else {
throw new Exception("Conversion failed. bookId=" + bookId + " to format=" + outputFormat);
}
}
private void redirectToFile(String bookId, File convertedFile, String outputFormat, HttpServletResponse response) throws IOException {
if (response == null) {
return; // do nothing
}
String redirectLocation;
if (staticRedirectUrlPrefix == null || staticRedirectUrlPrefix.length() == 0) { // redirect to download controller
redirectLocation = response.encodeRedirectURL("/converter/get/download/" + bookId + "/" + outputFormat + "/" + convertedFile.getName());
} else {
redirectLocation = staticRedirectUrlPrefix + bookDao.findBookPath(bookId, outputFormat);
}
if (!useXAccelRerirect) {
response.sendRedirect(redirectLocation);
} else {
response.setHeader("X-Accel-Redirect", redirectLocation);
response.setHeader("Content-Disposition", "attachment; filename=" + convertedFile.getName());
response.setStatus(HttpStatus.OK.value());
}
}
private String calculateBookId(File sourceFile) throws Exception {
FileInputStream source = new FileInputStream(sourceFile);
try {
return DigestUtils.md5Hex(source);
} finally {
IOUtils.closeQuietly(source);
}
}
private String resolveSourceFormat(File sourceFile) throws Exception {
String sourceFileName = sourceFile.getName();
String extension = FilenameUtils.getExtension(sourceFileName);
if (extension.length() > 1) {
return extension;
}
FileReader fileReader = new FileReader(sourceFile);
BufferedReader reader = new BufferedReader(fileReader, 1024);
String line;
line = getNextLine(reader);
if (line != null) {
if (line.startsWith("<?xml")) {
line = getNextLine(reader);
if (line != null && line.toLowerCase().contains("<fictionbook")) {
return "fb2";
}
} else {
if (line.contains("mimetypeapplication/epub+zip")) {
return "epub";
}
}
}
throw new Exception("Can't determine source format. Please set url parameter '" + PARAM_SOURCE_FORMAT + "'");
}
private String getNextLine(BufferedReader reader) throws IOException {
String line;
do {
line = reader.readLine();
} while (line != null && line.trim().length() == 0);
return line;
}
public void setStaticRedirectUrlPrefix(String staticRedirectUrlPrefix) {
if (!staticRedirectUrlPrefix.endsWith("/")) {
staticRedirectUrlPrefix = staticRedirectUrlPrefix + "/";
}
this.staticRedirectUrlPrefix = staticRedirectUrlPrefix;
}
public void setUseXAccelRerirect(Boolean useXAccelRerirect) {
this.useXAccelRerirect = useXAccelRerirect;
}
}