2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * This plugin is used to access files on server file system
21 * @package repository_filesystem
22 * @copyright 2010 Dongsheng Cai {@link http://dongsheng.org}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 require_once($CFG->dirroot . '/repository/lib.php');
26 require_once($CFG->libdir . '/filelib.php');
29 * repository_filesystem class
31 * Create a repository from your local filesystem
32 * *NOTE* for security issue, we use a fixed repository path
33 * which is %moodledata%/repository
36 * @copyright 2009 Dongsheng Cai {@link http://dongsheng.org}
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 class repository_filesystem extends repository {
44 * @param int $repositoryid repository ID
45 * @param int $context context ID
46 * @param array $options
48 public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) {
50 parent::__construct($repositoryid, $context, $options);
51 $root = $CFG->dataroot . '/repository/';
52 $subdir = $this->get_option('fs_path');
54 $this->root_path = $root;
55 if (!empty($subdir)) {
56 $this->root_path .= $subdir . '/';
59 if (!empty($options['ajax'])) {
60 if (!is_dir($this->root_path)) {
61 $created = mkdir($this->root_path, $CFG->directorypermissions, true);
63 $ret['msg'] = get_string('invalidpath', 'repository_filesystem');
64 $ret['nosearch'] = true;
65 if ($options['ajax'] && !$created) {
66 echo json_encode($ret);
72 public function get_listing($path = '', $page = '') {
75 $list['list'] = array();
76 // process breacrumb trail
77 $list['path'] = array(
78 array('name'=>get_string('root', 'repository_filesystem'), 'path'=>'')
82 $parts = explode('/', $path);
83 if (count($parts) > 1) {
84 foreach ($parts as $part) {
86 $trail .= ('/'.$part);
87 $list['path'][] = array('name'=>$part, 'path'=>$trail);
91 $list['path'][] = array('name'=>$path, 'path'=>$path);
93 $this->root_path .= ($path.'/');
95 $list['manage'] = false;
96 $list['dynload'] = true;
97 $list['nologin'] = true;
98 $list['nosearch'] = true;
99 // retrieve list of files and directories and sort them
100 $fileslist = array();
102 if ($dh = opendir($this->root_path)) {
103 while (($file = readdir($dh)) != false) {
104 if ( $file != '.' and $file !='..') {
105 if (is_file($this->root_path.$file)) {
106 $fileslist[] = $file;
113 core_collator::asort($fileslist, core_collator::SORT_STRING);
114 core_collator::asort($dirslist, core_collator::SORT_STRING);
115 // fill the $list['list']
116 foreach ($dirslist as $file) {
118 $current_path = $path . '/'. $file;
120 $current_path = $file;
122 $list['list'][] = array(
124 'children' => array(),
125 'datecreated' => filectime($this->root_path.$file),
126 'datemodified' => filemtime($this->root_path.$file),
127 'thumbnail' => $OUTPUT->pix_url(file_folder_icon(90))->out(false),
128 'path' => $current_path
131 foreach ($fileslist as $file) {
134 'source' => $path.'/'.$file,
135 'size' => filesize($this->root_path.$file),
136 'datecreated' => filectime($this->root_path.$file),
137 'datemodified' => filemtime($this->root_path.$file),
138 'thumbnail' => $OUTPUT->pix_url(file_extension_icon($file, 90))->out(false),
139 'icon' => $OUTPUT->pix_url(file_extension_icon($file, 24))->out(false)
141 if (file_extension_in_typegroup($file, 'image') && ($imageinfo = @getimagesize($this->root_path . $file))) {
142 // This means it is an image and we can return dimensions and try to generate thumbnail/icon.
143 $token = $node['datemodified'] . $node['size']; // To prevent caching by browser.
144 $node['realthumbnail'] = $this->get_thumbnail_url($path . '/' . $file, 'thumb', $token)->out(false);
145 $node['realicon'] = $this->get_thumbnail_url($path . '/' . $file, 'icon', $token)->out(false);
146 $node['image_width'] = $imageinfo[0];
147 $node['image_height'] = $imageinfo[1];
149 $list['list'][] = $node;
151 $list['list'] = array_filter($list['list'], array($this, 'filter'));
155 public function check_login() {
158 public function print_login() {
161 public function global_search() {
169 public function get_file($file, $title = '') {
171 if ($file{0} == '/') {
172 $file = $this->root_path.substr($file, 1, strlen($file)-1);
174 $file = $this->root_path.$file;
176 // this is a hack to prevent move_to_file deleteing files
177 // in local repository
178 $CFG->repository_no_delete = true;
179 return array('path'=>$file, 'url'=>'');
183 * Return the source information
185 * @param stdClass $filepath
186 * @return string|null
188 public function get_file_source_info($filepath) {
192 public function logout() {
196 public static function get_instance_option_names() {
197 return array('fs_path');
200 public function set_option($options = array()) {
201 $options['fs_path'] = clean_param($options['fs_path'], PARAM_PATH);
202 $ret = parent::set_option($options);
206 public static function instance_config_form($mform) {
208 if (has_capability('moodle/site:config', context_system::instance())) {
209 $path = $CFG->dataroot . '/repository/';
210 if (!is_dir($path)) {
211 mkdir($path, $CFG->directorypermissions, true);
213 if ($handle = opendir($path)) {
214 $fieldname = get_string('path', 'repository_filesystem');
216 while (false !== ($file = readdir($handle))) {
217 if (is_dir($path.$file) && $file != '.' && $file!= '..') {
218 $choices[$file] = $file;
222 if (empty($choices)) {
223 $mform->addElement('static', '', '', get_string('nosubdir', 'repository_filesystem', $path));
224 $mform->addElement('hidden', 'fs_path', '');
225 $mform->setType('fs_path', PARAM_PATH);
227 $mform->addElement('select', 'fs_path', $fieldname, $choices);
228 $mform->addElement('static', null, '', get_string('information','repository_filesystem', $path));
233 $mform->addElement('static', null, '', get_string('nopermissions', 'error', get_string('configplugin', 'repository_filesystem')));
238 public static function create($type, $userid, $context, $params, $readonly=0) {
240 if (has_capability('moodle/site:config', context_system::instance())) {
241 return parent::create($type, $userid, $context, $params, $readonly);
243 require_capability('moodle/site:config', context_system::instance());
247 public static function instance_form_validation($mform, $data, $errors) {
248 if (empty($data['fs_path'])) {
249 $errors['fs_path'] = get_string('invalidadminsettingname', 'error', 'fs_path');
255 * User cannot use the external link to dropbox
259 public function supported_returntypes() {
260 return FILE_INTERNAL | FILE_REFERENCE;
264 * Return reference file life time
269 public function get_reference_file_lifetime($ref) {
270 // Does not cost us much to synchronise within our own filesystem, set to 1 minute
275 * Return human readable reference information
277 * @param string $reference value of DB field files_reference.reference
278 * @param int $filestatus status of the file, 0 - ok, 666 - source missing
281 public function get_reference_details($reference, $filestatus = 0) {
282 $details = $this->get_name().': '.$reference;
284 return get_string('lostsource', 'repository', $details);
291 * Returns information about file in this repository by reference
293 * Returns null if file not found or is not readable
295 * @param stdClass $reference file reference db record
296 * @return stdClass|null contains one of the following:
297 * - 'filesize' if file should not be copied to moodle filepool
298 * - 'filepath' if file should be copied to moodle filepool
300 public function get_file_by_reference($reference) {
301 $ref = $reference->reference;
302 if ($ref{0} == '/') {
303 $filepath = $this->root_path.substr($ref, 1, strlen($ref)-1);
305 $filepath = $this->root_path.$ref;
307 if (file_exists($filepath) && is_readable($filepath)) {
308 if (file_extension_in_typegroup($filepath, 'web_image')) {
309 // return path to image files so it will be copied into moodle filepool
310 // we need the file in filepool to generate an image thumbnail
311 return (object)array('filepath' => $filepath);
313 // return just the file size so file will NOT be copied into moodle filepool
314 return (object)array(
315 'filesize' => filesize($filepath)
324 * Repository method to serve the referenced file
326 * @see send_stored_file
328 * @param stored_file $storedfile the file that contains the reference
329 * @param int $lifetime Number of seconds before the file should expire from caches (default 24 hours)
330 * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
331 * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
332 * @param array $options additional options affecting the file serving
334 public function send_file($storedfile, $lifetime=86400 , $filter=0, $forcedownload=false, array $options = null) {
335 $reference = $storedfile->get_reference();
336 if ($reference{0} == '/') {
337 $file = $this->root_path.substr($reference, 1, strlen($reference)-1);
339 $file = $this->root_path.$reference;
341 if (is_readable($file)) {
342 $filename = $storedfile->get_filename();
343 if ($options && isset($options['filename'])) {
344 $filename = $options['filename'];
346 $dontdie = ($options && isset($options['dontdie']));
347 send_file($file, $filename, $lifetime , $filter, false, $forcedownload, '', $dontdie);
349 send_file_not_found();
354 * Is this repository accessing private data?
358 public function contains_private_data() {
363 * Returns url of thumbnail file.
365 * @param string $filepath current path in repository (dir and filename)
366 * @param string $thumbsize 'thumb' or 'icon'
367 * @param string $token identifier of the file contents - to prevent browser from caching changed file
370 protected function get_thumbnail_url($filepath, $thumbsize, $token) {
371 return moodle_url::make_pluginfile_url($this->context->id, 'repository_filesystem', $thumbsize, $this->id,
372 '/' . trim($filepath, '/') . '/', $token);
376 * Returns the stored thumbnail file, generates it if not present.
378 * @param string $filepath current path in repository (dir and filename)
379 * @param string $thumbsize 'thumb' or 'icon'
380 * @return null|stored_file
382 public function get_thumbnail($filepath, $thumbsize) {
385 $filepath = trim($filepath, '/');
386 $origfile = $this->root_path . $filepath;
387 // As thumbnail filename we use original file content hash.
388 if (!($filecontents = @file_get_contents($origfile))) {
389 // File is not found or is not readable.
392 $filename = sha1($filecontents);
393 unset($filecontents);
395 // Try to get generated thumbnail for this file.
396 $fs = get_file_storage();
397 if (!($file = $fs->get_file(SYSCONTEXTID, 'repository_filesystem', $thumbsize, $this->id, '/' . $filepath . '/', $filename))) {
398 // Thumbnail not found . Generate and store thumbnail.
399 require_once($CFG->libdir . '/gdlib.php');
400 if ($thumbsize === 'thumb') {
405 if (!$data = @generate_image_thumbnail($origfile, $size, $size)) {
406 // Generation failed.
410 'contextid' => SYSCONTEXTID,
411 'component' => 'repository_filesystem',
412 'filearea' => $thumbsize,
413 'itemid' => $this->id,
414 'filepath' => '/' . $filepath . '/',
415 'filename' => $filename,
417 $file = $fs->create_file_from_string($record, $data);
423 * Run in cron for particular repository instance. Removes thumbnails for deleted/modified files.
425 * @param stored_file[] $storedfiles
427 public function remove_obsolete_thumbnails($storedfiles) {
428 // Group found files by filepath ('filepath' in Moodle file storage is dir+name in filesystem repository).
430 foreach ($storedfiles as $file) {
431 if (!isset($files[$file->get_filepath()])) {
432 $files[$file->get_filepath()] = array();
434 $files[$file->get_filepath()][] = $file;
437 // Loop through all files and make sure the original exists and has the same contenthash.
439 foreach ($files as $filepath => $filesinpath) {
440 if ($filecontents = @file_get_contents($this->root_path . trim($filepath, '/'))) {
441 // 'filename' in Moodle file storage is contenthash of the file in filesystem repository.
442 $filename = sha1($filecontents);
443 foreach ($filesinpath as $file) {
444 if ($file->get_filename() !== $filename && $file->get_filename() !== '.') {
445 // Contenthash does not match, this is an old thumbnail.
451 // Thumbnail exist but file not.
452 foreach ($filesinpath as $file) {
453 if ($file->get_filename() !== '.') {
461 mtrace(" instance {$this->id}: deleted $deletedcount thumbnails");
467 * Generates and sends the thumbnail for an image in filesystem.
469 * @param stdClass $course course object
470 * @param stdClass $cm course module object
471 * @param stdClass $context context object
472 * @param string $filearea file area
473 * @param array $args extra arguments
474 * @param bool $forcedownload whether or not force download
475 * @param array $options additional options affecting the file serving
478 function repository_filesystem_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
480 // Allowed filearea is either thumb or icon - size of the thumbnail.
481 if ($filearea !== 'thumb' && $filearea !== 'icon') {
485 // As itemid we pass repository instance id.
486 $itemid = array_shift($args);
487 // Filename is some token that we can ignore (used only to make sure browser does not serve cached copy when file is changed).
489 // As filepath we use full filepath (dir+name) of the file in this instance of filesystem repository.
490 $filepath = implode('/', $args);
492 // Make sure file exists in the repository and is accessible.
493 $repo = repository::get_repository_by_id($itemid, $context);
494 $repo->check_capability();
495 // Find stored or generated thumbnail.
496 if (!($file = $repo->get_thumbnail($filepath, $filearea))) {
497 // Generation failed, redirect to default icon for file extension.
498 redirect($OUTPUT->pix_url(file_extension_icon($file, 90)));
500 send_stored_file($file, 360, 0, $forcedownload, $options);
504 * Cron callback for repository_filesystem. Deletes the thumbnails for deleted or changed files.
506 function repository_filesystem_cron() {
507 $fs = get_file_storage();
508 // Find all generated thumbnails and group them in array by itemid (itemid == repository instance id).
509 $allfiles = array_merge(
510 $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb'),
511 $fs->get_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon')
513 $filesbyitem = array();
514 foreach ($allfiles as $file) {
515 if (!isset($filesbyitem[$file->get_itemid()])) {
516 $filesbyitem[$file->get_itemid()] = array();
518 $filesbyitem[$file->get_itemid()][] = $file;
520 // Find all instances of repository_filesystem.
521 $instances = repository::get_instances(array('type' => 'filesystem'));
522 // Loop through all itemids of generated thumbnails.
523 foreach ($filesbyitem as $itemid => $files) {
524 if (!isset($instances[$itemid]) || !($instances[$itemid] instanceof repository_filesystem)) {
525 // Instance was deleted.
526 $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'thumb', $itemid);
527 $fs->delete_area_files(SYSCONTEXTID, 'repository_filesystem', 'icon', $itemid);
528 mtrace(" instance $itemid does not exist: deleted all thumbnails");
530 // Instance has some generated thumbnails, check that they are not outdated.
531 $instances[$itemid]->remove_obsolete_thumbnails($files);