diff --git a/backup/util/dbops/restore_dbops.class.php b/backup/util/dbops/restore_dbops.class.php
index bdb3775b7a2..533553abe3f 100644
--- a/backup/util/dbops/restore_dbops.class.php
+++ b/backup/util/dbops/restore_dbops.class.php
@@ -848,6 +848,9 @@ abstract class restore_dbops {
      * optionally one source itemname to match itemids
      * put the corresponding files in the pool
      *
+     * If you specify a progress reporter, it will get called once per file with
+     * indeterminate progress.
+     *
      * @param string $basepath the full path to the root of unzipped backup file
      * @param string $restoreid the restore job's identification
      * @param string $component
@@ -858,9 +861,13 @@ abstract class restore_dbops {
      * @param int|null $olditemid
      * @param int|null $forcenewcontextid explicit value for the new contextid (skip mapping)
      * @param bool $skipparentitemidctxmatch
+     * @param core_backup_progress $progress Optional progress reporter
      * @return array of result object
      */
-    public static function send_files_to_pool($basepath, $restoreid, $component, $filearea, $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null, $forcenewcontextid = null, $skipparentitemidctxmatch = false) {
+    public static function send_files_to_pool($basepath, $restoreid, $component, $filearea,
+            $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null,
+            $forcenewcontextid = null, $skipparentitemidctxmatch = false,
+            core_backup_progress $progress = null) {
         global $DB, $CFG;
 
         $backupinfo = backup_general_helper::get_backup_information(basename($basepath));
@@ -924,8 +931,17 @@ abstract class restore_dbops {
 
         $fs = get_file_storage();         // Get moodle file storage
         $basepath = $basepath . '/files/';// Get backup file pool base
+        // Report progress before query.
+        if ($progress) {
+            $progress->progress();
+        }
         $rs = $DB->get_recordset_sql($sql, $params);
         foreach ($rs as $rec) {
+            // Report progress each time around loop.
+            if ($progress) {
+                $progress->progress();
+            }
+
             $file = (object)backup_controller_dbops::decode_backup_temp_info($rec->info);
 
             // ignore root dirs (they are created automatically)
diff --git a/backup/util/plan/restore_structure_step.class.php b/backup/util/plan/restore_structure_step.class.php
index 822c71b493c..300f92d485d 100644
--- a/backup/util/plan/restore_structure_step.class.php
+++ b/backup/util/plan/restore_structure_step.class.php
@@ -108,10 +108,10 @@ abstract class restore_structure_step extends restore_step {
 
         // And process it, dispatch to target methods in step will start automatically
         $xmlparser->process();
-        $progress->end_progress();
 
         // Have finished, launch the after_execute method of all the processing objects
         $this->launch_after_execute_methods();
+        $progress->end_progress();
     }
 
     /**
@@ -223,9 +223,20 @@ abstract class restore_structure_step extends restore_step {
      * Add all the existing file, given their component and filearea and one backup_ids itemname to match with
      */
     public function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null, $olditemid = null) {
+        // If the current progress object is set up and ready to receive
+        // indeterminate progress, then use it, otherwise don't. (This check is
+        // just in case this function is ever called from somewhere not within
+        // the execute() method here, which does set up progress like this.)
+        $progress = $this->get_task()->get_progress();
+        if (!$progress->is_in_progress_section() ||
+                $progress->get_current_max() !== core_backup_progress::INDETERMINATE) {
+            $progress = null;
+        }
+
         $filesctxid = is_null($filesctxid) ? $this->task->get_old_contextid() : $filesctxid;
         $results = restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component,
-                $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid);
+                $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid, null, false,
+                $progress);
         $resultstoadd = array();
         foreach ($results as $result) {
             $this->log($result->message, $result->level);
diff --git a/backup/util/progress/core_backup_progress.class.php b/backup/util/progress/core_backup_progress.class.php
index 513b18f1ea8..d4a5012075e 100644
--- a/backup/util/progress/core_backup_progress.class.php
+++ b/backup/util/progress/core_backup_progress.class.php
@@ -227,6 +227,20 @@ abstract class core_backup_progress {
         return !empty($this->descriptions);
     }
 
+    /**
+     * Checks max value of current progress section.
+     *
+     * @return int Current max value (may be core_backup_progress::INDETERMINATE)
+     * @throws coding_exception If not in a progress section
+     */
+    public function get_current_max() {
+        $max = end($this->maxes);
+        if ($max === false) {
+            throw new coding_exception('Not inside progress section');
+        }
+        return $max;
+    }
+
     /**
      * @return string Current progress section description
      */