filecache.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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=$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 = substr(dirname($path), 0, strrpos(dirname($path), DIRECTORY_SEPARATOR));
  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. $sql = 'UPDATE `*PREFIX*fscache` SET '.implode(' , ',$queryParts).' WHERE `id`=?';
  133. $query=OC_DB::prepare($sql);
  134. $result=$query->execute($arguments);
  135. if(OC_DB::isError($result)) {
  136. OC_Log::write('files','error while updating file('.$id.') in cache',OC_Log::ERROR);
  137. }
  138. }
  139. /**
  140. * register a file move in the cache
  141. * @param string oldPath
  142. * @param string newPath
  143. * @param string root (optional)
  144. */
  145. public static function move($oldPath,$newPath,$root=false) {
  146. if($root===false) {
  147. $root=OC_Filesystem::getRoot();
  148. }
  149. // If replacing an existing file, delete the file
  150. if (self::inCache($newPath, $root)) {
  151. self::delete($newPath, $root);
  152. }
  153. $oldPath=$root.$oldPath;
  154. $newPath=$root.$newPath;
  155. $newParent=self::getParentId($newPath);
  156. $query=OC_DB::prepare('UPDATE `*PREFIX*fscache` SET `parent`=? ,`name`=?, `path`=?, `path_hash`=? WHERE `path_hash`=?');
  157. $query->execute(array($newParent,basename($newPath),$newPath,md5($newPath),md5($oldPath)));
  158. if(($cache=OC_Cache::getUserCache(true)) && $cache->hasKey('fileid/'.$oldPath)) {
  159. $cache->set('fileid/'.$newPath,$cache->get('fileid/'.$oldPath));
  160. $cache->remove('fileid/'.$oldPath);
  161. }
  162. $query=OC_DB::prepare('SELECT `path` FROM `*PREFIX*fscache` WHERE `path` LIKE ?');
  163. $oldLength=strlen($oldPath);
  164. $updateQuery=OC_DB::prepare('UPDATE `*PREFIX*fscache` SET `path`=?, `path_hash`=? WHERE `path_hash`=?');
  165. while($row= $query->execute(array($oldPath.'/%'))->fetchRow()) {
  166. $old=$row['path'];
  167. $new=$newPath.substr($old,$oldLength);
  168. $updateQuery->execute(array($new,md5($new),md5($old)));
  169. if(($cache=OC_Cache::getUserCache(true)) && $cache->hasKey('fileid/'.$old)) {
  170. $cache->set('fileid/'.$new,$cache->get('fileid/'.$old));
  171. $cache->remove('fileid/'.$old);
  172. }
  173. }
  174. }
  175. /**
  176. * delete info from the cache
  177. * @param string path
  178. * @param string root (optional)
  179. */
  180. public static function delete($path,$root=false) {
  181. if($root===false) {
  182. $root=OC_Filesystem::getRoot();
  183. }
  184. $query=OC_DB::prepare('DELETE FROM `*PREFIX*fscache` WHERE `path_hash`=?');
  185. $query->execute(array(md5($root.$path)));
  186. //delete everything inside the folder
  187. $query=OC_DB::prepare('DELETE FROM `*PREFIX*fscache` WHERE `path` LIKE ?');
  188. $query->execute(array($root.$path.'/%'));
  189. OC_Cache::remove('fileid/'.$root.$path);
  190. }
  191. /**
  192. * return array of filenames matching the querty
  193. * @param string $query
  194. * @param boolean $returnData
  195. * @param string root (optional)
  196. * @return array of filepaths
  197. */
  198. public static function search($search,$returnData=false,$root=false) {
  199. if($root===false) {
  200. $root=OC_Filesystem::getRoot();
  201. }
  202. $rootLen=strlen($root);
  203. if(!$returnData) {
  204. $query=OC_DB::prepare('SELECT `path` FROM `*PREFIX*fscache` WHERE `name` LIKE ? AND `user`=?');
  205. }else{
  206. $query=OC_DB::prepare('SELECT * FROM `*PREFIX*fscache` WHERE `name` LIKE ? AND `user`=?');
  207. }
  208. $result=$query->execute(array("%$search%",OC_User::getUser()));
  209. $names=array();
  210. while($row=$result->fetchRow()) {
  211. if(!$returnData) {
  212. $names[]=substr($row['path'],$rootLen);
  213. }else{
  214. $row['path']=substr($row['path'],$rootLen);
  215. $names[]=$row;
  216. }
  217. }
  218. return $names;
  219. }
  220. /**
  221. * get all files and folders in a folder
  222. * @param string path
  223. * @param string root (optional)
  224. * @return array
  225. *
  226. * returns an array of assiciative arrays with the following keys:
  227. * - name
  228. * - size
  229. * - mtime
  230. * - ctime
  231. * - mimetype
  232. * - encrypted
  233. * - versioned
  234. */
  235. public static function getFolderContent($path,$root=false,$mimetype_filter='') {
  236. if(OC_FileCache_Update::hasUpdated($path,$root,true)) {
  237. OC_FileCache_Update::updateFolder($path,$root);
  238. }
  239. return OC_FileCache_Cached::getFolderContent($path,$root,$mimetype_filter);
  240. }
  241. /**
  242. * check if a file or folder is in the cache
  243. * @param string $path
  244. * @param string root (optional)
  245. * @return bool
  246. */
  247. public static function inCache($path,$root=false) {
  248. return self::getId($path,$root)!=-1;
  249. }
  250. /**
  251. * get the file id as used in the cache
  252. * @param string path
  253. * @param string root (optional)
  254. * @return int
  255. */
  256. public static function getId($path,$root=false) {
  257. if($root===false) {
  258. $root=OC_Filesystem::getRoot();
  259. }
  260. $fullPath=$root.$path;
  261. if(($cache=OC_Cache::getUserCache(true)) && $cache->hasKey('fileid/'.$fullPath)) {
  262. return $cache->get('fileid/'.$fullPath);
  263. }
  264. $query=OC_DB::prepare('SELECT `id` FROM `*PREFIX*fscache` WHERE `path_hash`=?');
  265. $result=$query->execute(array(md5($fullPath)));
  266. if(OC_DB::isError($result)) {
  267. OC_Log::write('files','error while getting file id of '.$path,OC_Log::ERROR);
  268. return -1;
  269. }
  270. $result=$result->fetchRow();
  271. if(is_array($result)) {
  272. $id=$result['id'];
  273. }else{
  274. $id=-1;
  275. }
  276. if($cache=OC_Cache::getUserCache(true)) {
  277. $cache->set('fileid/'.$fullPath,$id);
  278. }
  279. return $id;
  280. }
  281. /**
  282. * get the file path from the id, relative to the home folder of the user
  283. * @param int id
  284. * @param string user (optional)
  285. * @return string
  286. */
  287. public static function getPath($id,$user='') {
  288. if(!$user) {
  289. $user=OC_User::getUser();
  290. }
  291. $query=OC_DB::prepare('SELECT `path` FROM `*PREFIX*fscache` WHERE `id`=? AND `user`=?');
  292. $result=$query->execute(array($id,$user));
  293. $row=$result->fetchRow();
  294. $path=$row['path'];
  295. $root='/'.$user.'/files';
  296. if(substr($path,0,strlen($root))!=$root) {
  297. return false;
  298. }
  299. return substr($path,strlen($root));
  300. }
  301. /**
  302. * get the file id of the parent folder, taking into account '/' has no parent
  303. * @param string $path
  304. * @return int
  305. */
  306. private static function getParentId($path) {
  307. if($path=='/') {
  308. return -1;
  309. }else{
  310. return self::getId(dirname($path),'');
  311. }
  312. }
  313. /**
  314. * adjust the size of the parent folders
  315. * @param string $path
  316. * @param int $sizeDiff
  317. * @param string root (optinal)
  318. */
  319. public static function increaseSize($path,$sizeDiff, $root=false) {
  320. if($sizeDiff==0) return;
  321. $id=self::getId($path,$root);
  322. while($id!=-1) {//walk up the filetree increasing the size of all parent folders
  323. $query=OC_DB::prepare('UPDATE `*PREFIX*fscache` SET `size`=`size`+? WHERE `id`=?');
  324. $query->execute(array($sizeDiff,$id));
  325. $id=self::getParentId($path);
  326. $path=dirname($path);
  327. }
  328. }
  329. /**
  330. * recursively scan the filesystem and fill the cache
  331. * @param string $path
  332. * @param OC_EventSource $enventSource (optional)
  333. * @param int count (optional)
  334. * @param string root (optional)
  335. */
  336. public static function scan($path,$eventSource=false,&$count=0,$root=false) {
  337. if($eventSource) {
  338. $eventSource->send('scanning',array('file'=>$path,'count'=>$count));
  339. }
  340. $lastSend=$count;
  341. // NOTE: Ugly hack to prevent shared files from going into the cache (the source already exists somewhere in the cache)
  342. if (substr($path, 0, 7) == '/Shared') {
  343. return;
  344. }
  345. if($root===false) {
  346. $view=OC_Filesystem::getView();
  347. }else{
  348. $view=new OC_FilesystemView($root);
  349. }
  350. self::scanFile($path,$root);
  351. $dh=$view->opendir($path.'/');
  352. $totalSize=0;
  353. if($dh) {
  354. while (($filename = readdir($dh)) !== false) {
  355. if($filename != '.' and $filename != '..') {
  356. $file=$path.'/'.$filename;
  357. if($view->is_dir($file.'/')) {
  358. self::scan($file,$eventSource,$count,$root);
  359. }else{
  360. $totalSize+=self::scanFile($file,$root);
  361. $count++;
  362. if($count>$lastSend+25 and $eventSource) {
  363. $lastSend=$count;
  364. $eventSource->send('scanning',array('file'=>$path,'count'=>$count));
  365. }
  366. }
  367. }
  368. }
  369. }
  370. OC_FileCache_Update::cleanFolder($path,$root);
  371. self::increaseSize($path,$totalSize,$root);
  372. }
  373. /**
  374. * scan a single file
  375. * @param string path
  376. * @param string root (optional)
  377. * @return int size of the scanned file
  378. */
  379. public static function scanFile($path,$root=false) {
  380. // NOTE: Ugly hack to prevent shared files from going into the cache (the source already exists somewhere in the cache)
  381. if (substr($path, 0, 7) == '/Shared') {
  382. return;
  383. }
  384. if($root===false) {
  385. $view=OC_Filesystem::getView();
  386. }else{
  387. $view=new OC_FilesystemView($root);
  388. }
  389. if(!$view->is_readable($path)) return; //cant read, nothing we can do
  390. clearstatcache();
  391. $mimetype=$view->getMimeType($path);
  392. $stat=$view->stat($path);
  393. if($mimetype=='httpd/unix-directory') {
  394. $writable=$view->is_writable($path.'/');
  395. }else{
  396. $writable=$view->is_writable($path);
  397. }
  398. $stat['mimetype']=$mimetype;
  399. $stat['writable']=$writable;
  400. if($path=='/') {
  401. $path='';
  402. }
  403. self::put($path,$stat,$root);
  404. return $stat['size'];
  405. }
  406. /**
  407. * find files by mimetype
  408. * @param string $part1
  409. * @param string $part2 (optional)
  410. * @param string root (optional)
  411. * @return array of file paths
  412. *
  413. * $part1 and $part2 together form the complete mimetype.
  414. * e.g. searchByMime('text','plain')
  415. *
  416. * seccond mimetype part can be ommited
  417. * e.g. searchByMime('audio')
  418. */
  419. public static function searchByMime($part1,$part2=null,$root=false) {
  420. if($root===false) {
  421. $root=OC_Filesystem::getRoot();
  422. }
  423. $rootLen=strlen($root);
  424. $root .= '%';
  425. $user=OC_User::getUser();
  426. if(!$part2) {
  427. $query=OC_DB::prepare('SELECT `path` FROM `*PREFIX*fscache` WHERE `mimepart`=? AND `user`=? AND `path` LIKE ?');
  428. $result=$query->execute(array($part1,$user, $root));
  429. }else{
  430. $query=OC_DB::prepare('SELECT `path` FROM `*PREFIX*fscache` WHERE `mimetype`=? AND `user`=? AND `path` LIKE ? ');
  431. $result=$query->execute(array($part1.'/'.$part2,$user, $root));
  432. }
  433. $names=array();
  434. while($row=$result->fetchRow()) {
  435. $names[]=substr($row['path'],$rootLen);
  436. }
  437. return $names;
  438. }
  439. /**
  440. * clean old pre-path_hash entries
  441. */
  442. public static function clean() {
  443. $query=OC_DB::prepare('DELETE FROM `*PREFIX*fscache` WHERE LENGTH(`path_hash`)<30');
  444. $query->execute();
  445. }
  446. /**
  447. * clear filecache entries
  448. * @param string user (optonal)
  449. */
  450. public static function clear($user='') {
  451. if($user) {
  452. $query=OC_DB::prepare('DELETE FROM `*PREFIX*fscache` WHERE `user`=?');
  453. $query->execute(array($user));
  454. }else{
  455. $query=OC_DB::prepare('DELETE FROM `*PREFIX*fscache`');
  456. $query->execute();
  457. }
  458. }
  459. }
  460. //watch for changes and try to keep the cache up to date
  461. OC_Hook::connect('OC_Filesystem','post_write','OC_FileCache_Update','fileSystemWatcherWrite');
  462. OC_Hook::connect('OC_Filesystem','post_delete','OC_FileCache_Update','fileSystemWatcherDelete');
  463. OC_Hook::connect('OC_Filesystem','post_rename','OC_FileCache_Update','fileSystemWatcherRename');