PageRenderTime 62ms CodeModel.GetById 32ms RepoModel.GetById 1ms app.codeStats 0ms

/filestore/lib/Model/File.php

https://github.com/atk4/atk4-addons
PHP | 436 lines | 277 code | 43 blank | 116 comment | 24 complexity | a552e8de5f9372d7edf062970d852f15 MD5 | raw file
  1. <?php
  2. namespace filestore;
  3. class Model_File extends \SQL_Model
  4. {
  5. public $table = 'filestore_file';
  6. public $title_field = 'original_filename';
  7. // used Model classes
  8. public $type_model_class = 'filestore/Model_Type';
  9. public $volume_model_class = 'filestore/Model_Volume';
  10. public $magic_file = null; // path to magic database file used in finfo-open(), null = default
  11. public $import_mode = null;
  12. public $import_source = null;
  13. // set this to true, will allow to upload all file types
  14. // and will automatically create the type record for it
  15. public $policy_add_new_type = false;
  16. // set this to true, if you want to enable "soft delete", then only field
  17. // filestore_file.deleted will be set to true and files will not be
  18. // physically deleted
  19. public $policy_soft_delete = false;
  20. // Initially we store 4000 files per node until we reach 256 nodes.
  21. // After that we will determine node to use by modding filecounter.
  22. // @see generateFilename()
  23. protected $min_files_per_node = 4000;
  24. function init()
  25. {
  26. parent::init();
  27. // add fields
  28. $this->hasOne($this->type_model_class, 'filestore_type_id')
  29. ->caption('File Type')
  30. ->mandatory(true)
  31. ->sortable(true)
  32. ;
  33. $this->hasOne($this->volume_model_class, 'filestore_volume_id', false)
  34. ->caption('Volume')
  35. ->mandatory(true)
  36. ->sortable(true)
  37. ;
  38. $this->addField('original_filename')
  39. ->caption('Original Name')
  40. ->type('text')
  41. ->mandatory(true)
  42. ->sortable(true)
  43. ;
  44. $this->addField('filename')
  45. ->caption('Internal Name')
  46. ->mandatory(true)
  47. ->system(true)
  48. ->sortable(true)
  49. ;
  50. $this->addField('filesize')
  51. ->caption('File size')
  52. ->type('int')
  53. ->mandatory(true)
  54. ->defaultValue(0)
  55. ->sortable(true)
  56. ;
  57. $this->addField('deleted')
  58. ->caption('Deleted')
  59. ->type('boolean')
  60. ->mandatory(true)
  61. ->defaultValue(false)
  62. ->sortable(true)
  63. ;
  64. // join volume table and add fields from it
  65. $this->vol = $this->leftJoin('filestore_volume');
  66. $this->vol->addField('dirname')
  67. ->caption('Folder')
  68. ->mandatory(true)
  69. ->sortable(true)
  70. ->editable(false)
  71. ->display(array('form'=>'Readonly'))
  72. ;
  73. // calculated fields
  74. $this->addExpression('url')
  75. ->set(array($this,'getURLExpr'))
  76. ->caption('URL')
  77. ->sortable(true)
  78. ->editable(false)
  79. ->display(array('form'=>'Readonly'))
  80. ;
  81. // soft delete
  82. if ($this->policy_soft_delete) {
  83. $this->addCondition('deleted', '<>', 1);
  84. }
  85. // hooks
  86. $this->addHook('beforeSave', $this);
  87. $this->addHook('beforeDelete', $this);
  88. }
  89. /**
  90. * Produces expression which calculates full URL of image
  91. *
  92. * @param Model $m
  93. * @param DSQL $q
  94. *
  95. * @return DSQL
  96. */
  97. function getURLExpr($m,$q)
  98. {
  99. return $q->concat(
  100. @$m->api->pm->base_path,
  101. $m->getElement('dirname'),
  102. "/",
  103. $m->getElement('filename')
  104. );
  105. }
  106. /**
  107. * Before save hook
  108. *
  109. * @param Model $m
  110. *
  111. * @return void
  112. */
  113. function beforeSave($m)
  114. {
  115. // if new record, then choose volume and generate name
  116. if (!$m->loaded()) {
  117. // volume
  118. $m->set('filestore_volume_id', $m->getAvailableVolumeID());
  119. // generate random original_filename in case you import file contents as string
  120. if (! $m['original_filename']) {
  121. $m->set('original_filename', mt_rand());
  122. }
  123. // generate filename (with relative path)
  124. $m->set('filename', $m->generateFilename());
  125. }
  126. // perform import itself
  127. if ($m->import_mode) {
  128. $m->performImport();
  129. }
  130. }
  131. /**
  132. * Return available volume ID
  133. *
  134. * @return int
  135. */
  136. function getAvailableVolumeID()
  137. {
  138. // Determine best suited volume and returns it's ID
  139. $c = $this->ref("filestore_volume_id")
  140. ->addCondition('enabled', true)
  141. ->addCondition('stored_files_cnt', '<', 4096*256*256)
  142. ;
  143. $id = $c->dsql('select')
  144. ->field($this->id_field)
  145. ->order($this->id_field, 'asc') // to properly fill volumes, if multiple
  146. ->limit(1)
  147. ->getOne();
  148. if ($id !== null) {
  149. $c->tryLoad($id);
  150. }
  151. if (!$c->loaded()) {
  152. throw $this->exception('No volumes available. All of them are full or not enabled.');
  153. }
  154. /*
  155. if(disk_free_space($c->get('dirname') < $filesize)){
  156. throw new Exception_ForUser('Out of disk space on volume '.$id);
  157. }
  158. */
  159. return $id;
  160. }
  161. /**
  162. * Return file type ID
  163. *
  164. * @param string $mime_type
  165. * @param bool $add
  166. *
  167. * @return int
  168. */
  169. function getFiletypeID($mime_type = null, $add = false)
  170. {
  171. if ($mime_type === null) {
  172. $path = $this->get('filename') ? $this->getPath() : $this->import_source;
  173. if (!$path) {
  174. throw $this->exception('Load file entry from filestore or import');
  175. }
  176. if (!function_exists('finfo_open')) {
  177. throw $this->exception('You have to enable php_fileinfo extension of PHP.');
  178. }
  179. $finfo = finfo_open(FILEINFO_MIME_TYPE, $this->magic_file);
  180. if ($finfo === false) {
  181. throw $this->exception("Can't find magic_file with finfo_open().")
  182. ->addMoreInfo('Magic_file: ',isnull($this->magic_file) ? 'default' : $this->magic_file);
  183. }
  184. $mime_type = finfo_file($finfo, $path);
  185. finfo_close($finfo);
  186. }
  187. $c = $this->ref("filestore_type_id");
  188. $data = $c->getBy('mime_type', $mime_type);
  189. if (!$data['id'] && $add) {
  190. // automatically add new MIME type
  191. $c->set(array("mime_type" => $mime_type, "name" => $mime_type, "allow" => true));
  192. $c->save();
  193. $data = $c->get();
  194. } elseif (!$data['id'] || !$data['allow']) {
  195. // not allowed MIME type
  196. throw $this->exception(
  197. sprintf(
  198. $this->api->_('This file type is not allowed for upload (%s) or you are exceeding maximum file size'),
  199. $mime_type
  200. ), 'Exception_ForUser')
  201. ->addMoreInfo('type',$mime_type);
  202. }
  203. return $data['id'];
  204. }
  205. /**
  206. * Generate filename
  207. *
  208. * @return string
  209. */
  210. function generateFilename()
  211. {
  212. $this->hook("beforeGenerateFilename");
  213. if ($filename = $this->get("filename")) {
  214. return $filename;
  215. }
  216. $v = $this->ref('filestore_volume_id'); //won't work because of MVCFieldDefinition, line 304, loaded() check
  217. $dirname = $v->get('dirname');
  218. $seq = $v->getFileNumber();
  219. // Initially we store $min_files_per_node files per node until we reach 256 nodes.
  220. // After that we will determine node to use by modding filecounter.
  221. // This method ensures we don't create too many directories initially
  222. // and will grow files in directories indefinitely.
  223. $limit = $this->min_files_per_node * 256;
  224. if ($seq < $limit){
  225. $node = floor($seq / $this->min_files_per_node);
  226. } else {
  227. $node = $seq % 256;
  228. }
  229. $d = $dirname . '/' . dechex($node);
  230. if (!is_dir($d)) {
  231. mkdir($d);
  232. chmod($d, $this->api->getConfig('filestore/chmod', 0770));
  233. }
  234. // Generate temporary file
  235. // $file = basename(tempnam($d, 'fs'));
  236. // File name generation for store in file system, example: 20130201110338_5-myfile.jpg
  237. $cnt = (int) @$this->api->_filestore_unique_file++;
  238. $file = date("YmdHis") . '_' . $cnt . '_' . $this->convertName($this['original_filename']);
  239. $fp = @fopen($d . '/' . $file, "w");
  240. @fclose($fp);
  241. // Verify that file was created
  242. if (!file_exists($d . '/' . $file)) {
  243. throw $this->exception('Could not create file')
  244. ->addMoreInfo('path', $d)
  245. ->addMoreInfo('file', $file);
  246. }
  247. return dechex($node) . '/' . $file;
  248. }
  249. /**
  250. * Remove special characters in filename, replace spaces with -, trim and
  251. * set all characters to lowercase
  252. *
  253. * @param string $str
  254. *
  255. * @return string
  256. */
  257. function convertName($str)
  258. {
  259. $clean = iconv('UTF-8', 'ASCII//TRANSLIT', $str);
  260. $clean = preg_replace("/[^a-zA-Z0-9.\/_|+ -]/", '', $clean);
  261. $clean = strtolower(trim($clean, '-'));
  262. $clean = preg_replace("/[\/_|+ -]+/", '-', $clean);
  263. return $clean;
  264. }
  265. /**
  266. * Import file
  267. *
  268. * @param string $source
  269. * @param string $mode
  270. *
  271. * @return this
  272. */
  273. function import($source, $mode = 'upload')
  274. {
  275. /*
  276. Import file from different location.
  277. $mode can be
  278. - upload - for moving uploaded files. (additional validations apply)
  279. - move - original file will be removed
  280. - copy - original file will be kept
  281. - string - data is passed inside $source and is not an existant file
  282. */
  283. $this->import_source = $source;
  284. $this->import_mode = $mode;
  285. if ($this->loaded() && $this->id) {// -- if we have this, then
  286. // we can import right now
  287. // If file is already in database - put it into store
  288. $this->performImport();
  289. $this->save();
  290. }
  291. return $this;
  292. }
  293. /**
  294. * Return path
  295. *
  296. * @return string
  297. */
  298. function getPath()
  299. {
  300. $path =
  301. $this->ref("filestore_volume_id")->get("dirname") .
  302. "/" .
  303. $this['filename'];
  304. return $path;
  305. }
  306. /**
  307. * Return MIME type
  308. *
  309. * @return string
  310. */
  311. function getMimeType()
  312. {
  313. return $this->ref('filestore_type_id')
  314. ->get('mime_type');
  315. }
  316. /**
  317. * Perform import
  318. *
  319. * @return this
  320. */
  321. function performImport()
  322. {
  323. // After our filename is determined - performs the operation
  324. $destination = $this->getPath();
  325. switch ($this->import_mode) {
  326. case 'upload':
  327. move_uploaded_file($this->import_source, $destination);
  328. break;
  329. case 'move':
  330. rename($this->import_source, $destination);
  331. break;
  332. case 'copy':
  333. copy($this->import_source, $destination);
  334. break;
  335. case 'string':
  336. $fd = fopen($destination, 'w');
  337. fwrite($fd, $this->import_source);
  338. fclose($fd);
  339. break;
  340. case 'none': // file is already in place
  341. break;
  342. default:
  343. throw $this->exception('Incorrect import mode specified.')
  344. ->addMoreInfo('specified mode', $this->import_mode);
  345. }
  346. chmod($destination, $this->api->getConfig('filestore/chmod', 0660));
  347. clearstatcache();
  348. $this->set('filesize', filesize($destination));
  349. $this->set('deleted', false);
  350. $this->set('filestore_type_id', $this->getFiletypeID(null, $this->policy_add_new_type));
  351. $this->import_source = null;
  352. $this->import_mode = null;
  353. return $this;
  354. }
  355. /**
  356. * Delete file from file system before deleting it from DB
  357. *
  358. * @return void
  359. */
  360. function beforeDelete()
  361. {
  362. if (!$this->policy_soft_delete) {
  363. $file = $this->getPath();
  364. if (file_exists($file)) {
  365. unlink($file);
  366. }
  367. }
  368. }
  369. /**
  370. * Deletes record matching the ID (implementation of soft delete)
  371. *
  372. * @param int $id
  373. *
  374. * @return this
  375. */
  376. function delete($id=null)
  377. {
  378. if ($this->policy_soft_delete) {
  379. if(!is_null($id))$this->load($id);
  380. if(!$this->loaded())throw $this->exception('Unable to determine which record to delete');
  381. $this->set('deleted', true)->saveAndUnload();
  382. return $this;
  383. } else {
  384. return parent::delete($id);
  385. }
  386. }
  387. }