MDL-23296 repository_local: Implementing search through server files
authorFrederic Massart <fred@moodle.com>
Fri, 13 Feb 2015 08:53:42 +0000 (16:53 +0800)
committerFrederic Massart <fmcell@gmail.com>
Thu, 5 Mar 2015 12:41:47 +0000 (13:41 +0100)
lib/filestorage/file_storage.php
repository/local/lib.php

index 0e92edf..aa72269 100644 (file)
@@ -1962,6 +1962,63 @@ class file_storage {
         return $params;
     }
 
+    /**
+     * Search through the server files.
+     *
+     * The query parameter will be used in conjuction with the SQL directive
+     * LIKE, so include '%' in it if you need to. This search will always ignore
+     * user files and directories. Note that the search is case insensitive.
+     *
+     * This query can quickly become inefficient so use it sparignly.
+     *
+     * @param  string  $query The string used with SQL LIKE.
+     * @param  integer $from  The offset to start the search at.
+     * @param  integer $limit The maximum number of results.
+     * @param  boolean $count When true this methods returns the number of results availabe,
+     *                        disregarding the parameters $from and $limit.
+     * @return int|array      Integer when count, otherwise array of stored_file objects.
+     */
+    public function search_server_files($query, $from = 0, $limit = 20, $count = false) {
+        global $DB;
+        $params = array(
+            'contextlevel' => CONTEXT_USER,
+            'directory' => '.',
+            'query' => $query
+        );
+
+        if ($count) {
+            $select = 'COUNT(1)';
+        } else {
+            $select = self::instance_sql_fields('f', 'r');
+        }
+        $like = $DB->sql_like('f.filename', ':query', false);
+
+        $sql = "SELECT $select
+                  FROM {files} f
+             LEFT JOIN {files_reference} r
+                    ON f.referencefileid = r.id
+                  JOIN {context} c
+                    ON f.contextid = c.id
+                 WHERE c.contextlevel <> :contextlevel
+                   AND f.filename <> :directory
+                   AND " . $like . "";
+
+        if ($count) {
+            return $DB->count_records_sql($sql, $params);
+        }
+
+        $sql .= " ORDER BY f.filename";
+
+        $result = array();
+        $filerecords = $DB->get_recordset_sql($sql, $params, $from, $limit);
+        foreach ($filerecords as $filerecord) {
+            $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
+        }
+        $filerecords->close();
+
+        return $result;
+    }
+
     /**
      * Returns all aliases that refer to some stored_file via the given reference
      *
index 18eb721..a74b100 100644 (file)
@@ -45,7 +45,7 @@ class repository_local extends repository {
         global $CFG, $USER, $OUTPUT;
         $ret = array();
         $ret['dynload'] = true;
-        $ret['nosearch'] = true;
+        $ret['nosearch'] = false;
         $ret['nologin'] = true;
         $ret['list'] = array();
 
@@ -252,6 +252,104 @@ class repository_local extends repository {
         );
     }
 
+    /**
+     * Search through all the files.
+     *
+     * This method will do a raw search through the database, then will try
+     * to match with files that a user can access. A maximum of 50 files will be
+     * returned at a time, excluding possible duplicates found along the way.
+     *
+     * Queries are done in chunk of 100 files to prevent too many records to be fetched
+     * at once. When too many files are not included, or a maximum of 10 queries are
+     * performed we consider that this was the last page.
+     *
+     * @param  String  $q    The query string.
+     * @param  integer $page The page number.
+     * @return array of results.
+     */
+    public function search($q, $page = 1) {
+        global $DB, $SESSION;
+
+        // Because the repository API is weird, the first page is 0, but it should be 1.
+        if (!$page) {
+            $page = 1;
+        }
+
+        if (!isset($SESSION->repository_local_search)) {
+            $SESSION->repository_local_search = array();
+        }
+
+        $fs = get_file_storage();
+        $fb = get_file_browser();
+
+        $max = 50;
+        $limit = 100;
+        if ($page <= 1) {
+            $SESSION->repository_local_search['query'] = $q;
+            $SESSION->repository_local_search['from'] = 0;
+            $from = 0;
+        } else {
+            // Yes, the repository does not send the query again...
+            $q = $SESSION->repository_local_search['query'];
+            $from = (int) $SESSION->repository_local_search['from'];
+        }
+
+        $count = $fs->search_server_files('%' . $DB->sql_like_escape($q) . '%', null, null, true);
+        $remaining = $count - $from;
+        $maxloops = 3000;
+        $loops = 0;
+
+        $results = array();
+        while (count($results) < $max && $maxloops > 0 && $remaining > 0) {
+            if (empty($files)) {
+                $files = $fs->search_server_files('%' . $DB->sql_like_escape($q) . '%', $from, $limit);
+                $from += $limit;
+            };
+
+            $remaining--;
+            $maxloops--;
+            $loops++;
+
+            $file = array_shift($files);
+            if (!$file) {
+                // This should not happen.
+                throw new coding_exception('Unexpected end of files list.');
+            }
+
+            $key = $file->get_contenthash() . ':' . $file->get_filename();
+            if (isset($results[$key])) {
+                // We found the file with same content and same name, let's skip it.
+                continue;
+            }
+
+            $ctx = context::instance_by_id($file->get_contextid());
+            $fileinfo = $fb->get_file_info($ctx, $file->get_component(), $file->get_filearea(), $file->get_itemid(),
+                $file->get_filepath(), $file->get_filename());
+            if ($fileinfo) {
+                $results[$key] = $this->get_node($fileinfo);
+            }
+
+        }
+
+        // Save the position for the paging to work.
+        if ($maxloops > 0 && $remaining > 0) {
+            $SESSION->repository_local_search['from'] += $loops;
+            $pages = -1;
+        } else {
+            $SESSION->repository_local_search['from'] = 0;
+            $pages = 0;
+        }
+
+        $return = array(
+            'list' => array_values($results),
+            'dynload' => true,
+            'pages' => $pages,
+            'page' => $page
+        );
+
+        return $return;
+    }
+
     /**
      * Is this repository accessing private data?
      *