Skip to main content

Copy, backup and restore

Reusing a course: copying it into a new one, backing it up (or part of it) to a .mbz file, reading what a backup file holds, making it anonymous to share it, restoring a backup into a new course or an existing one, and importing activities from another course. Each needs Moodle's rights for it, which a site gives teachers only in part: on a default site an editing teacher can back up and import, and restore into their own courses, but not create courses, so copying and restoring into a new course take a manager, a course creator or an administrator. When Moodle refuses, the BackupError says which right it asks for, with Moodle's own message.

Terms: backup, course module id, section.

The signatures below are the functions'. Each operation is also a method of the Moodle object without its first two parameters (fn(context, site, …) is moodle.fn(…)), which is how the examples call them.

Copying goes through Moodle's copy service (core_backup_submit_copy_form), with the copy form's own fields; backing up, restoring and importing through their page wizards, stage by stage, as a person would. Moodle makes copies, and backups and restores when the site makes them asynchronous (its "asynchronous backups" setting), in its scheduled tasks (cron): the driver waits for them, asking Moodle's progress services, up to timeoutMs (ten minutes unless given). A site whose cron doesn't run never finishes them: the BackupError after the wait says so, and Moodle goes on when cron runs. Every function but backupContents and anonymizeBackup, which read and write files on this computer, throws NotLoggedInError when there's no session (see Errors).

A backup is a local file, so it can be restored on another site: back up with one Moodle object and restore with another, each on its site, both in one browser if wished (see Several sites, several accounts).

interface CourseRef {
id: number;
fullName: string;
shortName: string;
categoryId?: number;
url: string;
}

copyCourse​

Copies a course into a new one, as Moodle's "Copy course".

function copyCourse(
context: BrowserContext,
siteUrl: string,
courseId: number,
options: { fullName: string; shortName: string; categoryId?: number; start?: Date; end?: Date; withUsers?: boolean; visible?: boolean; timeoutMs?: number },
): Promise<{ changed: string; course: CourseRef }>;
  • fullName and shortName name the new course (the short name must be free); categoryId is the category (the course's own unless given); start and end its dates; visible whether students see it.
  • withUsers copies the people's data and keeps everyone's enrolments; without it, only the content.
  • Returns the new course, found by its short name once Moodle has finished.

Needs moodle/backup:backupcourse, moodle/restore:restorecourse and moodle/course:create. A copy whose settings Moodle refuses (a short name taken) is a BackupError.

backupCourse​

Backs a course up to a file and downloads it.

function backupCourse(
context: BrowserContext,
siteUrl: string,
courseId: number,
destDir: string,
options?: { withUsers?: boolean; anonymize?: boolean; sections?: number[]; activities?: number[]; anonymous?: boolean; timeoutMs?: number },
): Promise<{ changed: string; file: DownloadedFile }>;
  • The whole course, without its people's data unless withUsers (which needs moodle/backup:userinfo; anonymize makes their names anonymous, Moodle's own setting).
  • sections (section ids) and activities (course module ids, activities and resources alike, from the course's structure) back up only part of the course: each section with everything in it (its subsections too), each activity alone. Moodle needs an activity's section in the backup to hold it, so the section comes along with its name and description, and only the activities chosen. A section or activity the course doesn't have is a MoodleError with code not-found.
  • anonymous makes the file name no one and no site, through anonymizeBackup: true unless given for a selection (sections or activities), false for a whole course. It can't go with withUsers.
  • destDir is an absolute folder: the file is saved there under Moodle's name for it (backup-moodle2-course-3-…mbz), or backup-moodle2-anonymous-<date>.mbz when anonymous, never overwriting (see Files). Moodle keeps it as well, as Moodle made it, in the course's or the person's backup area, as its restore page lists them.

Needs moodle/backup:backupcourse; Moodle refuses a second backup of a course while one is waiting for cron.

Part of a course, to share​

A backup of part of a course is meant to travel: kept, shared with other teachers and restored in their courses, maybe on another site. So it carries the content and nothing of the course's people or of the course itself: the wizard's settings for users, their roles, comments, completion, logs, grade history and xAPI states are off (with users off, so are submissions, attempts, posts and grades), and so are the course's blocks, filters, badges, calendar events, groups, competencies, custom fields, content bank and legacy files, which a restore would otherwise add to the course restored into. Activities' own dates and events come back with them (Moodle rebuilds them). withUsers can't go with a selection.

The driver doesn't take that on trust: it reads the file afterwards, and a file that holds other activities than those asked, or any of the people's data, is deleted and the call fails with BackupError.

Even without people's data, Moodle's file names people and its origin: who uploaded each file and its author's name, who created, changed and owns each question, the original site's address and identifier, the course's id and names, the site's host in the questions' stamps. So a selection is anonymous unless anonymous: false: the file is ready for a catalog shared between teachers.

Backing up only reads the course: Moodle stores the file in the person's private backup area (a backup without users' data goes there) and logs that a backup was made; nothing else changes.

restoreCourse​

Restores a backup file through Moodle's restore wizard.

function restoreCourse(
context: BrowserContext,
siteUrl: string,
file: string,
options: {
into: "new" | number;
categoryId?: number;
mode?: "merge" | "replace";
fullName?: string;
shortName?: string;
withUsers?: boolean;
sectionId?: number;
afterSectionId?: number;
timeoutMs?: number;
},
): Promise<{ changed: string; course: CourseRef; sectionIds?: number[]; activityIds?: number[]; warnings?: RestoreWarning[] }>;
  • into: "new" creates a course in categoryId (needed), named as in the backup unless fullName and shortName are given; it needs moodle/course:create there.
  • into: <course id> restores into that course: mode: "merge" (the default) adds the backup's content to the course's, "replace" deletes the course's content first, which can't be undone. It returns the sections (sectionIds) and activities (activityIds) the course has new afterwards, in the course's order, and what the restore left broken in them (warnings, below).
  • sectionId and afterSectionId say where a merged restore goes (below); without them, where Moodle puts it.
  • withUsers restores the people's data when the backup has it (moodle/restore:userinfo).
  • file is an absolute path to a .mbz; it's uploaded as the restore page's file picker does.

Needs moodle/restore:restorecourse where it restores. A file that isn't a Moodle backup, or one Moodle can't read, is a BackupError.

Checked before the wizard​

The driver reads the backup first (its moodle_backup.xml) and refuses, with a BackupError that names what's missing, a backup Moodle would restore badly:

  • made on a newer Moodle than the site's (by branch: 5.3 on a 5.2 site; 5.2.4 on 5.2.0 goes on). Moodle only warns, and such a restore can fail half-way. The site's branch is read from the documentation link Moodle puts on its restore page; a site that shows none isn't checked.
  • holding activities of a type the site doesn't offer the person in that course (a plugin such as questionnaire not installed, disabled, or not theirs to add): Moodle would restore the rest and skip those without a word. A course where the person can add nothing isn't checked.

A backup in zip format (a site with Moodle's usezipbackups makes them) isn't read: Moodle restores it unchecked.

Where a merged restore goes​

Moodle puts each of the backup's sections into the course's section with the same number, creating it when the course has none, and each activity at the end of its section; a section of the course that had no name, description, restrictions or visibility of its own takes the backup's (restore_section_structure_step, confirmed on Moodle 5.2). The wizard can't choose a place, so the driver restores, then moves what Moodle made:

  • sectionId: every restored activity at the end of that section, in the backup's order; the sections Moodle made for them are deleted.
  • afterSectionId: each of the backup's sections as a new section, in order, right after that one. A section Moodle merged into one of the course's own is split out into a new one, with the backup section's name and visibility (its description too, unless it has files or links to other activities, which only Moodle's restore can carry over).

With either, what Moodle took from the backup into the course's own sections (a name, a description, restrictions, hiding them) is put back. Placing costs a move per restored section, and a look at the course page before and after.

What a restore left broken​

Moodle restores a piece taken out of its course without a word about what it pointed to: a restriction on an activity, a grade, a group or a grouping the backup didn't hold stays, marked as broken; a link in its text to an activity it didn't hold points back to the original one (on the original site). Restored into an existing course, warnings lists them, one per problem, read from the restored activities' settings forms (those with restrictions, and those whose text linked elsewhere in the backup), so a program can tell the teacher what to fix:

interface RestoreWarning {
activityId: number; // the restored activity, in the course restored into
name: string;
kind: "restriction" | "link";
condition?: "completion" | "grade" | "group" | "grouping"; // for a restriction
url?: string; // for a link: where it points
message: string;
}

backupContents tells the same before restoring, from the file.

backupContents​

What a backup file holds, read on this computer: no browser, no session, no site.

function backupContents(file: string): Promise<BackupContents>;

interface BackupContents {
name: string; // the file's
type: "course" | "section" | "activity"; // a part of a course is "course", as Moodle writes it
moodle: { version: string; release: string }; // the Moodle that made it: "2026042004", "5.2.4 (Build: 20261005)"
original: { site?: string; courseId: number; courseFullName?: string; courseShortName?: string; format?: string };
sections: { id: number; number?: number; name?: string; summary?: string; visible?: boolean; parentActivityId?: number }[];
activities: { id: number; type: string; title: string; sectionId: number; dependencies: BackupDependency[] }[];
plugins: string[]; // "mod_quiz", "qtype_multichoice", "block_html", "format_topics"
withUsers: boolean; // it holds people's data
gradebook: boolean; // it holds the course's gradebook setup (a part of a course doesn't)
identifies: BackupIdentifier[]; // what still names a person, the site or the course: empty once anonymous
}

interface BackupIdentifier {
file: string; // the backup's file it's in: "files.xml", "questions.xml", "moodle_backup.xml"…
field: string; // "userid", "author", "createdby", "original_wwwroot", "stamp"…, or "text" for the site's address in a text
value: string;
}

interface BackupDependency {
kind: "restriction" | "link" | "grade-category";
condition?: "completion" | "grade" | "group" | "grouping";
activityId?: number;
module?: string;
gradeItemId?: number;
groupId?: number;
groupingId?: number;
categoryId?: number;
}
  • Ids are the original site's: a section's number is where a merged restore puts it (see above), a section without name takes the course format's default.

  • plugins are what a site needs to restore all of it: the activities' modules, the questions' types, the blocks, and the course format (for a new course). restoreCourse checks the modules before restoring.

  • dependencies, per activity, what it refers to that the backup doesn't hold: a restriction on another activity's completion (activityId), on a grade item (gradeItemId), a group or a grouping; a link in its text to another activity (activityId, module); and, in a backup without the gradebook, the grade-category its grade sat in (restored, it goes to the course's top category; the original may have been the top one too, the backup doesn't say). Read from the activity's XML: the restrictions in module.xml, the links as Moodle encodes them in a backup ($@PAGEVIEWBYID*12@$), the grade items in grades.xml.

  • A file that isn't a Moodle backup is a BackupError that says so; one in zip format (usezipbackups) too, as the driver doesn't read those. A file that isn't there is a MoodleError with code not-found.

  • identifies lists what in the file names a person or where it came from, by the same rules anonymizeBackup clears: a catalog can refuse a file whose list isn't empty.

It's also moodle.backupContents(file) and moodle backup-contents <file>, neither of which opens the browser.

anonymizeBackup​

Writes a copy of a backup file that names no one and no site, for a catalog shared between teachers: on this computer, no browser, no session.

function anonymizeBackup(file: string, destFile: string): Promise<{ changed: string; file: { path: string; name: string; size: number }; cleared: BackupIdentifier[] }>;

What it clears, in every XML file of the backup (as Moodle 5.2 writes them, confirmed on backups of pages with images, files, quizzes with their own and a shared bank's questions, forums, labels and URLs):

  • People: every user id is 0 (userid, usermodified, usercreated, createdby, modifiedby, ownerid and the other fields Moodle annotates as users), every file's author empty (Moodle records the uploader's full name there), a badge's issuer name, address and contact empty.
  • The site: the original site's address becomes https://anonymous.invalid (a name that can't exist) and its identifier hash 32 zeros; the site's host in the question categories' and questions' stamps (localhost:8080+261013225433+oNq8QS) becomes anonymous; the site's address left in a text (a link Moodle doesn't encode, like a profile's) becomes the anonymous one; the course's category name is emptied; the backup's log is emptied.
  • The course: its id becomes 0 and its names "Restored course" and "restored" (in the backup's description and course/course.xml, with its idnumber emptied), the file's name in the description backup.mbz; Moodle's links to the course ($@COURSEVIEWBYID*…@$, the modules' index pages) follow the new id.

What stays: the content (texts, settings, the files themselves, their names and licences), the dates, and the numeric ids of the backup's pieces (activities, sections, contexts, questions), which only tie its files together. A file's own metadata (a photo's EXIF data, a document's author) is the content's, and stays too.

cleared lists what was cleared (as backupContents' identifies gave it), and the copy is read back: nothing may be left. destFile is an absolute path, and may be file itself. A backup with users' data is a BackupError: its people are more than metadata, and Moodle's own anonymization makes those (backupCourse's withUsers with anonymize). Not a backup, or a zip-format one, is a BackupError as for backupContents.

Restored, the copy is a backup from another site, wherever it goes, the site it came from too (confirmed on 5.2, restoring on the site that made it):

  • Everything arrives: activities, their files (a page's images included), the quizzes' questions; links between the activities restored with it are decoded to them, and links to the course and its index pages to the course restored into.
  • The questions and files are the person's who restores it (Moodle gives them the restorer when the backup's users aren't on the site), not their maker's.
  • A quiz's questions from a shared bank the backup left out come into a new bank of the course: the quiz is self-contained. Restored as Moodle made it on its own site, the quiz would point at the original bank's questions instead.
  • Links to activities left out of the backup point to https://anonymous.invalid/… (as Moodle made it, to the original site): restoreCourse reports them as warnings.
  • A new course takes the names "Restored course" and "restored" unless the restore gives others.

It's also moodle.anonymizeBackup(file, destFile) and moodle anonymize-backup <file> <destFile>, neither of which opens the browser.

importActivities​

Imports activities from another course, as Moodle's "Import".

function importActivities(context: BrowserContext, siteUrl: string, fromCourseId: number, toCourseId: number, options?: { activityIds?: number[] }): Promise<{ changed: string; activityIds: number[] }>;
  • Every activity of fromCourseId, or only activityIds (course module ids, from its structure), is added to toCourseId with its section; never its people's data.
  • Returns the course module ids toCourseId has new afterwards.

Needs moodle/restore:restoretargetimport in the course imported into and moodle/backup:backuptargetimport in the other, which editing teachers have in their courses.

Example​

import { BackupError } from "@falkenslab/moodle-driver";

// `moodle` is the object from createMoodle(), as in the getting-started pages.

try {
// Next year's course from this year's, and a forum from another course (4) into it.
const { course } = await moodle.copyCourse(3, { fullName: "Programming 2027", shortName: "prog-2027", start: new Date("2027-09-15") });
const { activityIds } = await moodle.importActivities(4, course.id, { activityIds: [46] });
console.log(`Course ${course.id}, with activities ${activityIds.join(", ")} imported`);

// A backup on disk, restored as a new course somewhere else.
const { file } = await moodle.backupCourse(3, "/home/ada/backups");
const restored = await moodle.restoreCourse(file.path, { into: "new", categoryId: 1, fullName: "Programming (archive)", shortName: "prog-archive" });
console.log(restored.changed);

// A unit (section 12) and a quiz (activity 57) to share (anonymous, as a part is), restored as new sections after section 30 of course 8.
const part = await moodle.backupCourse(3, "/home/ada/backups", { sections: [12], activities: [57] });
const contents = await moodle.backupContents(part.file.path);
if (contents.identifies.length > 0) throw new Error("Not ready for the catalog");
for (const activity of contents.activities) if (activity.dependencies.length > 0) console.log(`"${activity.title}" depends on things left out`, activity.dependencies);
const placed = await moodle.restoreCourse(part.file.path, { into: 8, afterSectionId: 30 });
console.log(`New sections ${placed.sectionIds?.join(", ")}, new activities ${placed.activityIds?.join(", ")}`);
for (const warning of placed.warnings ?? []) console.log(warning.message);
} catch (error) {
if (error instanceof BackupError) console.error(error.message);
else throw error;
}

Data categories​

  • copyCourse, backupCourse, restoreCourse: course-content, and others-personal (the people's data, with withUsers).
  • importActivities, backupContents, anonymizeBackup: course-content.

See Data categories.