1: <?php
2: /**
3: * VFS API for abstracted file storage and access.
4: *
5: * Copyright 2002-2012 Horde LLC (http://www.horde.org/)
6: *
7: * See the enclosed file COPYING for license information (LGPL). If you
8: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
9: *
10: * @author Chuck Hagenbuch <chuck@horde.org>
11: * @package Vfs
12: */
13: abstract class Horde_Vfs_Base
14: {
15: /**
16: * Hash containing connection parameters.
17: *
18: * @var array
19: */
20: protected $_params = array();
21:
22: /**
23: * List of additional credentials required for this VFS backend (example:
24: * For FTP, we need a username and password to log in to the server with).
25: *
26: * @var array
27: */
28: protected $_credentials = array();
29:
30: /**
31: * List of permissions and if they can be changed in this VFS backend.
32: *
33: * @var array
34: */
35: protected $_permissions = array(
36: 'owner' => array(
37: 'read' => false,
38: 'write' => false,
39: 'execute' => false
40: ),
41: 'group' => array(
42: 'read' => false,
43: 'write' => false,
44: 'execute' => false
45: ),
46: 'all' => array(
47: 'read' => false,
48: 'write' => false,
49: 'execute' => false
50: )
51: );
52:
53: /**
54: * The current size, in bytes, of the VFS tree.
55: *
56: * @var integer
57: */
58: protected $_vfsSize = null;
59:
60: /**
61: * Constructor.
62: *
63: * @param array $params A hash containing connection parameters.
64: */
65: public function __construct($params = array())
66: {
67: $this->setParams(array(
68: 'user' => '',
69: 'vfs_quotalimit' => -1,
70: 'vfs_quotaroot' => ''
71: ));
72: $this->setParams($params);
73: }
74:
75: /**
76: * Checks the credentials that we have by calling _connect(), to see if
77: * there is a valid login.
78: *
79: * @throws Horde_Vfs_Exception
80: */
81: public function checkCredentials()
82: {
83: $this->_connect();
84: }
85:
86: /**
87: * TODO
88: *
89: * @throws Horde_Vfs_Exception
90: */
91: protected function _connect()
92: {
93: }
94:
95: /**
96: * Sets configuration parameters.
97: *
98: * @param array $params An associative array with parameter names as
99: * keys.
100: */
101: public function setParams($params = array())
102: {
103: $this->_params = array_merge($this->_params, $params);
104: }
105:
106: /**
107: * Returns configuration parameters.
108: *
109: * @param string $name The parameter to return.
110: *
111: * @return mixed The parameter value or null if it doesn't exist.
112: */
113: public function getParam($name)
114: {
115: return isset($this->_params[$name])
116: ? $this->_params[$name]
117: : null;
118: }
119:
120: /**
121: * Retrieves the size of a file from the VFS.
122: *
123: * @abstract
124: *
125: * @param string $path The pathname to the file.
126: * @param string $name The filename to retrieve.
127: *
128: * @return integer The file size.
129: * @throws Horde_Vfs_Exception
130: */
131: public function size($path, $name)
132: {
133: throw new Horde_Vfs_Exception('Not supported.');
134: }
135:
136: /**
137: * Returns the size of a folder
138: *
139: * @param string $path The path to the folder.
140: * @param string $name The name of the folder.
141: *
142: * @return integer The size of the folder, in bytes.
143: * @throws Horde_Vfs_Exception
144: */
145: public function getFolderSize($path = null, $name = null)
146: {
147: $size = 0;
148: $root = (!is_null($path) ? $path . '/' : '') . $name;
149: $object_list = $this->listFolder($root, null, true, false, true);
150:
151: foreach ($object_list as $key => $val) {
152: $size += isset($val['subdirs'])
153: ? $this->getFolderSize($root, $key)
154: : $this->size($root, $key);
155: }
156:
157: return $size;
158: }
159:
160: /**
161: * Retrieves a file from the VFS.
162: *
163: * @abstract
164: *
165: * @param string $path The pathname to the file.
166: * @param string $name The filename to retrieve.
167: *
168: * @return string The file data.
169: * @throws Horde_Vfs_Exception
170: */
171: public function read($path, $name)
172: {
173: throw new Horde_Vfs_Exception('Not supported.');
174: }
175:
176: /**
177: * Retrieves a file from the VFS as an on-disk local file.
178: *
179: * This function provides a file on local disk with the data of a VFS file
180: * in it. This file <em>cannot</em> be modified! The behavior if you do
181: * modify it is undefined. It will be removed at the end of the request.
182: *
183: * @param string $path The pathname to the file.
184: * @param string $name The filename to retrieve.
185: *
186: * @return string A local filename.
187: * @throws Horde_Vfs_Exception
188: */
189: public function readFile($path, $name)
190: {
191: // Create a temporary file and register it for deletion at the
192: // end of this request.
193: if (!($localFile = Horde_Util::getTempFile('vfs'))) {
194: throw new Horde_Vfs_Exception('Unable to create temporary file.');
195: }
196:
197: if (is_callable(array($this, 'readStream'))) {
198: // Use a stream from the VFS if possible, to avoid reading all data
199: // into memory.
200: $stream = $this->readStream($path, $name);
201:
202: if (!($localStream = fopen($localFile, 'w'))) {
203: throw new Horde_Vfs_Exception('Unable to open temporary file.');
204: }
205: stream_copy_to_stream($stream, $localStream);
206: fclose($localStream);
207: } else {
208: // We have to read all of the data in one shot.
209: $data = $this->read($path, $name);
210: file_put_contents($localFile, $data);
211: }
212:
213: // $localFile now has $path/$name's data in it.
214: return $localFile;
215: }
216:
217: /**
218: * Retrieves a part of a file from the VFS. Particularly useful when
219: * reading large files which would exceed the PHP memory limits if they
220: * were stored in a string.
221: *
222: * @abstract
223: *
224: * @param string $path The pathname to the file.
225: * @param string $name The filename to retrieve.
226: * @param integer $offset The offset of the part. (The new offset will
227: * be stored in here).
228: * @param integer $length The length of the part. If the length = -1,
229: * the whole part after the offset is retrieved.
230: * If more bytes are given as exists after the
231: * given offset. Only the available bytes are
232: * read.
233: * @param integer $remaining The bytes that are left, after the part that
234: * is retrieved.
235: *
236: * @return string The file data.
237: * @throws Horde_Vfs_Exception
238: */
239: public function readByteRange($path, $name, &$offset, $length, &$remaining)
240: {
241: throw new Horde_Vfs_Exception('Not supported.');
242: }
243:
244: /**
245: * Stores a file in the VFS.
246: *
247: * @abstract
248: *
249: * @param string $path The path to store the file in.
250: * @param string $name The filename to use.
251: * @param string $tmpFile The temporary file containing the data to
252: * be stored.
253: * @param boolean $autocreate Automatically create directories?
254: *
255: * @throws Horde_Vfs_Exception
256: */
257: public function write($path, $name, $tmpFile, $autocreate = false)
258: {
259: throw new Horde_Vfs_Exception('Not supported.');
260: }
261:
262: /**
263: * Stores a file in the VFS from raw data.
264: *
265: * @abstract
266: *
267: * @param string $path The path to store the file in.
268: * @param string $name The filename to use.
269: * @param string $data The file data.
270: * @param boolean $autocreate Automatically create directories?
271: *
272: * @throws Horde_Vfs_Exception
273: */
274: public function writeData($path, $name, $data, $autocreate = false)
275: {
276: throw new Horde_Vfs_Exception('Not supported.');
277: }
278:
279: /**
280: * Moves a file through the backend.
281: *
282: * @param string $path The path of the original file.
283: * @param string $name The name of the original file.
284: * @param string $dest The destination file name.
285: * @param boolean $autocreate Automatically create directories?
286: *
287: * @throws Horde_Vfs_Exception
288: */
289: public function move($path, $name, $dest, $autocreate = false)
290: {
291: $this->copy($path, $name, $dest, $autocreate);
292: $this->deleteFile($path, $name);
293: }
294:
295: /**
296: * Copies a file through the backend.
297: *
298: * @param string $path The path of the original file.
299: * @param string $name The name of the original file.
300: * @param string $dest The name of the destination directory.
301: * @param boolean $autocreate Automatically create directories?
302: *
303: * @throws Horde_Vfs_Exception
304: */
305: public function copy($path, $name, $dest, $autocreate = false)
306: {
307: $orig = $this->_getPath($path, $name);
308: if (preg_match('|^' . preg_quote($orig) . '/?$|', $dest)) {
309: throw new Horde_Vfs_Exception('Cannot copy file(s) - source and destination are the same.');
310: }
311:
312: if ($autocreate) {
313: $this->autocreatePath($dest);
314: }
315:
316: if ($this->isFolder($path, $name)) {
317: $this->_copyRecursive($path, $name, $dest);
318: } else {
319: $this->writeData($dest, $name, $this->read($path, $name), $autocreate);
320: }
321: }
322:
323: /**
324: * Recursively copies a directory through the backend.
325: *
326: * @param string $path The path of the original file.
327: * @param string $name The name of the original file.
328: * @param string $dest The name of the destination directory.
329: *
330: * @throws Horde_Vfs_Exception
331: */
332: protected function _copyRecursive($path, $name, $dest)
333: {
334: $this->createFolder($dest, $name);
335:
336: $file_list = $this->listFolder($this->_getPath($path, $name));
337: foreach ($file_list as $file) {
338: $this->copy($this->_getPath($path, $name), $file['name'], $this->_getPath($dest, $name));
339: }
340: }
341:
342: /**
343: * Alias to deleteFile()
344: */
345: public function delete($path, $name)
346: {
347: return $this->deleteFile($path, $name);
348: }
349:
350: /**
351: * Deletes a file from the VFS.
352: *
353: * @abstract
354: *
355: * @param string $path The path to delete the file from.
356: * @param string $name The filename to delete.
357: *
358: * @throws Horde_Vfs_Exception
359: */
360: public function deleteFile($path, $name)
361: {
362: throw new Horde_Vfs_Exception('Not supported.');
363: }
364:
365: /**
366: * Renames a file in the VFS.
367: *
368: * @abstract
369: *
370: * @param string $oldpath The old path to the file.
371: * @param string $oldname The old filename.
372: * @param string $newpath The new path of the file.
373: * @param string $newname The new filename.
374: *
375: * @throws Horde_Vfs_Exception
376: */
377: public function rename($oldpath, $oldname, $newpath, $newname)
378: {
379: throw new Horde_Vfs_Exception('Not supported.');
380: }
381:
382: /**
383: * Returns if a given file or folder exists in a folder.
384: *
385: * @param string $path The path to the folder.
386: * @param string $name The file or folder name.
387: *
388: * @return boolean True if it exists, false otherwise.
389: */
390: public function exists($path, $name)
391: {
392: try {
393: $list = $this->listFolder($path);
394: return isset($list[$name]);
395: } catch (Horde_Vfs_Exception $e) {
396: return false;
397: }
398: }
399:
400: /**
401: * Creates a folder in the VFS.
402: *
403: * @abstract
404: *
405: * @param string $path The parent folder.
406: * @param string $name The name of the new folder.
407: *
408: * @throws Horde_Vfs_Exception
409: */
410: public function createFolder($path, $name)
411: {
412: throw new Horde_Vfs_Exception('Not supported.');
413: }
414:
415: /**
416: * Automatically creates any necessary parent directories in the specified
417: * $path.
418: *
419: * @param string $path The VFS path to autocreate.
420: *
421: * @throws Horde_Vfs_Exception
422: */
423: public function autocreatePath($path)
424: {
425: $dirs = explode('/', $path);
426: if (is_array($dirs)) {
427: $cur = '/';
428: foreach ($dirs as $dir) {
429: if (!strlen($dir)) {
430: continue;
431: }
432: if (!$this->isFolder($cur, $dir)) {
433: $result = $this->createFolder($cur, $dir);
434: }
435: if ($cur != '/') {
436: $cur .= '/';
437: }
438: $cur .= $dir;
439: }
440: }
441: }
442:
443: /**
444: * Checks if a given item is a folder.
445: *
446: * @param string $path The parent folder.
447: * @param string $name The item name.
448: *
449: * @return boolean True if it is a folder, false otherwise.
450: */
451: public function isFolder($path, $name)
452: {
453: try {
454: $folderList = $this->listFolder($path, null, true, true);
455: return isset($folderList[$name]);
456: } catch (Horde_Vfs_Exception $e) {
457: return false;
458: }
459: }
460:
461: /**
462: * Deletes a folder from the VFS.
463: *
464: * @abstract
465: *
466: * @param string $path The parent folder.
467: * @param string $name The name of the folder to delete.
468: * @param boolean $recursive Force a recursive delete?
469: *
470: * @throws Horde_Vfs_Exception
471: */
472: public function deleteFolder($path, $name, $recursive = false)
473: {
474: throw new Horde_Vfs_Exception('Not supported.');
475: }
476:
477: /**
478: * Recursively remove all files and subfolders from the given
479: * folder.
480: *
481: * @param string $path The path of the folder to empty.
482: *
483: * @throws Horde_Vfs_Exception
484: */
485: public function emptyFolder($path)
486: {
487: // Get and delete the subfolders.
488: foreach ($this->listFolder($path, null, true, true) as $folder) {
489: $this->deleteFolder($path, $folder['name'], true);
490: }
491:
492: // Only files are left, get and delete them.
493: foreach ($this->listFolder($path) as $file) {
494: $this->deleteFile($path, $file['name']);
495: }
496: }
497:
498: /**
499: * Returns a file list of the directory passed in.
500: *
501: * @param string $path The path of the directory.
502: * @param mixed $filter String/hash to filter file/dirname on.
503: * @param boolean $dotfiles Show dotfiles?
504: * @param boolean $dironly Show only directories?
505: * @param boolean $recursive Return all directory levels recursively?
506: *
507: * @return array File list.
508: * @throws Horde_Vfs_Exception
509: */
510: public function listFolder($path, $filter = null, $dotfiles = true,
511: $dironly = false, $recursive = false)
512: {
513: $list = $this->_listFolder($path, $filter, $dotfiles, $dironly);
514: if (!$recursive) {
515: return $list;
516: }
517:
518: if (strlen($path)) {
519: $path .= '/';
520: }
521: foreach ($list as $name => $values) {
522: if ($values['type'] == '**dir') {
523: $list[$name]['subdirs'] = $this->listFolder($path . $name, $filter, $dotfiles, $dironly, $recursive);
524: }
525: }
526:
527: return $list;
528: }
529:
530: /**
531: * Returns an an unsorted file list of the specified directory.
532: *
533: * @abstract
534: *
535: * @param string $path The path of the directory.
536: * @param mixed $filter String/hash to filter file/dirname on.
537: * @param boolean $dotfiles Show dotfiles?
538: * @param boolean $dironly Show only directories?
539: *
540: * @return array File list.
541: * @throws Horde_Vfs_Exception
542: */
543: protected function _listFolder($path, $filter = null, $dotfiles = true,
544: $dironly = false)
545: {
546: throw new Horde_Vfs_Exception('Not supported.');
547: }
548:
549: /**
550: * Returns the current working directory of the VFS backend.
551: *
552: * @return string The current working directory.
553: */
554: public function getCurrentDirectory()
555: {
556: return '';
557: }
558:
559: /**
560: * Returns whether or not a filename matches any filter element.
561: *
562: * @param mixed $filter String/hash to build the regular expression
563: * from.
564: * @param string $filename String containing the filename to match.
565: *
566: * @return boolean True on match, false on no match.
567: */
568: protected function _filterMatch($filter, $filename)
569: {
570: $namefilter = null;
571:
572: // Build a regexp based on $filter.
573: if (!is_null($filter)) {
574: $namefilter = '/';
575: if (is_array($filter)) {
576: $once = false;
577: foreach ($filter as $item) {
578: if ($once !== true) {
579: $once = true;
580: } else {
581: $namefilter .= '|';
582: }
583: $namefilter .= '(' . $item . ')';
584: }
585: } else {
586: $namefilter .= '(' . $filter . ')';
587: }
588: $namefilter .= '/';
589: }
590:
591: $match = false;
592: if (!is_null($namefilter)) {
593: $match = preg_match($namefilter, $filename);
594: }
595:
596: return $match;
597: }
598:
599: /**
600: * Changes permissions for an item on the VFS.
601: *
602: * @abstract
603: *
604: * @param string $path The parent folder of the item.
605: * @param string $name The name of the item.
606: * @param string $permission The permission to set.
607: *
608: * @throws Horde_Vfs_Exception
609: */
610: public function changePermissions($path, $name, $permission)
611: {
612: throw new Horde_Vfs_Exception('Not supported.');
613: }
614:
615: /**
616: * Returns a sorted list of folders in the specified directory.
617: *
618: * @abstract
619: *
620: * @param string $path The path of the directory to get the
621: * directory list for.
622: * @param mixed $filter Hash of items to filter based on folderlist.
623: * @param boolean $dotfolders Include dotfolders?
624: *
625: * @return array Folder list.
626: * @throws Horde_Vfs_Exception
627: */
628: public function listFolders($path = '', $filter = null, $dotfolders = true)
629: {
630: throw new Horde_Vfs_Exception('Not supported.');
631: }
632:
633: /**
634: * Returns the list of additional credentials required, if any.
635: *
636: * @return array Credential list.
637: */
638: public function getRequiredCredentials()
639: {
640: return array_diff($this->_credentials, array_keys($this->_params));
641: }
642:
643: /**
644: * Returns an array specifying what permissions are changeable for this
645: * VFS implementation.
646: *
647: * @return array Changeable permisions.
648: */
649: public function getModifiablePermissions()
650: {
651: return $this->_permissions;
652: }
653:
654: /**
655: * Returns the size of the VFS item.
656: *
657: * @return integer The size, in bytes, of the VFS item.
658: */
659: public function getVFSSize()
660: {
661: if (is_null($this->_vfsSize)) {
662: $this->_vfsSize = $this->getFolderSize($this->_params['vfs_quotaroot']);
663: }
664: return $this->_vfsSize;
665: }
666:
667: /**
668: * Sets the VFS quota limit.
669: *
670: * @param integer $quota The limit to apply.
671: * @param integer $metric The metric to multiply the quota into.
672: */
673: public function setQuota($quota, $metric = Horde_Vfs::QUOTA_METRIC_BYTE)
674: {
675: switch ($metric) {
676: case Horde_Vfs::QUOTA_METRIC_KB:
677: $quota *= pow(2, 10);
678: break;
679:
680: case Horde_Vfs::QUOTA_METRIC_MB:
681: $quota *= pow(2, 20);
682: break;
683:
684: case Horde_Vfs::QUOTA_METRIC_GB:
685: $quota *= pow(2, 30);
686: break;
687: }
688:
689: $this->_params['vfs_quotalimit'] = $quota;
690: }
691:
692: /**
693: * Sets the VFS quota root.
694: *
695: * @param string $dir The root directory for the quota determination.
696: */
697: public function setQuotaRoot($dir)
698: {
699: $this->_params['vfs_quotaroot'] = $dir;
700: }
701:
702: /**
703: * Get quota information (used/allocated), in bytes.
704: *
705: * @return mixed An associative array.
706: * <pre>
707: * 'limit' = Maximum quota allowed
708: * 'usage' = Currently used portion of quota (in bytes)
709: * </pre>
710: * @throws Horde_Vfs_Exception
711: */
712: public function getQuota()
713: {
714: if ($this->_params['vfs_quotalimit'] == -1) {
715: throw new Horde_Vfs_Exception('No quota set.');
716: }
717:
718: return array(
719: 'limit' => $this->_params['vfs_quotalimit'],
720: 'usage' => $this->getVFSSize()
721: );
722: }
723:
724: /**
725: * Checks the quota when preparing to write data.
726: *
727: * @param string $mode Either 'string' or 'file'. If 'string', $data is
728: * the data to be written. If 'file', $data is the
729: * filename containing the data to be written.
730: * @param string $data Either the data or the filename to the data.
731: *
732: * @throws Horde_Vfs_Exception
733: */
734: protected function _checkQuotaWrite($mode, $data)
735: {
736: if ($this->_params['vfs_quotalimit'] == -1 &&
737: is_null($this->_vfsSize)) {
738: return;
739: }
740:
741: if ($mode == 'file') {
742: $filesize = filesize($data);
743: if ($filesize === false) {
744: throw new Horde_Vfs_Exception('Unable to read VFS file (filesize() failed).');
745: }
746: } else {
747: $filesize = strlen($data);
748: }
749:
750: $vfssize = $this->getVFSSize();
751: if ($this->_params['vfs_quotalimit'] > -1 &&
752: ($vfssize + $filesize) > $this->_params['vfs_quotalimit']) {
753: throw new Horde_Vfs_Exception('Unable to write VFS file, quota will be exceeded.');
754: }
755:
756: if (!is_null($this->_vfsSize)) {
757: $this->_vfsSize += $filesize;
758: }
759: }
760:
761: /**
762: * Checks the quota when preparing to delete data.
763: *
764: * @param string $path The path the file is located in.
765: * @param string $name The filename.
766: *
767: * @throws Horde_Vfs_Exception
768: */
769: protected function _checkQuotaDelete($path, $name)
770: {
771: if (!is_null($this->_vfsSize)) {
772: $this->_vfsSize -= $this->size($path, $name);
773: }
774: }
775:
776: /**
777: * Returns the full path of an item.
778: *
779: * @param string $path The path of directory of the item.
780: * @param string $name The name of the item.
781: *
782: * @return mixed Full path when $path isset and just $name when not set.
783: */
784: protected function _getPath($path, $name)
785: {
786: if (strlen($path) > 0) {
787: if (substr($path, -1) == '/') {
788: return $path . $name;
789: }
790: return $path . '/' . $name;
791: }
792:
793: return $name;
794: }
795: }
796: