Resumable.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <?php
  2. namespace Dilab;
  3. use Cake\Filesystem\File;
  4. use Cake\Filesystem\Folder;
  5. use Dilab\Network\Request;
  6. use Dilab\Network\Response;
  7. use Monolog\Logger;
  8. use Monolog\Handler\StreamHandler;
  9. class Resumable
  10. {
  11. public $debug = false;
  12. public $tempFolder = 'tmp';
  13. public $uploadFolder = 'test/files/uploads';
  14. // for testing
  15. public $deleteTmpFolder = true;
  16. protected $request;
  17. protected $response;
  18. protected $params;
  19. protected $chunkFile;
  20. protected $log;
  21. protected $filename;
  22. protected $filepath;
  23. protected $extension;
  24. protected $originalFilename;
  25. protected $isUploadComplete = false;
  26. const WITHOUT_EXTENSION = true;
  27. public function __construct(Request $request, Response $response)
  28. {
  29. $this->request = $request;
  30. $this->response = $response;
  31. $this->log = new Logger('debug');
  32. $this->log->pushHandler(new StreamHandler('debug.log', Logger::DEBUG));
  33. $this->preProcess();
  34. }
  35. // sets original filename and extenstion, blah blah
  36. public function preProcess()
  37. {
  38. if (!empty($this->resumableParams())) {
  39. if (!empty($this->request->file())) {
  40. $this->extension = $this->findExtension($this->resumableParam('filename'));
  41. $this->originalFilename = $this->resumableParam('filename');
  42. }
  43. }
  44. }
  45. public function process()
  46. {
  47. if (!empty($this->resumableParams())) {
  48. if (!empty($this->request->file())) {
  49. $this->handleChunk();
  50. } else {
  51. $this->handleTestChunk();
  52. }
  53. }
  54. }
  55. /**
  56. * Get isUploadComplete
  57. *
  58. * @return boolean
  59. */
  60. public function isUploadComplete()
  61. {
  62. return $this->isUploadComplete;
  63. }
  64. /**
  65. * Set final filename.
  66. *
  67. * @param string Final filename
  68. */
  69. public function setFilename($filename)
  70. {
  71. $this->filename = $filename;
  72. return $this;
  73. }
  74. /**
  75. * Get final filename.
  76. *
  77. * @return string Final filename
  78. */
  79. public function getFilename()
  80. {
  81. return $this->filename;
  82. }
  83. /**
  84. * Get final filename.
  85. *
  86. * @return string Final filename
  87. */
  88. public function getOriginalFilename($withoutExtension = false)
  89. {
  90. if ($withoutExtension === static::WITHOUT_EXTENSION) {
  91. return $this->removeExtension($this->originalFilename);
  92. } else {
  93. return $this->originalFilename;
  94. }
  95. }
  96. /**
  97. * Get final filapath.
  98. *
  99. * @return string Final filename
  100. */
  101. public function getFilepath()
  102. {
  103. return $this->filepath;
  104. }
  105. /**
  106. * Get final extension.
  107. *
  108. * @return string Final extension name
  109. */
  110. public function getExtension()
  111. {
  112. return $this->extension;
  113. }
  114. /**
  115. * Makes sure the orginal extension never gets overriden by user defined filename.
  116. *
  117. * @param string User defined filename
  118. * @param string Original filename
  119. * @return string Filename that always has an extension from the original file
  120. */
  121. private function createSafeFilename($filename, $originalFilename)
  122. {
  123. $filename = $this->removeExtension($filename);
  124. $extension = $this->findExtension($originalFilename);
  125. return sprintf('%s.%s', $filename, $extension);
  126. }
  127. public function handleTestChunk()
  128. {
  129. $identifier = $this->resumableParam('identifier');
  130. $filename = $this->resumableParam('filename');
  131. $chunkNumber = $this->resumableParam('chunkNumber');
  132. if (!$this->isChunkUploaded($identifier, $filename, $chunkNumber)) {
  133. return $this->response->header(204);
  134. } else {
  135. return $this->response->header(200);
  136. }
  137. }
  138. public function handleChunk()
  139. {
  140. $file = $this->request->file();
  141. $identifier = $this->resumableParam('identifier');
  142. $filename = $this->resumableParam('filename');
  143. $chunkNumber = $this->resumableParam('chunkNumber');
  144. $chunkSize = $this->resumableParam('chunkSize');
  145. $totalSize = $this->resumableParam('totalSize');
  146. if (!$this->isChunkUploaded($identifier, $filename, $chunkNumber)) {
  147. $chunkFile = $this->tmpChunkDir($identifier) . DIRECTORY_SEPARATOR . $this->tmpChunkFilename($filename, $chunkNumber);
  148. $this->moveUploadedFile($file['tmp_name'], $chunkFile);
  149. }
  150. if ($this->isFileUploadComplete($filename, $identifier, $chunkSize, $totalSize)) {
  151. $this->isUploadComplete = true;
  152. $this->createFileAndDeleteTmp($identifier, $filename);
  153. }
  154. return $this->response->header(200);
  155. }
  156. /**
  157. * Create the final file from chunks
  158. */
  159. private function createFileAndDeleteTmp($identifier, $filename)
  160. {
  161. $tmpFolder = new Folder($this->tmpChunkDir($identifier));
  162. $chunkFiles = $tmpFolder->read(true, true, true)[1];
  163. // if the user has set a custom filename
  164. if (null !== $this->filename) {
  165. $finalFilename = $this->createSafeFilename($this->filename, $filename);
  166. } else {
  167. $finalFilename = $filename;
  168. }
  169. // replace filename reference by the final file
  170. $this->filepath = $this->uploadFolder . DIRECTORY_SEPARATOR . $finalFilename;
  171. $this->extension = $this->findExtension($this->filepath);
  172. if ($this->createFileFromChunks($chunkFiles, $this->filepath) && $this->deleteTmpFolder) {
  173. $tmpFolder->delete();
  174. $this->uploadComplete = true;
  175. }
  176. }
  177. private function resumableParam($shortName)
  178. {
  179. $resumableParams = $this->resumableParams();
  180. if (!isset($resumableParams['resumable' . ucfirst($shortName)])) {
  181. return null;
  182. }
  183. return $resumableParams['resumable' . ucfirst($shortName)];
  184. }
  185. public function resumableParams()
  186. {
  187. if ($this->request->is('get')) {
  188. return $this->request->data('get');
  189. }
  190. if ($this->request->is('post')) {
  191. return $this->request->data('post');
  192. }
  193. }
  194. public function isFileUploadComplete($filename, $identifier, $chunkSize, $totalSize)
  195. {
  196. if ($chunkSize <= 0) {
  197. return false;
  198. }
  199. $numOfChunks = intval($totalSize / $chunkSize) + ($totalSize % $chunkSize == 0 ? 0 : 1);
  200. for ($i = 1; $i < $numOfChunks; $i++) {
  201. if (!$this->isChunkUploaded($identifier, $filename, $i)) {
  202. return false;
  203. }
  204. }
  205. return true;
  206. }
  207. public function isChunkUploaded($identifier, $filename, $chunkNumber)
  208. {
  209. $file = new File($this->tmpChunkDir($identifier) . DIRECTORY_SEPARATOR . $this->tmpChunkFilename($filename, $chunkNumber));
  210. return $file->exists();
  211. }
  212. public function tmpChunkDir($identifier)
  213. {
  214. $tmpChunkDir = $this->tempFolder . DIRECTORY_SEPARATOR . $identifier;
  215. if (!file_exists($tmpChunkDir)) {
  216. mkdir($tmpChunkDir);
  217. }
  218. return $tmpChunkDir;
  219. }
  220. public function tmpChunkFilename($filename, $chunkNumber)
  221. {
  222. return $filename . '.' . str_pad($chunkNumber, 4, 0, STR_PAD_LEFT);
  223. }
  224. public function getExclusiveFileHandle($name)
  225. {
  226. // if the file exists, fopen() will raise a warning
  227. $previous_error_level = error_reporting();
  228. error_reporting(E_ERROR);
  229. $handle = fopen($name, 'x');
  230. error_reporting($previous_error_level);
  231. return $handle;
  232. }
  233. public function createFileFromChunks($chunkFiles, $destFile)
  234. {
  235. $this->log('Beginning of create files from chunks');
  236. natsort($chunkFiles);
  237. $handle = $this->getExclusiveFileHandle9$destFile);
  238. if (!$handle) {
  239. return false;
  240. }
  241. $destFile = new File($destFile);
  242. $destFile->handle = $handle;
  243. foreach ($chunkFiles as $chunkFile) {
  244. $file = new File($chunkFile);
  245. $destFile->append($file->read());
  246. $this->log('Append ', ['chunk file' => $chunkFile]);
  247. }
  248. $this->log('End of create files from chunks');
  249. return $destFile->exists();
  250. }
  251. public function moveUploadedFile($file, $destFile)
  252. {
  253. $file = new File($file);
  254. if ($file->exists()) {
  255. return $file->copy($destFile);
  256. }
  257. return false;
  258. }
  259. public function setRequest($request)
  260. {
  261. $this->request = $request;
  262. }
  263. public function setResponse($response)
  264. {
  265. $this->response = $response;
  266. }
  267. private function log($msg, $ctx = array())
  268. {
  269. if ($this->debug) {
  270. $this->log->addDebug($msg, $ctx);
  271. }
  272. }
  273. private function findExtension($filename)
  274. {
  275. $parts = explode('.', basename($filename));
  276. return end($parts);
  277. }
  278. private function removeExtension($filename)
  279. {
  280. $parts = explode('.', basename($filename));
  281. $ext = end($parts); // get extension
  282. // remove extension from filename if any
  283. return str_replace(sprintf('.%s', $ext), '', $filename);
  284. }
  285. }