filecache.php 16 KB

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