Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   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.
   8  //
   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.
  13  //
  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/>.
  16  
  17  use Psr\Http\Message\StreamInterface;
  18  
  19  /**
  20   * File system class used for low level access to real files in filedir.
  21   *
  22   * @package   core_files
  23   * @category  files
  24   * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
  25   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  26   */
  27  abstract class file_system {
  28  
  29      /**
  30       * Output the content of the specified stored file.
  31       *
  32       * Note, this is different to get_content() as it uses the built-in php
  33       * readfile function which is more efficient.
  34       *
  35       * @param stored_file $file The file to serve.
  36       * @return void
  37       */
  38      public function readfile(stored_file $file) {
  39          if ($this->is_file_readable_locally_by_storedfile($file, false)) {
  40              $path = $this->get_local_path_from_storedfile($file, false);
  41          } else {
  42              $path = $this->get_remote_path_from_storedfile($file);
  43          }
  44          if (readfile_allow_large($path, $file->get_filesize()) === false) {
  45              throw new file_exception('storedfilecannotreadfile', $file->get_filename());
  46          }
  47      }
  48  
  49      /**
  50       * Get the full path on disk for the specified stored file.
  51       *
  52       * Note: This must return a consistent path for the file's contenthash
  53       * and the path _will_ be in a standard local format.
  54       * Streamable paths will not work.
  55       * A local copy of the file _will_ be fetched if $fetchifnotfound is tree.
  56       *
  57       * The $fetchifnotfound allows you to determine the expected path of the file.
  58       *
  59       * @param stored_file $file The file to serve.
  60       * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
  61       * @return string full path to pool file with file content
  62       */
  63      public function get_local_path_from_storedfile(stored_file $file, $fetchifnotfound = false) {
  64          return $this->get_local_path_from_hash($file->get_contenthash(), $fetchifnotfound);
  65      }
  66  
  67      /**
  68       * Get a remote filepath for the specified stored file.
  69       *
  70       * This is typically either the same as the local filepath, or it is a streamable resource.
  71       *
  72       * See https://secure.php.net/manual/en/wrappers.php for further information on valid wrappers.
  73       *
  74       * @param stored_file $file The file to serve.
  75       * @return string full path to pool file with file content
  76       */
  77      public function get_remote_path_from_storedfile(stored_file $file) {
  78          return $this->get_remote_path_from_hash($file->get_contenthash(), false);
  79      }
  80  
  81      /**
  82       * Get the full path for the specified hash, including the path to the filedir.
  83       *
  84       * Note: This must return a consistent path for the file's contenthash
  85       * and the path _will_ be in a standard local format.
  86       * Streamable paths will not work.
  87       * A local copy of the file _will_ be fetched if $fetchifnotfound is tree.
  88       *
  89       * The $fetchifnotfound allows you to determine the expected path of the file.
  90       *
  91       * @param string $contenthash The content hash
  92       * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
  93       * @return string The full path to the content file
  94       */
  95      abstract protected function get_local_path_from_hash($contenthash, $fetchifnotfound = false);
  96  
  97      /**
  98       * Get the full path for the specified hash, including the path to the filedir.
  99       *
 100       * This is typically either the same as the local filepath, or it is a streamable resource.
 101       *
 102       * See https://secure.php.net/manual/en/wrappers.php for further information on valid wrappers.
 103       *
 104       * @param string $contenthash The content hash
 105       * @return string The full path to the content file
 106       */
 107      abstract protected function get_remote_path_from_hash($contenthash);
 108  
 109      /**
 110       * Determine whether the file is present on the file system somewhere.
 111       * A local copy of the file _will_ be fetched if $fetchifnotfound is tree.
 112       *
 113       * The $fetchifnotfound allows you to determine the expected path of the file.
 114       *
 115       * @param stored_file $file The file to ensure is available.
 116       * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
 117       * @return bool
 118       */
 119      public function is_file_readable_locally_by_storedfile(stored_file $file, $fetchifnotfound = false) {
 120          if (!$file->get_filesize()) {
 121              // Files with empty size are either directories or empty.
 122              // We handle these virtually.
 123              return true;
 124          }
 125  
 126          // Check to see if the file is currently readable.
 127          $path = $this->get_local_path_from_storedfile($file, $fetchifnotfound);
 128          if (is_readable($path)) {
 129              return true;
 130          }
 131  
 132          return false;
 133      }
 134  
 135      /**
 136       * Determine whether the file is present on the local file system somewhere.
 137       *
 138       * @param stored_file $file The file to ensure is available.
 139       * @return bool
 140       */
 141      public function is_file_readable_remotely_by_storedfile(stored_file $file) {
 142          if (!$file->get_filesize()) {
 143              // Files with empty size are either directories or empty.
 144              // We handle these virtually.
 145              return true;
 146          }
 147  
 148          $path = $this->get_remote_path_from_storedfile($file, false);
 149          if (is_readable($path)) {
 150              return true;
 151          }
 152  
 153          return false;
 154      }
 155  
 156      /**
 157       * Determine whether the file is present on the file system somewhere given
 158       * the contenthash.
 159       *
 160       * @param string $contenthash The contenthash of the file to check.
 161       * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
 162       * @return bool
 163       */
 164      public function is_file_readable_locally_by_hash($contenthash, $fetchifnotfound = false) {
 165          if ($contenthash === file_storage::hash_from_string('')) {
 166              // Files with empty size are either directories or empty.
 167              // We handle these virtually.
 168              return true;
 169          }
 170  
 171          // This is called by file_storage::content_exists(), and in turn by the repository system.
 172          $path = $this->get_local_path_from_hash($contenthash, $fetchifnotfound);
 173  
 174          // Note - it is not possible to perform a content recovery safely from a hash alone.
 175          return is_readable($path);
 176      }
 177  
 178      /**
 179       * Determine whether the file is present locally on the file system somewhere given
 180       * the contenthash.
 181       *
 182       * @param string $contenthash The contenthash of the file to check.
 183       * @return bool
 184       */
 185      public function is_file_readable_remotely_by_hash($contenthash) {
 186          if ($contenthash === file_storage::hash_from_string('')) {
 187              // Files with empty size are either directories or empty.
 188              // We handle these virtually.
 189              return true;
 190          }
 191  
 192          $path = $this->get_remote_path_from_hash($contenthash, false);
 193  
 194          // Note - it is not possible to perform a content recovery safely from a hash alone.
 195          return is_readable($path);
 196      }
 197  
 198      /**
 199       * Copy content of file to given pathname.
 200       *
 201       * @param stored_file $file The file to be copied
 202       * @param string $target real path to the new file
 203       * @return bool success
 204       */
 205      abstract public function copy_content_from_storedfile(stored_file $file, $target);
 206  
 207      /**
 208       * Remove the file with the specified contenthash.
 209       *
 210       * Note, if overriding this function, you _must_ check that the file is
 211       * no longer in use - see {check_file_usage}.
 212       *
 213       * DO NOT call directly - reserved for core!!
 214       *
 215       * @param string $contenthash
 216       */
 217      abstract public function remove_file($contenthash);
 218  
 219      /**
 220       * Check whether a file is removable.
 221       *
 222       * This must be called prior to file removal.
 223       *
 224       * @param string $contenthash
 225       * @return bool
 226       */
 227      protected static function is_file_removable($contenthash) {
 228          global $DB;
 229  
 230          if ($contenthash === file_storage::hash_from_string('')) {
 231              // No need to delete files without content.
 232              return false;
 233          }
 234  
 235          // Note: This section is critical - in theory file could be reused at the same time, if this
 236          // happens we can still recover the file from trash.
 237          // Technically this is the responsibility of the file_storage API, but as this method is public, we go belt-and-braces.
 238          if ($DB->record_exists('files', array('contenthash' => $contenthash))) {
 239              // File content is still used.
 240              return false;
 241          }
 242  
 243          return true;
 244      }
 245  
 246      /**
 247       * Get the content of the specified stored file.
 248       *
 249       * Generally you will probably want to use readfile() to serve content,
 250       * and where possible you should see if you can use
 251       * get_content_file_handle and work with the file stream instead.
 252       *
 253       * @param stored_file $file The file to retrieve
 254       * @return string The full file content
 255       */
 256      public function get_content(stored_file $file) {
 257          if (!$file->get_filesize()) {
 258              // Directories are empty. Empty files are not worth fetching.
 259              return '';
 260          }
 261  
 262          $source = $this->get_remote_path_from_storedfile($file);
 263          return file_get_contents($source);
 264      }
 265  
 266      /**
 267       * List contents of archive.
 268       *
 269       * @param stored_file $file The archive to inspect
 270       * @param file_packer $packer file packer instance
 271       * @return array of file infos
 272       */
 273      public function list_files($file, file_packer $packer) {
 274          $archivefile = $this->get_local_path_from_storedfile($file, true);
 275          return $packer->list_files($archivefile);
 276      }
 277  
 278      /**
 279       * Extract file to given file path (real OS filesystem), existing files are overwritten.
 280       *
 281       * @param stored_file $file The archive to inspect
 282       * @param file_packer $packer File packer instance
 283       * @param string $pathname Target directory
 284       * @param file_progress $progress progress indicator callback or null if not required
 285       * @return array|bool List of processed files; false if error
 286       */
 287      public function extract_to_pathname(stored_file $file, file_packer $packer, $pathname, file_progress $progress = null) {
 288          $archivefile = $this->get_local_path_from_storedfile($file, true);
 289          return $packer->extract_to_pathname($archivefile, $pathname, null, $progress);
 290      }
 291  
 292      /**
 293       * Extract file to given file path (real OS filesystem), existing files are overwritten.
 294       *
 295       * @param stored_file $file The archive to inspect
 296       * @param file_packer $packer file packer instance
 297       * @param int $contextid context ID
 298       * @param string $component component
 299       * @param string $filearea file area
 300       * @param int $itemid item ID
 301       * @param string $pathbase path base
 302       * @param int $userid user ID
 303       * @param file_progress $progress Progress indicator callback or null if not required
 304       * @return array|bool list of processed files; false if error
 305       */
 306      public function extract_to_storage(stored_file $file, file_packer $packer, $contextid,
 307              $component, $filearea, $itemid, $pathbase, $userid = null, file_progress $progress = null) {
 308  
 309          // Since we do not know which extractor we have, and whether it supports remote paths, use a local path here.
 310          $archivefile = $this->get_local_path_from_storedfile($file, true);
 311          return $packer->extract_to_storage($archivefile, $contextid,
 312                  $component, $filearea, $itemid, $pathbase, $userid, $progress);
 313      }
 314  
 315      /**
 316       * Add file/directory into archive.
 317       *
 318       * @param stored_file $file The file to archive
 319       * @param file_archive $filearch file archive instance
 320       * @param string $archivepath pathname in archive
 321       * @return bool success
 322       */
 323      public function add_storedfile_to_archive(stored_file $file, file_archive $filearch, $archivepath) {
 324          if ($file->is_directory()) {
 325              return $filearch->add_directory($archivepath);
 326          } else {
 327              // Since we do not know which extractor we have, and whether it supports remote paths, use a local path here.
 328              return $filearch->add_file_from_pathname($archivepath, $this->get_local_path_from_storedfile($file, true));
 329          }
 330      }
 331  
 332      /**
 333       * Adds this file path to a curl request (POST only).
 334       *
 335       * @param stored_file $file The file to add to the curl request
 336       * @param curl $curlrequest The curl request object
 337       * @param string $key What key to use in the POST request
 338       * @return void
 339       * This needs the fullpath for the storedfile :/
 340       * Can this be achieved in some other fashion?
 341       */
 342      public function add_to_curl_request(stored_file $file, &$curlrequest, $key) {
 343          // Note: curl_file_create does not work with remote paths.
 344          $path = $this->get_local_path_from_storedfile($file, true);
 345          $curlrequest->_tmp_file_post_params[$key] = curl_file_create($path, null, $file->get_filename());
 346      }
 347  
 348      /**
 349       * Returns information about image.
 350       * Information is determined from the file content
 351       *
 352       * @param stored_file $file The file to inspect
 353       * @return mixed array with width, height and mimetype; false if not an image
 354       */
 355      public function get_imageinfo(stored_file $file) {
 356          if (!$this->is_image_from_storedfile($file)) {
 357              return false;
 358          }
 359  
 360          $hash = $file->get_contenthash();
 361          $cache = cache::make('core', 'file_imageinfo');
 362          $info = $cache->get($hash);
 363          if ($info !== false) {
 364              return $info;
 365          }
 366  
 367          // Whilst get_imageinfo_from_path can use remote paths, it must download the entire file first.
 368          // It is more efficient to use a local file when possible.
 369          $info = $this->get_imageinfo_from_path($this->get_local_path_from_storedfile($file, true));
 370          $cache->set($hash, $info);
 371          return $info;
 372      }
 373  
 374      /**
 375       * Attempt to determine whether the specified file is likely to be an
 376       * image.
 377       * Since this relies upon the mimetype stored in the files table, there
 378       * may be times when this information is not 100% accurate.
 379       *
 380       * @param stored_file $file The file to check
 381       * @return bool
 382       */
 383      public function is_image_from_storedfile(stored_file $file) {
 384          if (!$file->get_filesize()) {
 385              // An empty file cannot be an image.
 386              return false;
 387          }
 388  
 389          $mimetype = $file->get_mimetype();
 390          if (!preg_match('|^image/|', $mimetype)) {
 391              // The mimetype does not include image.
 392              return false;
 393          }
 394  
 395          // If it looks like an image, and it smells like an image, perhaps it's an image!
 396          return true;
 397      }
 398  
 399      /**
 400       * Returns image information relating to the specified path or URL.
 401       *
 402       * @param string $path The full path of the image file.
 403       * @return array|bool array that containing width, height, and mimetype or false if cannot get the image info.
 404       */
 405      protected function get_imageinfo_from_path($path) {
 406          $imagemimetype = file_storage::mimetype_from_file($path);
 407          $issvgimage = file_is_svg_image_from_mimetype($imagemimetype);
 408  
 409          if (!$issvgimage) {
 410              $imageinfo = getimagesize($path);
 411              if (!is_array($imageinfo)) {
 412                  return false; // Nothing to process, the file was not recognised as image by GD.
 413              }
 414              $image = [
 415                      'width' => $imageinfo[0],
 416                      'height' => $imageinfo[1],
 417                      'mimetype' => image_type_to_mime_type($imageinfo[2]),
 418              ];
 419          } else {
 420              // Since SVG file is actually an XML file, GD cannot handle.
 421              $svgcontent = @simplexml_load_file($path);
 422              if (!$svgcontent) {
 423                  // Cannot parse the file.
 424                  return false;
 425              }
 426              $svgattrs = $svgcontent->attributes();
 427  
 428              if (!empty($svgattrs->viewBox)) {
 429                  // We have viewBox.
 430                  $viewboxval = explode(' ', $svgattrs->viewBox);
 431                  $width = intval($viewboxval[2]);
 432                  $height = intval($viewboxval[3]);
 433              } else {
 434                  // Get the width.
 435                  if (!empty($svgattrs->width) && intval($svgattrs->width) > 0) {
 436                      $width = intval($svgattrs->width);
 437                  } else {
 438                      // Default width.
 439                      $width = 800;
 440                  }
 441                  // Get the height.
 442                  if (!empty($svgattrs->height) && intval($svgattrs->height) > 0) {
 443                      $height = intval($svgattrs->height);
 444                  } else {
 445                      // Default width.
 446                      $height = 600;
 447                  }
 448              }
 449  
 450              $image = [
 451                      'width' => $width,
 452                      'height' => $height,
 453                      'mimetype' => $imagemimetype,
 454              ];
 455          }
 456  
 457          if (empty($image['width']) or empty($image['height']) or empty($image['mimetype'])) {
 458              // GD can not parse it, sorry.
 459              return false;
 460          }
 461          return $image;
 462      }
 463  
 464      /**
 465       * Serve file content using X-Sendfile header.
 466       * Please make sure that all headers are already sent and the all
 467       * access control checks passed.
 468       *
 469       * This alternate method to xsendfile() allows an alternate file system
 470       * to use the full file metadata and avoid extra lookups.
 471       *
 472       * @param stored_file $file The file to send
 473       * @return bool success
 474       */
 475      public function xsendfile_file(stored_file $file): bool {
 476          return $this->xsendfile($file->get_contenthash());
 477      }
 478  
 479      /**
 480       * Serve file content using X-Sendfile header.
 481       * Please make sure that all headers are already sent and the all
 482       * access control checks passed.
 483       *
 484       * @param string $contenthash The content hash of the file to be served
 485       * @return bool success
 486       */
 487      public function xsendfile($contenthash) {
 488          global $CFG;
 489          require_once($CFG->libdir . "/xsendfilelib.php");
 490  
 491          return xsendfile($this->get_remote_path_from_hash($contenthash));
 492      }
 493  
 494      /**
 495       * Returns true if filesystem is configured to support xsendfile.
 496       *
 497       * @return bool
 498       */
 499      public function supports_xsendfile() {
 500          global $CFG;
 501          return !empty($CFG->xsendfile);
 502      }
 503  
 504      /**
 505       * Validate that the content hash matches the content hash of the file on disk.
 506       *
 507       * @param string $contenthash The current content hash to validate
 508       * @param string $pathname The path to the file on disk
 509       * @return array The content hash (it might change) and file size
 510       */
 511      protected function validate_hash_and_file_size($contenthash, $pathname) {
 512          global $CFG;
 513  
 514          if (!is_readable($pathname)) {
 515              throw new file_exception('storedfilecannotread', '', $pathname);
 516          }
 517  
 518          $filesize = filesize($pathname);
 519          if ($filesize === false) {
 520              throw new file_exception('storedfilecannotread', '', $pathname);
 521          }
 522  
 523          if (is_null($contenthash)) {
 524              $contenthash = file_storage::hash_from_path($pathname);
 525          } else if ($CFG->debugdeveloper) {
 526              $filehash = file_storage::hash_from_path($pathname);
 527              if ($filehash === false) {
 528                  throw new file_exception('storedfilecannotread', '', $pathname);
 529              }
 530              if ($filehash !== $contenthash) {
 531                  // Hopefully this never happens, if yes we need to fix calling code.
 532                  debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
 533                  $contenthash = $filehash;
 534              }
 535          }
 536          if ($contenthash === false) {
 537              throw new file_exception('storedfilecannotread', '', $pathname);
 538          }
 539  
 540          if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) {
 541              // Did the file change or is file_storage::hash_from_path() borked for this file?
 542              clearstatcache();
 543              $contenthash = file_storage::hash_from_path($pathname);
 544              $filesize    = filesize($pathname);
 545  
 546              if ($contenthash === false or $filesize === false) {
 547                  throw new file_exception('storedfilecannotread', '', $pathname);
 548              }
 549              if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) {
 550                  // This is very weird...
 551                  throw new file_exception('storedfilecannotread', '', $pathname);
 552              }
 553          }
 554  
 555          return [$contenthash, $filesize];
 556      }
 557  
 558      /**
 559       * Add the supplied file to the file system.
 560       *
 561       * Note: If overriding this function, it is advisable to store the file
 562       * in the path returned by get_local_path_from_hash as there may be
 563       * subsequent uses of the file in the same request.
 564       *
 565       * @param string $pathname Path to file currently on disk
 566       * @param string $contenthash SHA1 hash of content if known (performance only)
 567       * @return array (contenthash, filesize, newfile)
 568       */
 569      abstract public function add_file_from_path($pathname, $contenthash = null);
 570  
 571      /**
 572       * Add a file with the supplied content to the file system.
 573       *
 574       * Note: If overriding this function, it is advisable to store the file
 575       * in the path returned by get_local_path_from_hash as there may be
 576       * subsequent uses of the file in the same request.
 577       *
 578       * @param string $content file content - binary string
 579       * @return array (contenthash, filesize, newfile)
 580       */
 581      abstract public function add_file_from_string($content);
 582  
 583      /**
 584       * Returns file handle - read only mode, no writing allowed into pool files!
 585       *
 586       * When you want to modify a file, create a new file and delete the old one.
 587       *
 588       * @param stored_file $file The file to retrieve a handle for
 589       * @param int $type Type of file handle (FILE_HANDLE_xx constant)
 590       * @return resource file handle
 591       */
 592      public function get_content_file_handle(stored_file $file, $type = stored_file::FILE_HANDLE_FOPEN) {
 593          if ($type === stored_file::FILE_HANDLE_GZOPEN) {
 594              // Local file required for gzopen.
 595              $path = $this->get_local_path_from_storedfile($file, true);
 596          } else {
 597              $path = $this->get_remote_path_from_storedfile($file);
 598          }
 599  
 600          return self::get_file_handle_for_path($path, $type);
 601      }
 602  
 603      /**
 604       * Return a file handle for the specified path.
 605       *
 606       * This abstraction should be used when overriding get_content_file_handle in a new file system.
 607       *
 608       * @param string $path The path to the file. This shoudl be any type of path that fopen and gzopen accept.
 609       * @param int $type Type of file handle (FILE_HANDLE_xx constant)
 610       * @return resource
 611       * @throws coding_exception When an unexpected type of file handle is requested
 612       */
 613      protected static function get_file_handle_for_path($path, $type = stored_file::FILE_HANDLE_FOPEN) {
 614          switch ($type) {
 615              case stored_file::FILE_HANDLE_FOPEN:
 616                  // Binary reading.
 617                  return fopen($path, 'rb');
 618              case stored_file::FILE_HANDLE_GZOPEN:
 619                  // Binary reading of file in gz format.
 620                  return gzopen($path, 'rb');
 621              default:
 622                  throw new coding_exception('Unexpected file handle type');
 623          }
 624      }
 625  
 626      /**
 627       * Get a PSR7 Stream for the specified file which implements the PSR Message StreamInterface.
 628       *
 629       * @param stored_file $file
 630       * @return StreamInterface
 631       */
 632      public function get_psr_stream(stored_file $file): StreamInterface {
 633          return \GuzzleHttp\Psr7\Utils::streamFor($this->get_content_file_handle($file));
 634      }
 635  
 636      /**
 637       * Retrieve the mime information for the specified stored file.
 638       *
 639       * @param string $contenthash
 640       * @param string $filename
 641       * @return string The MIME type.
 642       */
 643      public function mimetype_from_hash($contenthash, $filename) {
 644          $pathname = $this->get_local_path_from_hash($contenthash);
 645          $mimetype = file_storage::mimetype($pathname, $filename);
 646  
 647          if ($mimetype === 'document/unknown' && !$this->is_file_readable_locally_by_hash($contenthash)) {
 648              // The type is unknown, but the full checks weren't completed because the file isn't locally available.
 649              // Ensure we have a local copy and try again.
 650              $pathname = $this->get_local_path_from_hash($contenthash, true);
 651              $mimetype = file_storage::mimetype_from_file($pathname);
 652          }
 653  
 654          return $mimetype;
 655      }
 656  
 657      /**
 658       * Retrieve the mime information for the specified stored file.
 659       *
 660       * @param stored_file $file The stored file to retrieve mime information for
 661       * @return string The MIME type.
 662       */
 663      public function mimetype_from_storedfile($file) {
 664          if (!$file->get_filesize()) {
 665              // Files with an empty filesize are treated as directories and have no mimetype.
 666              return null;
 667          }
 668          return $this->mimetype_from_hash($file->get_contenthash(), $file->get_filename());
 669      }
 670  
 671      /**
 672       * Run any periodic tasks which must be performed.
 673       */
 674      public function cron() {
 675      }
 676  }