Download a course's files
A program for a student that saves every file of a course (file resources and the files in folders) to a local folder, one subfolder per section, to keep or read offline.
Uses: courses, content, files.
The program
import path from "node:path";
import { createMoodle, MoodleError } from "@falkenslab/moodle-driver";
// The course id, from listCourses() (the id in course/view.php?id=3).
const COURSE_ID = 3;
// Where the files go, as an absolute path: one subfolder per section.
const ROOT = "/home/ada/moodle-files/programming-101";
await using moodle = createMoodle({
site: "https://campus.example.org/",
profileDir: "/home/ada/.moodle-profile",
headless: true,
downloadBrowser: true,
// Reading a file's link isn't reading: no pauses needed, and a folder is read from its tree at once.
trail: "fast",
// What downloadFile(url) uses when given no folder; here, the root the section folders go in.
downloadDir: ROOT,
});
await moodle.logIn();
// "03 Recursion": the number first, so folders sort as the course does; no characters a folder name can't have.
const folderFor = (number: number, title: string) => `${String(number).padStart(2, "0")} ${title.replace(/[\\/:*?"<>|.]+/g, "_").trim()}`;
let count = 0;
for (const section of await moodle.courseStructure(COURSE_ID)) {
// File resources ("resource") and folders; restricted ones can't be opened yet.
const withFiles = section.activities.filter((a) => (a.type === "resource" || a.type === "folder") && a.availableToMe);
if (withFiles.length === 0) continue;
const dir = path.join(moodle.defaults.downloadDir ?? ROOT, folderFor(section.number, section.title));
console.log(`${section.title}/`);
for (const activity of withFiles) {
// The links only (pluginfile.php URLs): nothing is downloaded yet, but Moodle logs the view.
const { files = [] } = await moodle.activityContent(activity);
for (const file of files) {
try {
// Fetched with the session's cookies, without opening a tab; an existing name becomes "name (2).ext".
const saved = await moodle.downloadFile(file.url, dir);
console.log(` ${saved.name} (${Math.round(saved.size / 1024)} KB)`);
count++;
} catch (error) {
// A file removed or hidden since the folder was read: skip it and go on.
if (error instanceof MoodleError && error.code === "not-found") console.log(` ${file.name}: not available`);
else throw error;
}
}
}
}
console.log(`\n${count} files saved in ${ROOT}`);
Step by step
createMoodletakestrail: "fast"anddownloadDironce, as defaults for every call (see Defaults). The fast trail fits here: there's no text to read, only links.logIn()opens a window the first time for the person to log in, and answers at once afterwards.courseStructure(COURSE_ID)gives the course's sections in order, each with its activities.COURSE_IDcomes fromlistCourses(); each activity'sidandtypecome from here.- For each file resource or folder the person can open,
activityContent(activity)returnsfiles: their names andpluginfile.phplinks. A file resource has one; a folder has the files its tree shows, all in one list. downloadFile(url, dir)saves each one in the section's folder, creating it when needed. It names the file as Moodle sends it and never overwrites.
What it prints
An example; the titles and names are the course's.
General/
Syllabus.pdf (182 KB)
01 Getting started/
Installing Python.pdf (640 KB)
02 Loops/
Loops slides.pdf (1204 KB)
exercises.zip (36 KB)
solutions.zip: not available
03 Recursion/
Recursion notes.pdf (512 KB)
5 files saved in /home/ada/moodle-files/programming-101
Variations
- Every file in one folder, no subfolders:
await moodle.downloadFile(file.url)saves into the object'sdownloadDir. - The files attached to assignments' descriptions too: for activities of type
"assign",(await moodle.studentAssignment(activity.id)).descriptionFileshas the same{ name, url }links. - Every course in progress: loop over
await moodle.listCourses("inprogress")and use each course'sshortnameas the root's subfolder. - Leave a realistic trail in the logs instead (a person opening each resource): drop
trail: "fast". Files and folders aren't paused anyway, so it only matters if the program also reads pages.
When something goes wrong
NotLoggedInError: the session expired, or a file's URL answered with the login page. Callmoodle.logIn()and run again; files already saved are kept, and saved again as "name (2)" unless the program skips them.MoodleErrorwith codenot-found: Moodle answered with an error status for that file (removed, or hidden from the person). The program skips it.MoodleErrorwith codeoutside-site: the URL isn't on the site. It doesn't happen withfilesfrom a resource or folder, which arepluginfile.phplinks; it does with a URL resource's target.NotViewableErrorfromactivityContent: the activity can't be viewed as that type (it was changed, or the person lost access). See Errors.
Data it touches
courseStructure is course-content and own, activityContent is course-content, and downloadFile is course-content and others-personal (the same function saves others' submissions; here every URL comes from the course's resources). See data categories.