]> git.mjollnir.org Git - moodle.git/commitdiff
MDL-16231 reimplemented deleting of files + cron cleanup
authorskodak <skodak>
Wed, 3 Jun 2009 08:10:21 +0000 (08:10 +0000)
committerskodak <skodak>
Wed, 3 Jun 2009 08:10:21 +0000 (08:10 +0000)
admin/cron.php
lib/db/install.xml
lib/db/upgrade.php
lib/file/file_storage.php
lib/file/stored_file.php
lib/moodlelib.php
lib/setup.php
lib/simpletest/testfilelib.php
version.php

index fcc2c003f1470bc5460acb27288181a60d4ab879..2af42acc751adfefd13d03853230eeaf773e674e 100644 (file)
             }
         }
     }
-    
+
+    // cleanup file trash
+    $fs = get_file_storage();
+    $fs->cron();
+
     // run any customized cronjobs, if any
     // looking for functions in lib/local/cron.php
     if (file_exists($CFG->dirroot.'/local/cron.php')) {
index 0c12ced41c0a0bb38a26ce72aa56dd4d7de728b4..74032e80057dd6e2a9106a545b73ccd97b0777f6 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20090501" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20090602" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="primary key of the table, please edit me"/>
       </KEYS>
     </TABLE>
-    <TABLE NAME="files" COMMENT="description of files, content stored in sha1 file pool" PREVIOUS="message_working" NEXT="files_cleanup">
+    <TABLE NAME="files" COMMENT="description of files, content stored in sha1 file pool" PREVIOUS="message_working" NEXT="repository">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="contenthash"/>
         <FIELD NAME="contenthash" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false" COMMENT="sha1 hash of file content" PREVIOUS="id" NEXT="pathnamehash"/>
         <INDEX NAME="pathnamehash" UNIQUE="true" FIELDS="pathnamehash" PREVIOUS="contenthash"/>
       </INDEXES>
     </TABLE>
-    <TABLE NAME="files_cleanup" COMMENT="File pool cleanup candidates" PREVIOUS="files" NEXT="repository">
-      <FIELDS>
-        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="contenthash"/>
-        <FIELD NAME="contenthash" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false" PREVIOUS="id"/>
-      </FIELDS>
-      <KEYS>
-        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
-      </KEYS>
-      <INDEXES>
-        <INDEX NAME="contenthash" UNIQUE="true" FIELDS="contenthash"/>
-      </INDEXES>
-    </TABLE>
-    <TABLE NAME="repository" COMMENT="This table contains one entry for every configured external repository instance." PREVIOUS="files_cleanup" NEXT="repository_instances">
+    <TABLE NAME="repository" COMMENT="This table contains one entry for every configured external repository instance." PREVIOUS="files" NEXT="repository_instances">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="type"/>
         <FIELD NAME="type" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" PREVIOUS="id" NEXT="visible"/>
index 775895cb4c866b359e4aec74181d77d7e2ee2868..3987a1b51907cebb6152c058d22be38d04bc6581 100644 (file)
@@ -518,27 +518,6 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint($result, 2008073111);
     }
 
-    if ($result && $oldversion < 2008073112) {
-    /// Define table files_cleanup to be created
-        $table = new xmldb_table('files_cleanup');
-
-    /// Adding fields to table files_cleanup
-        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
-        $table->add_field('contenthash', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null);
-
-    /// Adding keys to table files_cleanup
-        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
-
-    /// Adding indexes to table files_cleanup
-        $table->add_index('contenthash', XMLDB_INDEX_UNIQUE, array('contenthash'));
-
-    /// Conditionally launch create table for files_cleanup
-        $dbman->create_table($table);
-
-    /// Main savepoint reached
-        upgrade_main_savepoint($result, 2008073112);
-    }
-
     if ($result && $oldversion < 2008073113) {
     /// move all course, backup and other files to new filepool based storage
         upgrade_migrate_files_courses();
@@ -2155,6 +2134,19 @@ WHERE gradeitemid IS NOT NULL AND grademax IS NOT NULL");
         upgrade_main_savepoint($result, 2009051700);
     }
 
+    if ($result && $oldversion < 2009060200) {
+    /// Define table files_cleanup to be dropped - not needed
+        $table = new xmldb_table('files_cleanup');
+
+    /// Conditionally launch drop table for files_cleanup
+        if ($dbman->table_exists($table)) {
+            $dbman->drop_table($table);
+        }
+
+    /// Main savepoint reached
+        upgrade_main_savepoint($result, 2009060200);
+    }
+
     return $result;
 }
 
index aa03ebd076e024f5700ac9a5253d1ca9513ae401..4ca660b9135b291bced87660bb17d3ad3487c4df 100644 (file)
@@ -35,29 +35,46 @@ require_once("$CFG->libdir/file/stored_file.php");
  * to use file_browser class instead.
  */
 class file_storage {
+    /** Directory with file contents */
     private $filedir;
-
+    /** Contents of deleted files not needed any more */
+    private $trashdir;
+    /** Permissions for new directories */
+    private $dirpermissions;
+    /** Permissions for new files */
+    private $filepermissions;
     /**
      * Contructor
      * @param string $filedir full path to pool directory
      */
-    public function __construct($filedir) {
-        $this->filedir = $filedir;
+    public function __construct($filedir, $trashdir, $dirpermissions, $filepermissions) {
+        $this->filedir         = $filedir;
+        $this->trashdir        = $trashdir;
+        $this->dirpermissions  = $dirpermissions;
+        $this->filepermissions = $filepermissions;
 
         // make sure the file pool directory exists
         if (!is_dir($this->filedir)) {
-            if (!check_dir_exists($this->filedir, true, true)) {
+            if (!mkdir($this->filedir, $this->dirpermissions, true)) {
                 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
             }
             // place warning file in file pool root
-            file_put_contents($this->filedir.'/warning.txt',
-                              'This directory contains the content of uploaded files and is controlled by Moodle code. Do not manually move, change or rename any of the files and subdirectories here.');
+            if (!file_exists($this->filedir.'/warning.txt')) {
+                file_put_contents($this->filedir.'/warning.txt',
+                                  'This directory contains the content of uploaded files and is controlled by Moodle code. Do not manually move, change or rename any of the files and subdirectories here.');
+            }
+        }
+        // make sure the file pool directory exists
+        if (!is_dir($this->trashdir)) {
+            if (!mkdir($this->trashdir, $this->dirpermissions, true)) {
+                throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
+            }
         }
     }
 
     /**
      * Returns location of filedir (file pool)
-     * Do not use, this method is intended for stored_file instances.
+     * Do not use, this method is intended for stored_file instances only.
      * @return string pathname
      */
     public function get_filedir() {
@@ -805,8 +822,10 @@ class file_storage {
             $newfile = false;
 
         } else {
-            if (!check_dir_exists($hashpath, true, true)) {
-                throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
+            if (!is_dir($hashpath)) {
+                if (!mkdir($hashpath, $this->dirpermissions, true)) {
+                    throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
+                }
             }
             $newfile = true;
 
@@ -818,6 +837,7 @@ class file_storage {
                 @unlink($hashfile);
                 throw new file_pool_content_exception($contenthash);
             }
+            chmod($hashfile, $this->filepermissions); // fix permissions if needed
         }
 
 
@@ -844,8 +864,10 @@ class file_storage {
             $newfile = false;
 
         } else {
-            if (!check_dir_exists($hashpath, true, true)) {
-                throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
+            if (!is_dir($hashpath)) {
+                if (!mkdir($hashpath, $this->dirpermissions, true)) {
+                    throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
+                }
             }
             $newfile = true;
 
@@ -855,6 +877,7 @@ class file_storage {
                 @unlink($hashfile);
                 throw new file_pool_content_exception($contenthash);
             }
+            chmod($hashfile, $this->filepermissions); // fix permissions if needed
         }
 
         return array($contenthash, $filesize, $newfile);
@@ -875,38 +898,102 @@ class file_storage {
         return "$this->filedir/$l1/$l2/$l3";
     }
 
+    /**
+     * Return path to file with given hash
+     *
+     * NOTE: must not be public, files in pool must not be modified
+     *
+     * @param string $contenthash
+     * @return string expected file location
+     */
+    protected function trash_path_from_hash($contenthash) {
+        $l1 = $contenthash[0].$contenthash[1];
+        $l2 = $contenthash[2].$contenthash[3];
+        $l3 = $contenthash[4].$contenthash[5];
+        return "$this->trashdir/$l1/$l2/$l3";
+    }
+
+    /**
+     * Tries to recover missing content of file from trash
+     * @param object $file_record
+     * @return bool success
+     */
+    public function try_content_recovery($file) {
+        $contenthash = $file->get_contenthash();
+        $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
+        if (!is_readable($trashfile)) {
+            if (!is_readable($this->trashdir.'/'.$contenthash)) {
+                return false;
+            }
+            // nice, at least alternative trash file in trash root exists
+            $trashfile = $this->trashdir.'/'.$contenthash;
+        }
+        if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
+            //weird, better fail early
+            return false;
+        }
+        $contentdir  = $this->path_from_hash($contenthash);
+        $contentfile = $contentdir.'/'.$contenthash;
+        if (file_exists($contentfile)) {
+            //strange, no need to recover anything
+            return true;
+        }
+        if (!is_dir($contentdir)) {
+            if (!mkdir($contentdir, $this->dirpermissions, true)) {
+                return false;
+            }
+        }
+        return rename($trashfile, $contentfile);
+    }
+
     /**
      * Marks pool file as candidate for deleting
+     * DO NOT call directly - reserved for core!
      * @param string $contenthash
+     * @return void
      */
-    public function mark_delete_candidate($contenthash) {
+    public function deleted_file_cleanup($contenthash) {
         global $DB;
 
-        if ($DB->record_exists('files_cleanup', array('contenthash'=>$contenthash))) {
+        //Note: this section is critical - in theory file could be reused at the same
+        //      time, if this happens we can still recover the file from trash
+        if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
+            // file content is still used
+            return;
+        }
+        //move content file to trash
+        $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
+        if (!file_exists($contentfile)) {
+            //weird, but no problem
             return;
         }
-        $rec = new object();
-        $rec->contenthash = $contenthash;
-        $DB->insert_record('files_cleanup', $rec);
+        $trashpath = $this->trash_path_from_hash($contenthash);
+        $trashfile = $trashpath.'/'.$contenthash;
+        if (file_exists($trashfile)) {
+            // we already have this content in trash, no need to move it there
+            unlink($contentfile);
+            return;
+        }
+        if (!is_dir($trashpath)) {
+            mkdir($trashpath, $this->dirpermissions, true);
+        }
+        rename($contentfile, $trashfile);
+        chmod($trashfile, $this->filepermissions); // fix permissions if needed
     }
 
     /**
      * Cron cleanup job.
      */
     public function cron() {
-        global $DB;
-
-        //TODO: there is a small chance that reused files might be deleted
-        //      if this function takes too long we should add some table locking here
-
-        $sql = "SELECT 1 AS id, fc.contenthash
-                  FROM {files_cleanup} fc
-                  LEFT JOIN {files} f ON f.contenthash = fc.contenthash
-                 WHERE f.id IS NULL";
-        while ($hash = $DB->get_record_sql($sql, null, true)) {
-            $file = $this->path_from_hash($hash->contenthash).'/'.$hash->contenthash;
-            @unlink($file);
-            $DB->delete_records('files_cleanup', array('contenthash'=>$hash->contenthash));
+        global $CFG;
+        // remove trash pool files once a day
+        // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
+        if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
+            require_once($CFG->libdir.'/filelib.php');
+            mtrace('Deleting trash files... ', '');
+            fulldelete($this->trashdir);
+            set_config('fileslastcleanup', time());
+            mtrace('done.');
         }
     }
 }
index 542f971c66eb9bad678057d5c1faeb5892999023..6f2437e08db675ff12388bef8b5d153fb258a5b7 100644 (file)
@@ -54,12 +54,14 @@ class stored_file {
 
     /**
      * Delete file
-     * @return success
+     * @return bool success
      */
     public function delete() {
         global $DB;
-        $this->fs->mark_delete_candidate($this->file_record->contenthash);
-        return $DB->delete_records('files', array('id'=>$this->file_record->id));
+        $DB->delete_records('files', array('id'=>$this->file_record->id));
+        // moves pool file to trash if content not needed any more
+        $this->fs->deleted_file_cleanup($this->file_record->contenthash);
+        return true;
     }
 
     /**
@@ -93,7 +95,9 @@ class stored_file {
     public function get_content_file_handle() {
         $path = $this->get_content_file_location();
         if (!is_readable($path)) {
-            throw new file_exception('storedfilecannotread');
+            if (!$this->fs->try_content_recovery($this) or !is_readable($path)) {
+                throw new file_exception('storedfilecannotread');
+            }
         }
         return fopen($path, 'rb'); //binary reading only!!
     }
@@ -105,7 +109,9 @@ class stored_file {
     public function readfile() {
         $path = $this->get_content_file_location();
         if (!is_readable($path)) {
-            throw new file_exception('storedfilecannotread');
+            if (!$this->fs->try_content_recovery($this) or !is_readable($path)) {
+                throw new file_exception('storedfilecannotread');
+            }
         }
         readfile($path);
     }
@@ -117,7 +123,9 @@ class stored_file {
     public function get_content() {
         $path = $this->get_content_file_location();
         if (!is_readable($path)) {
-            throw new file_exception('storedfilecannotread');
+            if (!$this->fs->try_content_recovery($this) or !is_readable($path)) {
+                throw new file_exception('storedfilecannotread');
+            }
         }
         return file_get_contents($this->get_content_file_location());
     }
@@ -130,7 +138,9 @@ class stored_file {
     public function copy_content_to($pathname) {
         $path = $this->get_content_file_location();
         if (!is_readable($path)) {
-            throw new file_exception('storedfilecannotread');
+            if (!$this->fs->try_content_recovery($this) or !is_readable($path)) {
+                throw new file_exception('storedfilecannotread');
+            }
         }
         return copy($path, $pathname);
     }
index b1e8c5f1b0a8f670c42fcb4aa7e3c760cfa9cd66..c48e438f444f41baafe10a3dde29d9409c8c32e4 100644 (file)
@@ -4963,7 +4963,13 @@ function get_file_storage() {
         $filedir = $CFG->dataroot.'/filedir';
     }
 
-    $fs = new file_storage($filedir);
+    if (isset($CFG->trashdir)) {
+        $trashdirdir = $CFG->trashdir;
+    } else {
+        $trashdirdir = $CFG->dataroot.'/trashdir';
+    }
+
+    $fs = new file_storage($filedir, $trashdirdir, $CFG->directorypermissions, $CFG->filepermissions);
 
     return $fs;
 }
index e39e6744ab9b3cbff26e17cd6f7f5355f1a32ac4..c0c8b6f28516eceb11d248b36cb38d8804cc1d54 100644 (file)
@@ -421,6 +421,11 @@ global $SCRIPT;
     if (empty($CFG->directorypermissions)) {
         $CFG->directorypermissions = 0777;      // Must be octal (that's why it's here)
     }
+    if (empty($CFG->filepermissions)) {
+        $CFG->filepermissions = ($CFG->directorypermissions & 0666); // strip execute flags
+    }
+/// better also set default umask because recursive mkdir() does not apply permissions recursively otherwise
+    umask(0000);
 
 /// Calculate and set $CFG->ostype to be used everywhere. Possible values are:
 /// - WINDOWS: for any Windows flavour.
index b0416ffd3e27f544e4d936a0d7b0ad917884ae40..e0d1419a9cab976667444c1d7dc52c51fbd5c025 100644 (file)
@@ -51,7 +51,7 @@ class filelib_test extends UnitTestCaseUsingDatabase {
         parent::setUp();
         $tables = array('block_instance', 'cache_flags', 'capabilities', 'context', 'context_temp',
                         'course', 'course_modules', 'course_categories', 'course_sections','files',
-                        'files_cleanup', 'grade_items', 'grade_categories', 'groups', 'groups_members',
+                        'grade_items', 'grade_categories', 'groups', 'groups_members',
                         'modules', 'role', 'role_names', 'role_context_levels', 'role_assignments',
                         'role_capabilities', 'user');
         $this->create_test_tables($tables, 'lib');
index 07a30b1a313e89273aa3f0c089ab22b510d9e063..f0b9eda66d8fb2883f1562e4bdec5d5821773dfb 100644 (file)
@@ -6,7 +6,7 @@
 // This is compared against the values stored in the database to determine
 // whether upgrades should be performed (see lib/db/*.php)
 
-    $version = 2009051700;  // YYYYMMDD   = date of the last version bump
+    $version = 2009060200;  // YYYYMMDD   = date of the last version bump
                             //         XX = daily increments
 
     $release = '2.0 dev (Build: 20090603)';  // Human-friendly version name