PageRenderTime 44ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/www/process.php

http://fb2pdf.googlecode.com/
PHP | 447 lines | 317 code | 75 blank | 55 comment | 63 complexity | 63e7b960401e39db60cb61be3b8fcbf4 MD5 | raw file
  1. <?php
  2. require_once 'awscfg.php';
  3. require_once 'fbparser.php';
  4. require_once 's3.php';
  5. require_once 'db.php';
  6. require_once 'sqshelper.php';
  7. require_once 'book_info.php';
  8. require_once 'book_status.php';
  9. require_once 'utils.php';
  10. require_once 'dUnzip2.inc.php';
  11. // Async. book conversion process
  12. class ConvertBook
  13. {
  14. private $email = null;
  15. private $fbFile = null;
  16. private $zipFile = null;
  17. private $fileName = null;
  18. public $book = null;
  19. public $bookKey = null;
  20. // Error codes for Exception
  21. const ERR_ILLEGAL_ARG = 1; // illegal agrument
  22. const ERR_LOAD = 2; // unable to load file
  23. const ERR_FORMAT = 3; // unrecognized format
  24. const ERR_CONVERT = 4; // conversion error
  25. const ERR_SIZE = 5; // file is too big
  26. // db book status constants (private)
  27. const DB_BOOK_NOT_FOUND = 0; // book not found in the DB
  28. const DB_BOOK_CONVERTED = 1; // book is already converted
  29. const DB_BOOK_RECONVERT = 2; // book is converted but with previous conveter's version
  30. // test mode (no amazon, no db)
  31. const TEST_MODE = false; // set false on prod.
  32. // Process book from file uploaded via POST
  33. public function convertFromFile($filePath, $fileName, $format, $email = null)
  34. {
  35. $this->email = $email;
  36. if (!trim($fileName) || !trim($filePath))
  37. throw new Exception("File is not specified.", self::ERR_ILLEGAL_ARG);
  38. if (!is_uploaded_file($filePath))
  39. throw new Exception("Possible file upload attack: $filePath", self::ERR_LOAD);
  40. // Move uploaded file
  41. $tempFile = $this->tempFileName();
  42. if (!move_uploaded_file($filePath, $tempFile))
  43. throw new Exception("Unable to move uploaded file from $filePath to $tempFile", self::ERR_LOAD);
  44. // Process book
  45. $exc = null;
  46. try
  47. {
  48. $this->convert($tempFile, $fileName, $format);
  49. }
  50. catch (Exception $e)
  51. {
  52. $exc = $e;
  53. }
  54. // Remove temporary files
  55. $this->cleanupTempFiles();
  56. // exception handling
  57. if ($exc)
  58. throw $exc;
  59. }
  60. // Process book from url
  61. public function convertFromUrl($url, $format, $email = null)
  62. {
  63. $this->email = $email;
  64. if (!trim($url))
  65. throw new Exception("URL is not specified.", self::ERR_ILLEGAL_ARG);
  66. // Copy file
  67. $tempFile = $this->tempFileName();
  68. if (!copy($url, $tempFile))
  69. throw new Exception("Unable to copy file from $url to $tempFile", self::ERR_LOAD);
  70. // Process book
  71. $exc = null;
  72. try
  73. {
  74. $this->convert($tempFile, $url, $format);
  75. }
  76. catch (Exception $e)
  77. {
  78. $exc = $e;
  79. }
  80. // Remove temporary files
  81. $this->cleanupTempFiles();
  82. // exception handling
  83. if ($exc)
  84. throw $exc;
  85. }
  86. // Process book from storage
  87. public function convertFromS3($key, $format, $email = null)
  88. {
  89. $this->email = $email;
  90. $this->bookKey = $key;
  91. global $awsS3Bucket;
  92. $status = $this->checkFormat($format);
  93. if ($status == self::DB_BOOK_CONVERTED) // book is up-to-date
  94. {
  95. $this->notifyUserByEmail($this->email, $this->bookKey, "r");
  96. return;
  97. }
  98. if ($status == self::DB_BOOK_NOT_FOUND)
  99. {
  100. $this->insertFormat($format);
  101. }
  102. else if ($status == self::DB_BOOK_RECONVERT)
  103. {
  104. // Prepare book for reconverting
  105. $this->updateFormat($format);
  106. }
  107. // save fb2 file
  108. $s3 = getS3Object();
  109. $this->fileName = $s3->getObjectFilename($awsS3Bucket, $this->bookKey . ".fb2");
  110. if ($this->fileName)
  111. $this->fileName = removeExt($this->fileName);
  112. else
  113. $this->fileName = $this->bookKey;
  114. // Send request to convert book
  115. $this->requestConvert($format);
  116. }
  117. // This method should be called from callback when conversion is done
  118. public function converted($email, $password, $key, $status, $ver, $format)
  119. {
  120. global $secret;
  121. if (!$key)
  122. throw new Exception("Invalid key.", self::ERR_ILLEGAL_ARG);
  123. if ($status != "r" and $status != "e")
  124. throw new Exception("Invalid status.", self::ERR_ILLEGAL_ARG);
  125. // check password
  126. if ($password != md5($secret . $key))
  127. throw new Exception("Invalid password.", self::ERR_ILLEGAL_ARG);
  128. if (!self::TEST_MODE)
  129. {
  130. // update status in the DB
  131. $db = getDBObject();
  132. if (!$db->updateBookStatus($key, $status, $ver, $format))
  133. error_log("FB2PDF ERROR. Unable to update book status. Key=$key");
  134. // send email to user
  135. $this->notifyUserByEmail($email, $key, $status);
  136. }
  137. }
  138. //Check the converted status for specific format
  139. public function checkConverted($key, $format) {
  140. $this->bookKey = $key;
  141. return $this->checkFormat($format);
  142. }
  143. // Convert file
  144. private function convert($filePath, $fileName, $format)
  145. {
  146. $this->zipFile = null;
  147. $this->fbFile = null;
  148. // Extract fb2 from zip
  149. $zipArr = $this->unzip($filePath);
  150. if ($zipArr === false) // not a zip file
  151. {
  152. $this->zipFile = null;
  153. $this->fbFile = $filePath;
  154. }
  155. else // zip file
  156. {
  157. $this->zipFile = $filePath;
  158. $this->fbFile = $zipArr["filePath"];
  159. $fileName = $zipArr["fileName"];
  160. }
  161. // Parse fb2
  162. $parser = new FBParser();
  163. $this->book = $parser->parse($this->fbFile);
  164. if ($this->book === false)
  165. throw new Exception("$fileName is not a fb2 or zip file", self::ERR_FORMAT);
  166. // genarate unique book key
  167. $this->bookKey = md5(uniqid(""));
  168. // get md5 of the file content (
  169. // NOTE! Here is a BUG. We should calculate md5 based on full book content (md5file), but we can do it only after reconverting all books.
  170. $md5 = md5($fileName);
  171. // get the filename without extension
  172. $this->fileName = $this->getBaseFileName($fileName);
  173. if (!$this->fileName)
  174. $this->fileName = $this->bookKey . ".zip";
  175. // Process book
  176. if (!self::TEST_MODE)
  177. {
  178. if (!$this->checkBook($md5))
  179. {
  180. $this->insertBook($md5);
  181. }
  182. $status = $this->checkFormat($format);
  183. if ($status == self::DB_BOOK_CONVERTED) // book is up-to-date
  184. {
  185. $this->notifyUserByEmail($this->email, $this->bookKey, "r");
  186. return;
  187. }
  188. if ($status == self::DB_BOOK_NOT_FOUND)
  189. {
  190. $this->insertFormat($format);
  191. }
  192. else if ($status == self::DB_BOOK_RECONVERT)
  193. {
  194. // Prepare book for reconverting
  195. $this->updateFormat($format);
  196. }
  197. // Send request to convert book
  198. $this->requestConvert($format);
  199. }
  200. }
  201. // Extract fb2 from zip file.
  202. // Returns associative array ("fileName" - name of the file, "filePath" - local path to unzipped fb2 file) or false if the file is not zip archive.
  203. private function unzip($zipfile)
  204. {
  205. $ret = false;
  206. $zip = new dUnzip2($zipfile);
  207. $zip->debug = false;
  208. $list = $zip->getList();
  209. if ($list)
  210. {
  211. foreach($list as $fileName=>$zippedFile)
  212. {
  213. if (substr($fileName, -1) != "/") // it's not a DIR
  214. {
  215. // unzip to a temporary file
  216. $tempFile = $this->tempFileName();
  217. $zip->unzip($fileName, $tempFile);
  218. $ret = array("fileName"=>$fileName, "filePath"=>$tempFile);
  219. break;
  220. }
  221. }
  222. }
  223. $zip->close();
  224. return $ret;
  225. }
  226. private function checkBook($md5)
  227. {
  228. $db = getDBObject();
  229. $bookInfo = $db->getBookByMd5($md5);
  230. if ($bookInfo)
  231. {
  232. $this->bookKey = $bookInfo["storage_key"];
  233. return TRUE;
  234. }
  235. return FALSE;
  236. }
  237. // Insert a new book to be converted
  238. private function insertBook($md5)
  239. {
  240. global $awsS3Bucket;
  241. // save fb2 file
  242. $s3 = getS3Object();
  243. $httpHeaders = array("Content-Disposition"=>"attachement; filename=\"$this->fileName.fb2\"");
  244. if (!$s3->writeFile($awsS3Bucket, $this->bookKey . ".fb2", $this->fbFile, "application/fb2+xml", "public-read", "", $httpHeaders))
  245. throw new Exception("Unable to store file $this->bookKey.fb2 in the Amazon S3 storage.", self::ERR_CONVERT);
  246. // save to DB
  247. $db = getDBObject();
  248. if (!$db->insertBook($this->bookKey, $this->book->author, $this->book->title, $this->book->isbn, $md5))
  249. {
  250. error_log("FB2PDF ERROR. Unable to insert book with key $this->bookKey.zip into DB.");
  251. // do not stop if DB is failed!
  252. }
  253. }
  254. // Update an existing book to be reconverted
  255. private function updateFormat($format)
  256. {
  257. global $awsS3Bucket;
  258. // update book status in the DB
  259. $db = getDBObject();
  260. if (!$db->updateBookStatus($this->bookKey, "p", 0, $format))
  261. {
  262. error_log("FB2PDF ERROR. Callback: Unable to update book status. Key=$$this->bookKey");
  263. // do not stop if DB is failed!
  264. }
  265. $s3 = getS3Object();
  266. $zipFile = getStorageName($this->bookKey, $format, ".zip");
  267. if (!$s3->deleteObject($awsS3Bucket, $zipFile))
  268. {
  269. error_log("FB2PDF ERROR. Unable to delete converted file $zipFile from the Amazon S3 storage.");
  270. // do not stop if failed!
  271. }
  272. $txtFile = getStorageName($this->bookKey, $format, ".txt");
  273. if (!$s3->deleteObject($awsS3Bucket, $txtFile))
  274. {
  275. error_log("FB2PDF ERROR. Unable to delete log file $txtFile from the Amazon S3 storage.");
  276. // do not stop if failed!
  277. }
  278. }
  279. // Returns status of specified format
  280. private function checkFormat($format)
  281. {
  282. global $convVersion;
  283. $db = getDBObject();
  284. // check if this book already exists
  285. $status = self::DB_BOOK_NOT_FOUND;
  286. $bookInfo = $db->getBookStatus($this->bookKey, $format);
  287. if ($bookInfo)
  288. {
  289. // check error status and converter version
  290. $bookStatus = $bookInfo["status"];
  291. $bookVer = $bookInfo["conv_ver"];
  292. if (($bookStatus == 'r' and $bookVer >= $convVersion) or $bookStatus == 'p')
  293. {
  294. $status = self::DB_BOOK_CONVERTED;
  295. }
  296. else
  297. {
  298. error_log("FB2PDF INFO. Books $this->bookKey needs to be converted again. Status=$bookStatus, Version=$bookVer");
  299. $status = self::DB_BOOK_RECONVERT;
  300. }
  301. }
  302. return $status;
  303. }
  304. // Returns TRUE if specified format was successfully inserted
  305. private function insertFormat($format)
  306. {
  307. $db = getDBObject();
  308. if (!$db->insertBookFormat($this->bookKey, $format))
  309. {
  310. error_log("FB2PDF ERROR. Unable to insert book format $format for book $this->bookKey into DB.");
  311. return TRUE;
  312. }
  313. return FALSE;
  314. }
  315. // Send request to convert book
  316. private function requestConvert($format)
  317. {
  318. global $awsS3Bucket, $secret;
  319. // send SQS message
  320. $callbackUrl = getFullUrl("conv_callback.php");
  321. $db = getDBObject();
  322. $formatInfo = $db->getFormat($format);
  323. $formatParams = $db->getFormatParameters($format);
  324. $fileType = $formatInfo["file_type"];
  325. $contentType = $formatInfo["content_type"];
  326. if(!sqsPutMessage($this->bookKey, "http://s3.amazonaws.com/$awsS3Bucket/$this->bookKey.fb2",
  327. $this->fileName, $callbackUrl, md5($secret . $this->bookKey), $this->email,
  328. $format, $formatParams, $fileType, $contentType))
  329. throw new Exception("Unable to send Amazon SQS message for key $this->bookKey.", self::ERR_CONVERT);
  330. }
  331. // Send notification email to user
  332. private function notifyUserByEmail($email, $key, $status)
  333. {
  334. if ($email)
  335. {
  336. $statusUrl = getFullUrl("status.php?key=$key");
  337. $subject = "Your book";
  338. $message = "<html><body>";
  339. if ($status == "r")
  340. $message .= "Ваша книга была успешно сконвертированна.";
  341. else if ($status == "e")
  342. $message .= "При конвертации Вашей книги произошла ошибка.";
  343. $message .= "<br><a href=\"$statusUrl\">Посмотреть результат конвертации</a>";
  344. $message .= "</body></html>";
  345. $headers = 'MIME-Version: 1.0' . "\r\n";
  346. $headers .= 'Content-type: text/html; charset=utf-8' . "\r\n";
  347. $headers .= 'From: FB2PDF <noreply@codeminders.com>' . "\r\n";
  348. mail($this->email, $subject, $message, $headers);
  349. }
  350. }
  351. // Cleanup temporarly stored files
  352. private function cleanupTempFiles()
  353. {
  354. if ($this->zipFile and !unlink($this->zipFile))
  355. error_log("FB2PDF WARN. Unable to remove temporary file $this->zipFile");
  356. if ($this->fbFile and !unlink($this->fbFile))
  357. error_log("FB2PDF WARN. Unable to remove temporary file $this->fbFile");
  358. }
  359. // Generate temporary file name
  360. private function tempFileName()
  361. {
  362. return tempnam(md5(uniqid(rand(), TRUE)), '');
  363. }
  364. // Returns filename without path and extension
  365. private function getBaseFileName($fileName)
  366. {
  367. $pathParts = pathinfo($fileName);
  368. $name = removeExt($pathParts["basename"]);
  369. return $name;
  370. }
  371. }
  372. ?>