filecache.php 14 KB

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