====== Using The Moodle File API in Modules ====== Moodle uses a virtual file system on top of the 'physical' directory structure located in moodledata. Each file is stored only once in a physical location, which is referenced in the database. The file is identified using a unique hash (based, of course, on the content of the file). So, whenever that file is copied, or renamed, the actual file remains the same. Only the references in the database are changed and stored in combination with the hash. This also means that uploading the same file twice does not take up any more space on the hard drive. Even if you rename the file before the second upload, Moodle will still maintain only one copy of the file. ===== Why Use Moodle's File API At All? ===== Why would you want to use the Moodle file API in your modules? After all, simply saving an uploaded file to the server is not that hard. Well, apart from the reason that Moodle always only keeps one copy (saving hard disk space), it also makes it much easier to program backup and restore functions for your module. To use Moodle's file API in your own modules, you need to follow a few conventions. Before we get to that, let's discuss the database table mdl_files first. ===== Where Are My Files Actually Stored? ===== As stated before, the actual files are still stored on the server's hard drive, usually inside the ''moodledata/filedir'' directory. A reference to the file is kept in ''mdl_files.contenthash''. The first two pairs of characters of the content hash are used to create two subdirectories. For instance, if your file's content hash is ''f3d58f82c21e90df4e00cc1c2c081c148c6ac686'', the file will be stored in: ''moodledata/filedir/f3/d5'' You can see this for yourself by uploading a file, for instance through the assign module, and then checking the content of the most recently added mdl_files record (provided you never uploaded the file before). You should be able to find the content hash and match it with the directory structure and finally the name of the physical file. The moodle.org website contains some documentation on how to store an arbitrary data blob or string: [[http://docs.moodle.org/dev/File_API#Create_file|http://docs.moodle.org/dev/File_API#Create_file]]. ==== Meaning of The Columns in The 'mdl_files' Table ==== Here's a brief primer on the anatomy of the mdl_files table: * contenthash: unique hash of the file. Moodles stores incoming files under this name. * pathnamehash: hash of the 'virtual' filepath and filename * contextid: id of an mdl_context record * component: name of the component (e.g. a plugin) which created or uploaded the file * filearea: arbitrary string to further subdivide virtual files into groups * itemid: id of table (optional) containing information pertaining to the file * filepath: virtual path * filename: original filename (remember: the file itself is renamed to the contenthash by Moodle after uploading) * userid: uploader or creator * filesize: 0 seems to be used to indicate a "directory". For some reason, Moodle creates a record with filesize 0 for each new file, in addition to the 'valid' record. ===== Conventions For Handling Files in Your Module ===== Modules are supposed to stick to a few rules concerning files: * Save files using ''mod_[your-mod-name]'' as the value for the component column * Create a callback function ''[your-mod-name]_pluginfile'' in your mod's lib.php file We will explain the two rules in the following sections. ===== Saving Files ===== To save an uploaded file, you should first check whether it already exists as the exact same virtual file that you intend to create. Let's look at some code to see what that means. First of all, get the uploaded file: $uploaded_file_path = $_FILES['user_file']['tmp_name']; // temp path to the actual file $filename = $_FILES['user_file']['name']; // the original (human readable) filename Then, create an instance of Moodle's file_storage class: $file_storage = get_file_storage(); We are going to use the $file_storage object to: - Check if the file already exists, and if not: - Actually create the file inside Moodle's virtual file system So, here we go: $context = context_module::instance($id); $fileinfo = array( 'contextid' => $context->id, 'component' => 'mod_rainmaker', // mod_[your-mod-name] 'filearea' => 'rainmaker_docs', // arbitrary string 'itemid' => $id, // use a unique id in the context of the filearea and you should be safe 'filepath' => '/', // virtual path 'filename' => $filename); // virtual filename $file_storage = get_file_storage(); if ($file_storage->file_exists($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'], $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename'])) return false; // (this code is actually in a function) $file = $file_storage->create_file_from_pathname($fileinfo, $uploaded_file_path); As you can see, the exact same array of file information that is used to store the new file, is also used to check for the presence of an existing file. Remember, this is not a check for the physical file, but for the 'virtual' file with all the same properties. If you omit this check, and you upload the exact same file twice, Moodle will give an error, because it would result in a duplicate ''mdl_file.pathnamehash'' value. ===== Serving Files ===== To serve files from inside you module, you need to: * Create a link to a specific file * Add a '[your-mod-name]_pluginfile' function to your module's lib.php file ==== Create A File Link ==== As stated in See [[http://docs.moodle.org/dev/File_API#Serving_files_to_users|http://docs.moodle.org/dev/File_API#Serving_files_to_users]], call ''moodle_url::make_pluginfile_url''. The function ''moodle_url::make_pluginfile_url'' takes a lot of specific arguments, so we have split this task into two parts: - get file information - using that information, create the url Here's the code for retrieving the file information: function get_rainmaker_file($course_module_id) { $context = context_module::instance($course_module_id); $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'mod_rainmaker', 'rainmaker_docs', $course_module_id, $sort = false, $includedirs = false); if (!count($files)) return false; return array_shift($files); } // function get_rainmaker_file Please be sure to change ''mod_rainmaker'' to your own module's name. Using the file information, we can now construct the url. function get_rainmaker_doc_url() { global $id; // the course_module id if (! $file = get_rainmaker_file($id)) return false; return moodle_url::make_pluginfile_url( $file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(), $file->get_filepath(), $file->get_filename(), $forcedownload = true); } // function get_rainmaker_doc_url Calling the ''get_rainmaker_doc_url'' function will return a url which you can display in your module's "views". ==== Serving The File ==== If users click the newly constructed url, they'll be redirected to something like: ''/pluginfile.php/100/mod_rainmaker/rainmaker_docs/64/test.docx?forcedownload=1'' Unfortunately, that's not enough. The ''pluginfile.php'' script is going to look for a callback function, which should be located in your module's lib.php. Here's an example: function rainmaker_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload) { global $CFG; require_login($course, true, $cm); if (! $file = get_rainmaker_file($cm->id)) return false; send_stored_file($file); } // function rainmaker_pluginfile Or a more detailed example: function local_section_icons_images_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { // Check the contextlevel is as expected - if your plugin is a block, this becomes CONTEXT_BLOCK, etc. if ($context->contextlevel != CONTEXT_COURSE) { return false; } // Make sure the filearea is one of those used by the plugin. if ($filearea !== 'section_icons_images') { return false; } // Make sure the user is logged in and has access to the module (plugins that are not course modules should leave out the 'cm' part). require_login($course, true, $cm); // No check for capability, because everybody needs to see it // Check the relevant capabilities - these may vary depending on the filearea being accessed. /* if (!has_capability('mod/bookcase:addinstance', $context)) { return false; } */ // Leave this line out if you set the itemid to null in make_pluginfile_url (set $itemid to 0 instead). $itemid = array_shift($args); // The first item in the $args array. // Use the itemid to retrieve any relevant data records and perform any security checks to see if the // user really does have access to the file in question. // Extract the filename / filepath from the $args array. $filename = array_pop($args); // The last item in the $args array. if (!$args) { $filepath = '/'; // $args is empty => the path is '/' } else { $filepath = '/'.implode('/', $args).'/'; // $args contains elements of the filepath } // Retrieve the file from the Files API. $fs = get_file_storage(); $file = $fs->get_file($context->id, 'local_section_icons_images', $filearea, $itemid, $filepath, $filename); if (!$file) { return false; // The file does not exist. } // We can now send the file back to the browser - in this case with a cache lifetime of 1 day and no filtering. // From Moodle 2.3, use send_stored_file instead. send_stored_file($file, 86400, 0, $forcedownload, $options); } ===== Attachments in Mod Instance Forms (mod_form.php) ===== See mod assign for an example of how to add a filemanager for attachments. In your mod_form.php: function definition() { // other form elements $mform->addElement('filemanager', 'attachments', get_string('contentimage', 'mod_rainmaker'), null, $filemanager_options); // remaining form elements } function data_preprocessing(&$default_values) { if ($this->current->instance) { // ... //files $draftitemid = file_get_submitted_draft_itemid('attachments'); file_prepare_draft_area($draftitemid, $this->context->id, 'mod_rainmaker', 'attachments', 0, array('subdirs'=>true)); $default_values['attachments'] = $draftitemid; // ... } } In your mod's lib.php file: function rainmaker_save_draft_area_files($data) { $context = context_module::instance($data->coursemodule); if (isset($data->attachments)) { file_save_draft_area_files($data->attachments, $context->id, 'mod_rainmaker', 'attachments', 0, array('subdirs' => 0, 'maxbytes' => $maxbytes, 'maxfiles' => 50)); } } // function rainmaker_save_draft_area_files function rainmaker_add_instance(stdClass $data, mod_rainmaker_mod_form $mform = null) { global $DB; fasetwoelement_save_draft_area_files($data); // ... remaining code } function rainmaker_update_instance(stdClass $data, mod_rainmaker_mod_form $mform = null) { global $DB; fasetwoelement_save_draft_area_files($data); // ... remaining code }