filecache.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. <?php
  2. /**
  3. * @author Robin Appelman
  4. * @copyright 2011 Robin Appelman icewind1991@gmail.com
  5. *
  6. * This library is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or any later version.
  10. *
  11. * This library is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public
  17. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
  18. *
  19. */
  20. /**
  21. * provide caching for filesystem info in the database
  22. *
  23. * not used by OC_Filesystem for reading filesystem info,
  24. * instread apps should use OC_FileCache::get where possible
  25. *
  26. * It will try to keep the data up to date but changes from outside ownCloud can invalidate the cache
  27. */
  28. class OC_FileCache{
  29. /**
  30. * get the filesystem info from the cache
  31. * @param string path
  32. * @param string root (optional)
  33. * @return array
  34. *
  35. * returns an associative array with the following keys:
  36. * - size
  37. * - mtime
  38. * - ctime
  39. * - mimetype
  40. * - encrypted
  41. * - versioned
  42. */
  43. public static function get($path, $root=false) {
  44. if(OC_FileCache_Update::hasUpdated($path, $root)) {
  45. if($root===false) {//filesystem hooks are only valid for the default root
  46. OC_Hook::emit('OC_Filesystem', 'post_write', array('path'=>$path));
  47. }else{
  48. OC_FileCache_Update::update($path, $root);
  49. }
  50. }
  51. return OC_FileCache_Cached::get($path, $root);
  52. }
  53. /**
  54. * put filesystem info in the cache
  55. * @param string $path
  56. * @param array data
  57. * @param string root (optional)
  58. *
  59. * $data is an assiciative array in the same format as returned by get
  60. */
  61. public static function put($path, $data, $root=false) {
  62. if($root===false) {
  63. $root=OC_Filesystem::getRoot();
  64. }
  65. $fullpath=OC_Filesystem::normalizePath($root.'/'.$path);
  66. $parent=self::getParentId($fullpath);
  67. $id=self::getId($fullpath, '');
  68. if(isset(OC_FileCache_Cached::$savedData[$fullpath])) {
  69. $data=array_merge(OC_FileCache_Cached::$savedData[$fullpath], $data);
  70. unset(OC_FileCache_Cached::$savedData[$fullpath]);
  71. }
  72. if($id!=-1) {
  73. self::update($id, $data);
  74. return;
  75. }
  76. // add parent directory to the file cache if it does not exist yet.
  77. if ($parent == -1 && $fullpath != $root) {
  78. $parentDir = dirname($path);
  79. self::scanFile($parentDir);
  80. $parent = self::getParentId($fullpath);
  81. }
  82. if(!isset($data['size']) or !isset($data['mtime'])) {//save incomplete data for the next time we write it
  83. OC_FileCache_Cached::$savedData[$fullpath]=$data;
  84. return;
  85. }
  86. if(!isset($data['encrypted'])) {
  87. $data['encrypted']=false;
  88. }
  89. if(!isset($data['versioned'])) {
  90. $data['versioned']=false;
  91. }
  92. $mimePart=dirname($data['mimetype']);
  93. $data['size']=(int)$data['size'];
  94. $data['ctime']=(int)$data['mtime'];
  95. $data['writable']=(int)$data['writable'];
  96. $data['encrypted']=(int)$data['encrypted'];
  97. $data['versioned']=(int)$data['versioned'];
  98. $user=OC_User::getUser();
  99. $query=OC_DB::prepare('INSERT INTO `*PREFIX*fscache`(`parent`, `name`, `path`, `path_hash`, `size`, `mtime`, `ctime`, `mimetype`, `mimepart`,`user`,`writable`,`encrypted`,`versioned`) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)');
  100. $result=$query->execute(array($parent, basename($fullpath), $fullpath, md5($fullpath), $data['size'], $data['mtime'], $data['ctime'], $data['mimetype'], $mimePart, $user, $data['writable'], $data['encrypted'], $data['versioned']));
  101. if(OC_DB::isError($result)) {
  102. OC_Log::write('files', 'error while writing file('.$fullpath.') to cache', OC_Log::ERROR);
  103. }
  104. if($cache=OC_Cache::getUserCache(true)) {
  105. $cache->remove('fileid/'.$fullpath);//ensure we don't have -1 cached
  106. }
  107. }
  108. /**
  109. * update filesystem info of a file
  110. * @param int $id
  111. * @param array $data
  112. */
  113. private static function update($id, $data) {
  114. $arguments=array();
  115. $queryParts=array();
  116. foreach(array('size','mtime','ctime','mimetype','encrypted','versioned', 'writable') as $attribute) {
  117. if(isset($data[$attribute])) {
  118. //Convert to int it args are false
  119. if($data[$attribute] === false) {
  120. $arguments[] = 0;
  121. }else{
  122. $arguments[] = $data[$attribute];
  123. }
  124. $queryParts[]='`'.$attribute.'`=?';
  125. }
  126. }
  127. if(isset($data['mimetype'])) {
  128. $arguments[]=dirname($data['mimetype']);
  129. $queryParts[]='`mimepart`=?';
  130. }
  131. $arguments[]=$id;
  132. if(!empty($queryParts)) {
  133. $sql = 'UPDATE `*PREFIX*fscache` SET '.implode(' , ', $queryParts).' WHERE `id`=?';
  134. $query=OC_DB::prepare($sql);
  135. $result=$query->execute($arguments);
  136. if(OC_DB::isError($result)) {
  137. OC_Log::write('files', 'error while updating file('.$id.') in cache', OC_Log::ERROR);
  138. }
  139. }
  140. }
  141. /**
  142. * register a file move in the cache
  143. * @param string oldPath
  144. * @param string newPath
  145. * @param string root (optional)
  146. */
  147. public static function move($oldPath, $newPath, $root=false) {
  148. if($root===false) {
  149. $root=OC_Filesystem::getRoot();
  150. }
  151. // If replacing an existing file, delete the file
  152. if (self::inCache($newPath, $root)) {
  153. self::delete($newPath, $root);
  154. }
  155. $oldPath=$root.$oldPath;
  156. $newPath=$root.$newPath;
  157. $newParent=self::getParentId($newPath);
  158. $query=OC_DB::prepare('UPDATE `*PREFIX*fscache` SET `parent`=? ,`name`=?, `path`=?, `path_hash`=? WHERE `path_hash`=?');
  159. $query->execute(array($newParent, basename($newPath), $newPath, md5($newPath), md5($oldPath)));
  160. if(($cache=OC_Cache::getUserCache(true)) && $cache->hasKey('fileid/'.$oldPath)) {
  161. $cache->set('fileid/'.$newPath, $cache->get('fileid/'.$oldPath));
  162. $cache->remove('fileid/'.$oldPath);
  163. }
  164. $query=OC_DB::prepare('SELECT `path` FROM `*PREFIX*fscache` WHERE `path` LIKE ?');
  165. $oldLength=strlen($oldPath);
  166. $updateQuery=OC_DB::prepare('UPDATE `*PREFIX*fscache` SET `path`=?, `path_hash`=? WHERE `path_hash`=?');
  167. while($row= $query->execute(array($oldPath.'/%'))->fetchRow()) {
  168. $old=$row['path'];
  169. $new=$newPath.substr($old, $oldLength);
  170. $updateQuery->execute(array($new, md5($new), md5($old)));
  171. if(($cache=OC_Cache::getUserCache(true)) && $cache->hasKey('fileid/'.$old)) {
  172. $cache->set('fileid/'.$new, $cache->get('fileid/'.$old));
  173. $cache->remove('fileid/'.$old);
  174. }
  175. }
  176. }
  177. /**
  178. * delete info from the cache
  179. * @param string path
  180. * @param string root (optional)
  181. */
  182. public static function delete($path, $root=false) {
  183. if($root===false) {
  184. $root=OC_Filesystem::getRoot();
  185. }
  186. $query=OC_DB::prepare('DELETE FROM `*PREFIX*fscache` WHERE `path_hash`=?');
  187. $query->execute(array(md5($root.$path)));
  188. //delete everything inside the folder
  189. $query=OC_DB::prepare('DELETE FROM `*PREFIX*fscache` WHERE `path` LIKE ?');
  190. $query->execute(array($root.$path.'/%'));
  191. OC_Cache::remove('fileid/'.$root.$path);
  192. }
  193. /**
  194. * return array of filenames matching the querty
  195. * @param string $query
  196. * @param boolean $returnData
  197. * @param string root (optional)
  198. * @return array of filepaths
  199. */
  200. public static function search($search, $returnData=false, $root=false) {
  201. if($root===false) {
  202. $root=OC_Filesystem::getRoot();
  203. }
  204. $rootLen=strlen($root);
  205. if(!$returnData) {
  206. $select = '`path`';
  207. }else{
  208. $select = '*';
  209. }
  210. if (OC_Config::getValue('dbtype') === 'oci8') {
  211. $where = 'LOWER(`name`) LIKE LOWER(?) AND `user`=?';
  212. } else {
  213. $where = '`name` LIKE ? AND `user`=?';
  214. }
  215. $query=OC_DB::prepare('SELECT '.$select.' FROM `*PREFIX*fscache` WHERE '.$where);
  216. $result=$query->execute(array("%$search%", OC_User::getUser()));
  217. $names=array();
  218. while($row=$result->fetchRow()) {
  219. if(!$returnData) {
  220. $names[]=substr($row['path'], $rootLen);
  221. }else{
  222. $row['path']=substr($row['path'], $rootLen);
  223. $names[]=$row;
  224. }
  225. }
  226. return $names;
  227. }
  228. /**
  229. * get all files and folders in a folder
  230. * @param string path
  231. * @param string root (optional)
  232. * @return array
  233. *
  234. * returns an array of assiciative arrays with the following keys:
  235. * - name
  236. * - size
  237. * - mtime
  238. * - ctime
  239. * - mimetype
  240. * - encrypted
  241. * - versioned
  242. */
  243. public static function getFolderContent($path, $root=false, $mimetype_filter='') {
  244. if(OC_FileCache_Update::hasUpdated($path, $root, true)) {
  245. OC_FileCache_Update::updateFolder($path, $root);
  246. }
  247. return OC_FileCache_Cached::getFolderContent($path, $root, $mimetype_filter);
  248. }
  249. /**
  250. * check if a file or folder is in the cache
  251. * @param string $path
  252. * @param string root (optional)
  253. * @return bool
  254. */
  255. public static function inCache($path, $root=false) {
  256. return self::getId($path, $root)!=-1;
  257. }
  258. /**
  259. * get the file id as used in the cache
  260. * @param string path
  261. * @param string root (optional)
  262. * @return int
  263. */
  264. public static function getId($path, $root=false) {
  265. if($root===false) {
  266. $root=OC_Filesystem::getRoot();
  267. }
  268. $fullPath=$root.$path;
  269. if(($cache=OC_Cache::getUserCache(true)) && $cache->hasKey('fileid/'.$fullPath)) {
  270. return $cache->get('fileid/'.$fullPath);
  271. }
  272. $query=OC_DB::prepare('SELECT `id` FROM `*PREFIX*fscache` WHERE `path_hash`=?');
  273. $result=$query->execute(array(md5($fullPath)));
  274. if(OC_DB::isError($result)) {
  275. OC_Log::write('files', 'error while getting file id of '.$path, OC_Log::ERROR);
  276. return -1;
  277. }
  278. $result=$result->fetchRow();
  279. if(is_array($result)) {
  280. $id=$result['id'];
  281. }else{
  282. $id=-1;
  283. }
  284. if($cache=OC_Cache::getUserCache(true)) {
  285. $cache->set('fileid/'.$fullPath, $id);
  286. }
  287. return $id;
  288. }
  289. /**
  290. * get the file path from the id, relative to the home folder of the user
  291. * @param int id
  292. * @param string user (optional)
  293. * @return string
  294. */
  295. public static function getPath($id, $user='') {
  296. if(!$user) {
  297. $user=OC_User::getUser();
  298. }
  299. $query=OC_DB::prepare('SELECT `path` FROM `*PREFIX*fscache` WHERE `id`=? AND `user`=?');
  300. $result=$query->execute(array($id, $user));
  301. $row=$result->fetchRow();
  302. $path=$row['path'];
  303. $root='/'.$user.'/files';
  304. if(substr($path, 0, strlen($root))!=$root) {
  305. return false;
  306. }
  307. return substr($path, strlen($root));
  308. }
  309. /**
  310. * get the file id of the parent folder, taking into account '/' has no parent
  311. * @param string $path
  312. * @return int
  313. */
  314. private static function getParentId($path) {
  315. if($path=='/') {
  316. return -1;
  317. }else{
  318. return self::getId(dirname($path), '');
  319. }
  320. }
  321. /**
  322. * adjust the size of the parent folders
  323. * @param string $path
  324. * @param int $sizeDiff
  325. * @param string root (optinal)
  326. */
  327. public static function increaseSize($path, $sizeDiff, $root=false) {
  328. if($sizeDiff==0) return;
  329. $item = OC_FileCache_Cached::get($path);
  330. //stop walking up the filetree if we hit a non-folder
  331. if($item['mimetype'] !== 'httpd/unix-directory'){
  332. return;
  333. }
  334. $id = $item['id'];
  335. while($id!=-1) {//walk up the filetree increasing the size of all parent folders
  336. $query=OC_DB::prepare('UPDATE `*PREFIX*fscache` SET `size`=`size`+? WHERE `id`=?');
  337. $query->execute(array($sizeDiff, $id));
  338. if($path == '' or $path =='/'){
  339. return;
  340. }
  341. $path=dirname($path);
  342. $parent = OC_FileCache_Cached::get($path);
  343. $id = $parent['id'];
  344. //stop walking up the filetree if we hit a non-folder
  345. if($parent['mimetype'] !== 'httpd/unix-directory'){
  346. return;
  347. }
  348. }
  349. }
  350. /**
  351. * recursively scan the filesystem and fill the cache
  352. * @param string $path
  353. * @param OC_EventSource $eventSource (optional)
  354. * @param int $count (optional)
  355. * @param string $root (optional)
  356. */
  357. public static function scan($path, $eventSource=false,&$count=0, $root=false) {
  358. if($eventSource) {
  359. $eventSource->send('scanning', array('file'=>$path, 'count'=>$count));
  360. }
  361. $lastSend=$count;
  362. // NOTE: Ugly hack to prevent shared files from going into the cache (the source already exists somewhere in the cache)
  363. if (substr($path, 0, 7) == '/Shared') {
  364. return;
  365. }
  366. if($root===false) {
  367. $view=OC_Filesystem::getView();
  368. }else{
  369. $view=new OC_FilesystemView($root);
  370. }
  371. self::scanFile($path, $root);
  372. $dh=$view->opendir($path.'/');
  373. $totalSize=0;
  374. if($dh) {
  375. while (($filename = readdir($dh)) !== false) {
  376. if($filename != '.' and $filename != '..') {
  377. $file=$path.'/'.$filename;
  378. if($view->is_dir($file.'/')) {
  379. self::scan($file, $eventSource, $count, $root);
  380. }else{
  381. $totalSize+=self::scanFile($file, $root);
  382. $count++;
  383. if($count>$lastSend+25 and $eventSource) {
  384. $lastSend=$count;
  385. $eventSource->send('scanning', array('file'=>$path, 'count'=>$count));
  386. }
  387. }
  388. }
  389. }
  390. }
  391. OC_FileCache_Update::cleanFolder($path, $root);
  392. self::increaseSize($path, $totalSize, $root);
  393. }
  394. /**
  395. * scan a single file
  396. * @param string path
  397. * @param string root (optional)
  398. * @return int size of the scanned file
  399. */
  400. public static function scanFile($path, $root=false) {
  401. // NOTE: Ugly hack to prevent shared files from going into the cache (the source already exists somewhere in the cache)
  402. if (substr($path, 0, 7) == '/Shared') {
  403. return;
  404. }
  405. if($root===false) {
  406. $view=OC_Filesystem::getView();
  407. }else{
  408. $view=new OC_FilesystemView($root);
  409. }
  410. if(!$view->is_readable($path)) return; //cant read, nothing we can do
  411. clearstatcache();
  412. $mimetype=$view->getMimeType($path);
  413. $stat=$view->stat($path);
  414. if($mimetype=='httpd/unix-directory') {
  415. $stat['size'] = 0;
  416. $writable=$view->is_writable($path.'/');
  417. }else{
  418. $writable=$view->is_writable($path);
  419. }
  420. $stat['mimetype']=$mimetype;
  421. $stat['writable']=$writable;
  422. if($path=='/') {
  423. $path='';
  424. }
  425. self::put($path, $stat, $root);
  426. return $stat['size'];
  427. }
  428. /**
  429. * find files by mimetype
  430. * @param string $part1
  431. * @param string $part2 (optional)
  432. * @param string root (optional)
  433. * @return array of file paths
  434. *
  435. * $part1 and $part2 together form the complete mimetype.
  436. * e.g. searchByMime('text', 'plain')
  437. *
  438. * seccond mimetype part can be ommited
  439. * e.g. searchByMime('audio')
  440. */
  441. public static function searchByMime($part1, $part2=null, $root=false) {
  442. if($root===false) {
  443. $root=OC_Filesystem::getRoot();
  444. }
  445. $rootLen=strlen($root);
  446. $root .= '%';
  447. $user=OC_User::getUser();
  448. if(!$part2) {
  449. $query=OC_DB::prepare('SELECT `path` FROM `*PREFIX*fscache` WHERE `mimepart`=? AND `user`=? AND `path` LIKE ?');
  450. $result=$query->execute(array($part1, $user, $root));
  451. }else{
  452. $query=OC_DB::prepare('SELECT `path` FROM `*PREFIX*fscache` WHERE `mimetype`=? AND `user`=? AND `path` LIKE ? ');
  453. $result=$query->execute(array($part1.'/'.$part2, $user, $root));
  454. }
  455. $names=array();
  456. while($row=$result->fetchRow()) {
  457. $names[]=substr($row['path'], $rootLen);
  458. }
  459. return $names;
  460. }
  461. /**
  462. * clean old pre-path_hash entries
  463. */
  464. public static function clean() {
  465. $query=OC_DB::prepare('DELETE FROM `*PREFIX*fscache` WHERE LENGTH(`path_hash`)<30');
  466. $query->execute();
  467. }
  468. /**
  469. * clear filecache entries
  470. * @param string user (optonal)
  471. */
  472. public static function clear($user='') {
  473. if($user) {
  474. $query=OC_DB::prepare('DELETE FROM `*PREFIX*fscache` WHERE `user`=?');
  475. $query->execute(array($user));
  476. }else{
  477. $query=OC_DB::prepare('DELETE FROM `*PREFIX*fscache`');
  478. $query->execute();
  479. }
  480. }
  481. /**
  482. * trigger an update for the cache by setting the mtimes to 0
  483. * @param string $user (optional)
  484. */
  485. public static function triggerUpdate($user='') {
  486. if($user) {
  487. $query=OC_DB::prepare('UPDATE `*PREFIX*fscache` SET `mtime`=0 WHERE `user`=? AND `mimetype`= ? ');
  488. $query->execute(array($user,'httpd/unix-directory'));
  489. }else{
  490. $query=OC_DB::prepare('UPDATE `*PREFIX*fscache` SET `mtime`=0 AND `mimetype`= ? ');
  491. $query->execute(array('httpd/unix-directory'));
  492. }
  493. }
  494. }
  495. //watch for changes and try to keep the cache up to date
  496. OC_Hook::connect('OC_Filesystem', 'post_write', 'OC_FileCache_Update', 'fileSystemWatcherWrite');
  497. OC_Hook::connect('OC_Filesystem', 'post_delete', 'OC_FileCache_Update', 'fileSystemWatcherDelete');
  498. OC_Hook::connect('OC_Filesystem', 'post_rename', 'OC_FileCache_Update', 'fileSystemWatcherRename');
  499. OC_Hook::connect('OC_User', 'post_deleteUser', 'OC_FileCache_Update', 'deleteFromUser');