mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-26 15:48:32 -06:00
workign on test folders
This commit is contained in:
181
nextjs/src/services/fileStorage/utils/couresMarkdownLoader.ts
Normal file
181
nextjs/src/services/fileStorage/utils/couresMarkdownLoader.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { LocalAssignment, localAssignmentMarkdown } from "@/models/local/assignmnet/localAssignment";
|
||||
import { LocalCourse, LocalCourseSettings, localCourseYamlUtils } from "@/models/local/localCourse";
|
||||
import { LocalModule } from "@/models/local/localModules";
|
||||
import { LocalCoursePage, localPageMarkdownUtils } from "@/models/local/page/localCoursePage";
|
||||
import { LocalQuiz, localQuizMarkdownUtils } from "@/models/local/quiz/localQuiz";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
const basePath = process.env.STORAGE_DIRECTORY ?? "./storage";
|
||||
|
||||
export const courseMarkdownLoader = {
|
||||
async loadSavedCourses(): Promise<LocalCourse[]> {
|
||||
const courseDirectories = await fs.readdir(basePath, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
const coursePromises = courseDirectories
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map(async (dirent) => {
|
||||
const coursePath = path.join(basePath, dirent.name);
|
||||
const settingsPath = path.join(coursePath, "settings.yml");
|
||||
if (await this.fileExists(settingsPath)) {
|
||||
return this.loadCourseByPath(coursePath);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const courses = (await Promise.all(coursePromises)).filter(
|
||||
(course) => course !== null
|
||||
) as LocalCourse[];
|
||||
return courses.sort((a, b) =>
|
||||
a.settings.name.localeCompare(b.settings.name)
|
||||
);
|
||||
},
|
||||
|
||||
async loadCourseByPath(courseDirectory: string): Promise<LocalCourse> {
|
||||
if (!(await this.directoryExists(courseDirectory))) {
|
||||
const errorMessage = `Error loading course by name, could not find folder ${courseDirectory}`;
|
||||
console.log(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const settings = await this.loadCourseSettings(courseDirectory);
|
||||
const modules = await this.loadCourseModules(courseDirectory);
|
||||
|
||||
return {
|
||||
settings,
|
||||
modules,
|
||||
};
|
||||
},
|
||||
|
||||
async loadCourseSettings(
|
||||
courseDirectory: string
|
||||
): Promise<LocalCourseSettings> {
|
||||
const settingsPath = path.join(courseDirectory, "settings.yml");
|
||||
if (!(await this.fileExists(settingsPath))) {
|
||||
const errorMessage = `Error loading course by name, settings file ${settingsPath}`;
|
||||
console.log(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const settingsString = await fs.readFile(settingsPath, "utf-8");
|
||||
const settings = localCourseYamlUtils.parseSettingYaml(settingsString);
|
||||
|
||||
const folderName = path.basename(courseDirectory);
|
||||
return { ...settings, name: folderName };
|
||||
},
|
||||
|
||||
async loadCourseModules(courseDirectory: string): Promise<LocalModule[]> {
|
||||
const moduleDirectories = await fs.readdir(courseDirectory, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
const modulePromises = moduleDirectories
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) =>
|
||||
this.loadModuleFromPath(path.join(courseDirectory, dirent.name))
|
||||
);
|
||||
|
||||
const modules = await Promise.all(modulePromises);
|
||||
return modules.sort((a, b) => a.name.localeCompare(b.name));
|
||||
},
|
||||
|
||||
async loadModuleFromPath(modulePath: string): Promise<LocalModule> {
|
||||
const moduleName = path.basename(modulePath);
|
||||
const assignments = await this.loadAssignmentsFromPath(modulePath);
|
||||
const quizzes = await this.loadQuizzesFromPath(modulePath);
|
||||
const pages = await this.loadModulePagesFromPath(modulePath);
|
||||
|
||||
return {
|
||||
name: moduleName,
|
||||
assignments,
|
||||
quizzes,
|
||||
pages,
|
||||
};
|
||||
},
|
||||
|
||||
async loadAssignmentsFromPath(
|
||||
modulePath: string
|
||||
): Promise<LocalAssignment[]> {
|
||||
const assignmentsPath = path.join(modulePath, "assignments");
|
||||
if (!(await this.directoryExists(assignmentsPath))) {
|
||||
console.log(
|
||||
`Error loading course by name, assignments folder does not exist in ${modulePath}`
|
||||
);
|
||||
await fs.mkdir(assignmentsPath);
|
||||
}
|
||||
|
||||
const assignmentFiles = await fs.readdir(assignmentsPath);
|
||||
const assignmentPromises = assignmentFiles.map(async (file) => {
|
||||
const filePath = path.join(assignmentsPath, file);
|
||||
const rawFile = (await fs.readFile(filePath, "utf-8")).replace(
|
||||
/\r\n/g,
|
||||
"\n"
|
||||
);
|
||||
return localAssignmentMarkdown.parseMarkdown(rawFile);
|
||||
});
|
||||
|
||||
return await Promise.all(assignmentPromises);
|
||||
},
|
||||
|
||||
async loadQuizzesFromPath(modulePath: string): Promise<LocalQuiz[]> {
|
||||
const quizzesPath = path.join(modulePath, "quizzes");
|
||||
if (!(await this.directoryExists(quizzesPath))) {
|
||||
console.log(
|
||||
`Quizzes folder does not exist in ${modulePath}, creating now`
|
||||
);
|
||||
await fs.mkdir(quizzesPath);
|
||||
}
|
||||
|
||||
const quizFiles = await fs.readdir(quizzesPath);
|
||||
const quizPromises = quizFiles.map(async (file) => {
|
||||
const filePath = path.join(quizzesPath, file);
|
||||
const rawQuiz = (await fs.readFile(filePath, "utf-8")).replace(
|
||||
/\r\n/g,
|
||||
"\n"
|
||||
);
|
||||
return localQuizMarkdownUtils.parseMarkdown(rawQuiz);
|
||||
});
|
||||
|
||||
return await Promise.all(quizPromises);
|
||||
},
|
||||
|
||||
async loadModulePagesFromPath(
|
||||
modulePath: string
|
||||
): Promise<LocalCoursePage[]> {
|
||||
const pagesPath = path.join(modulePath, "pages");
|
||||
if (!(await this.directoryExists(pagesPath))) {
|
||||
console.log(`Pages folder does not exist in ${modulePath}, creating now`);
|
||||
await fs.mkdir(pagesPath);
|
||||
}
|
||||
|
||||
const pageFiles = await fs.readdir(pagesPath);
|
||||
const pagePromises = pageFiles.map(async (file) => {
|
||||
const filePath = path.join(pagesPath, file);
|
||||
const rawPage = (await fs.readFile(filePath, "utf-8")).replace(
|
||||
/\r\n/g,
|
||||
"\n"
|
||||
);
|
||||
return localPageMarkdownUtils.parseMarkdown(rawPage);
|
||||
});
|
||||
|
||||
return await Promise.all(pagePromises);
|
||||
},
|
||||
|
||||
async fileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
async directoryExists(directoryPath: string): Promise<boolean> {
|
||||
try {
|
||||
const stat = await fs.stat(directoryPath);
|
||||
return stat.isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
152
nextjs/src/services/fileStorage/utils/courseDifferences.ts
Normal file
152
nextjs/src/services/fileStorage/utils/courseDifferences.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { LocalCourse, LocalCourseSettings } from "@/models/local/localCourse";
|
||||
import { LocalModule } from "@/models/local/localModules";
|
||||
|
||||
export const CourseDifferences = {
|
||||
getDeletedChanges(
|
||||
newCourse: LocalCourse,
|
||||
oldCourse: LocalCourse
|
||||
): DeleteCourseChanges {
|
||||
if (newCourse === oldCourse) {
|
||||
const emptyDeletes: DeleteCourseChanges = {
|
||||
namesOfModulesToDeleteCompletely: [],
|
||||
deleteContentsOfModule: [],
|
||||
};
|
||||
return emptyDeletes;
|
||||
}
|
||||
|
||||
const moduleNamesNoLongerReferenced = oldCourse.modules
|
||||
.filter(
|
||||
(oldModule) =>
|
||||
!newCourse.modules.some(
|
||||
(newModule) => newModule.name === oldModule.name
|
||||
)
|
||||
)
|
||||
.map((oldModule) => oldModule.name);
|
||||
|
||||
const modulesWithDeletions = oldCourse.modules
|
||||
.filter(
|
||||
(oldModule) =>
|
||||
!newCourse.modules.some(
|
||||
(newModule) =>
|
||||
JSON.stringify(newModule) === JSON.stringify(oldModule)
|
||||
)
|
||||
)
|
||||
.map((oldModule) => {
|
||||
const newModule = newCourse.modules.find(
|
||||
(m) => m.name === oldModule.name
|
||||
);
|
||||
if (!newModule) return oldModule;
|
||||
|
||||
const unreferencedAssignments = oldModule.assignments.filter(
|
||||
(oldAssignment) =>
|
||||
!newModule.assignments.some(
|
||||
(newAssignment) => newAssignment.name === oldAssignment.name
|
||||
)
|
||||
);
|
||||
const unreferencedQuizzes = oldModule.quizzes.filter(
|
||||
(oldQuiz) =>
|
||||
!newModule.quizzes.some((newQuiz) => newQuiz.name === oldQuiz.name)
|
||||
);
|
||||
const unreferencedPages = oldModule.pages.filter(
|
||||
(oldPage) =>
|
||||
!newModule.pages.some((newPage) => newPage.name === oldPage.name)
|
||||
);
|
||||
|
||||
return {
|
||||
...oldModule,
|
||||
assignments: unreferencedAssignments,
|
||||
quizzes: unreferencedQuizzes,
|
||||
pages: unreferencedPages,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
namesOfModulesToDeleteCompletely: moduleNamesNoLongerReferenced,
|
||||
deleteContentsOfModule: modulesWithDeletions,
|
||||
};
|
||||
},
|
||||
|
||||
getNewChanges(
|
||||
newCourse: LocalCourse,
|
||||
oldCourse: LocalCourse
|
||||
): NewCourseChanges {
|
||||
if (newCourse === oldCourse) {
|
||||
const emptyChanges: NewCourseChanges = {
|
||||
modules: [],
|
||||
};
|
||||
return emptyChanges;
|
||||
}
|
||||
|
||||
const differentModules = newCourse.modules
|
||||
.filter(
|
||||
(newModule) =>
|
||||
!oldCourse.modules.some(
|
||||
(oldModule) =>
|
||||
JSON.stringify(oldModule) === JSON.stringify(newModule)
|
||||
)
|
||||
)
|
||||
.map((newModule) => {
|
||||
const oldModule = oldCourse.modules.find(
|
||||
(m) => m.name === newModule.name
|
||||
);
|
||||
if (!oldModule) return newModule;
|
||||
|
||||
const newAssignments = newModule.assignments.filter(
|
||||
(newAssignment) =>
|
||||
!oldModule.assignments.some(
|
||||
(oldAssignment) =>
|
||||
JSON.stringify(newAssignment) === JSON.stringify(oldAssignment)
|
||||
)
|
||||
);
|
||||
const newQuizzes = newModule.quizzes.filter(
|
||||
(newQuiz) =>
|
||||
!oldModule.quizzes.some(
|
||||
(oldQuiz) => JSON.stringify(newQuiz) === JSON.stringify(oldQuiz)
|
||||
)
|
||||
);
|
||||
const newPages = newModule.pages.filter(
|
||||
(newPage) =>
|
||||
!oldModule.pages.some(
|
||||
(oldPage) => JSON.stringify(newPage) === JSON.stringify(oldPage)
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
...newModule,
|
||||
assignments: newAssignments,
|
||||
quizzes: newQuizzes,
|
||||
pages: newPages,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
settings: newCourse.settings,
|
||||
modules: differentModules,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export interface DeleteCourseChanges {
|
||||
namesOfModulesToDeleteCompletely: string[];
|
||||
deleteContentsOfModule: LocalModule[];
|
||||
}
|
||||
|
||||
export interface NewCourseChanges {
|
||||
modules: LocalModule[];
|
||||
settings?: LocalCourseSettings;
|
||||
}
|
||||
|
||||
// Default values for DeleteCourseChanges and NewCourseChanges
|
||||
// export const createDeleteCourseChanges = (
|
||||
// init?: Partial<DeleteCourseChanges>
|
||||
// ): DeleteCourseChanges => ({
|
||||
// namesOfModulesToDeleteCompletely: init?.namesOfModulesToDeleteCompletely ?? [],
|
||||
// deleteContentsOfModule: init?.deleteContentsOfModule ?? [],
|
||||
// });
|
||||
|
||||
// export const createNewCourseChanges = (
|
||||
// init?: Partial<NewCourseChanges>
|
||||
// ): NewCourseChanges => ({
|
||||
// modules: init?.modules ?? [],
|
||||
// settings: init?.settings,
|
||||
// });
|
||||
245
nextjs/src/services/fileStorage/utils/courseMarkdownSaver.ts
Normal file
245
nextjs/src/services/fileStorage/utils/courseMarkdownSaver.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { localAssignmentMarkdown } from "@/models/local/assignmnet/localAssignment";
|
||||
import { LocalCourse, localCourseYamlUtils } from "@/models/local/localCourse";
|
||||
import { LocalModule } from "@/models/local/localModules";
|
||||
import { localPageMarkdownUtils } from "@/models/local/page/localCoursePage";
|
||||
import { quizMarkdownUtils } from "@/models/local/quiz/utils/quizMarkdownUtils";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
|
||||
const basePath = process.env.STORAGE_DIRECTORY ?? "./storage";
|
||||
|
||||
const directoryExists = async (directoryPath: string): Promise<boolean> => {
|
||||
try {
|
||||
const stat = await fs.stat(directoryPath);
|
||||
return stat.isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveSettings = async (course: LocalCourse, courseDirectory: string) => {
|
||||
const settingsFilePath = path.join(courseDirectory, "settings.yml");
|
||||
const settingsYaml = localCourseYamlUtils.settingsToYaml(course.settings);
|
||||
await fs.writeFile(settingsFilePath, settingsYaml);
|
||||
};
|
||||
|
||||
const saveModules = async (
|
||||
course: LocalCourse,
|
||||
courseDirectory: string,
|
||||
previouslyStoredCourse?: LocalCourse
|
||||
) => {
|
||||
for (const localModule of course.modules) {
|
||||
const moduleDirectory = path.join(courseDirectory, localModule.name);
|
||||
if (!(await directoryExists(moduleDirectory))) {
|
||||
await fs.mkdir(moduleDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
await saveQuizzes(course, localModule, previouslyStoredCourse);
|
||||
await saveAssignments(course, localModule, previouslyStoredCourse);
|
||||
await savePages(course, localModule, previouslyStoredCourse);
|
||||
}
|
||||
|
||||
const moduleNames = course.modules.map((m) => m.name);
|
||||
const moduleDirectories = await fs.readdir(courseDirectory, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
for (const dirent of moduleDirectories) {
|
||||
if (dirent.isDirectory() && !moduleNames.includes(dirent.name)) {
|
||||
const moduleDirPath = path.join(courseDirectory, dirent.name);
|
||||
console.log(
|
||||
`Deleting extra module directory, it was probably renamed ${moduleDirPath}`
|
||||
);
|
||||
await fs.rmdir(moduleDirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const saveQuizzes = async (
|
||||
course: LocalCourse,
|
||||
module: LocalModule,
|
||||
previouslyStoredCourse?: LocalCourse
|
||||
) => {
|
||||
const quizzesDirectory = path.join(
|
||||
basePath,
|
||||
course.settings.name,
|
||||
module.name,
|
||||
"quizzes"
|
||||
);
|
||||
if (!(await directoryExists(quizzesDirectory))) {
|
||||
await fs.mkdir(quizzesDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
for (const quiz of module.quizzes) {
|
||||
const previousModule = previouslyStoredCourse?.modules.find(
|
||||
(m) => m.name === module.name
|
||||
);
|
||||
const previousQuiz = previousModule?.quizzes.find((q) => q === quiz);
|
||||
|
||||
if (!previousQuiz) {
|
||||
const markdownPath = path.join(quizzesDirectory, `${quiz.name}.md`);
|
||||
const quizMarkdown = quizMarkdownUtils.toMarkdown(quiz);
|
||||
console.log(`Saving quiz ${markdownPath}`);
|
||||
await fs.writeFile(markdownPath, quizMarkdown);
|
||||
}
|
||||
}
|
||||
|
||||
await removeOldQuizzes(quizzesDirectory, module);
|
||||
};
|
||||
|
||||
const saveAssignments = async (
|
||||
course: LocalCourse,
|
||||
module: LocalModule,
|
||||
previouslyStoredCourse?: LocalCourse
|
||||
) => {
|
||||
const assignmentsDirectory = path.join(
|
||||
basePath,
|
||||
course.settings.name,
|
||||
module.name,
|
||||
"assignments"
|
||||
);
|
||||
if (!(await directoryExists(assignmentsDirectory))) {
|
||||
await fs.mkdir(assignmentsDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
for (const assignment of module.assignments) {
|
||||
const previousModule = previouslyStoredCourse?.modules.find(
|
||||
(m) => m.name === module.name
|
||||
);
|
||||
const previousAssignment = previousModule?.assignments.find(
|
||||
(a) => a === assignment
|
||||
);
|
||||
|
||||
if (!previousAssignment) {
|
||||
const assignmentMarkdown = localAssignmentMarkdown.toMarkdown(assignment);
|
||||
const filePath = path.join(assignmentsDirectory, `${assignment.name}.md`);
|
||||
console.log(`Saving assignment ${filePath}`);
|
||||
await fs.writeFile(filePath, assignmentMarkdown);
|
||||
}
|
||||
}
|
||||
|
||||
await removeOldAssignments(assignmentsDirectory, module);
|
||||
};
|
||||
|
||||
const savePages = async (
|
||||
course: LocalCourse,
|
||||
module: LocalModule,
|
||||
previouslyStoredCourse?: LocalCourse
|
||||
) => {
|
||||
const pagesDirectory = path.join(
|
||||
basePath,
|
||||
course.settings.name,
|
||||
module.name,
|
||||
"pages"
|
||||
);
|
||||
if (!(await directoryExists(pagesDirectory))) {
|
||||
await fs.mkdir(pagesDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
for (const page of module.pages) {
|
||||
const previousModule = previouslyStoredCourse?.modules.find(
|
||||
(m) => m.name === module.name
|
||||
);
|
||||
const previousPage = previousModule?.pages.find((p) => p === page);
|
||||
|
||||
if (!previousPage) {
|
||||
const pageMarkdown = localPageMarkdownUtils.toMarkdown(page);
|
||||
const filePath = path.join(pagesDirectory, `${page.name}.md`);
|
||||
console.log(`Saving page ${filePath}`);
|
||||
await fs.writeFile(filePath, pageMarkdown);
|
||||
}
|
||||
}
|
||||
|
||||
await removeOldPages(pagesDirectory, module);
|
||||
};
|
||||
|
||||
const removeOldQuizzes = async (
|
||||
quizzesDirectory: string,
|
||||
module: LocalModule
|
||||
) => {
|
||||
const existingFiles = await fs.readdir(quizzesDirectory);
|
||||
const quizFilesToDelete = existingFiles.filter((file) => {
|
||||
const quizMarkdownPath = path.join(
|
||||
quizzesDirectory,
|
||||
`${file.replace(".md", "")}.md`
|
||||
);
|
||||
return !module.quizzes.some(
|
||||
(quiz) =>
|
||||
path.join(quizzesDirectory, `${quiz.name}.md`) === quizMarkdownPath
|
||||
);
|
||||
});
|
||||
|
||||
for (const file of quizFilesToDelete) {
|
||||
console.log(
|
||||
`Removing old quiz, it has probably been renamed ${path.join(
|
||||
quizzesDirectory,
|
||||
file
|
||||
)}`
|
||||
);
|
||||
await fs.unlink(path.join(quizzesDirectory, file));
|
||||
}
|
||||
};
|
||||
|
||||
const removeOldAssignments = async (
|
||||
assignmentsDirectory: string,
|
||||
module: LocalModule
|
||||
) => {
|
||||
const existingFiles = await fs.readdir(assignmentsDirectory);
|
||||
const assignmentFilesToDelete = existingFiles.filter((file) => {
|
||||
const assignmentMarkdownPath = path.join(
|
||||
assignmentsDirectory,
|
||||
`${file.replace(".md", "")}.md`
|
||||
);
|
||||
return !module.assignments.some(
|
||||
(assignment) =>
|
||||
path.join(assignmentsDirectory, `${assignment.name}.md`) ===
|
||||
assignmentMarkdownPath
|
||||
);
|
||||
});
|
||||
|
||||
for (const file of assignmentFilesToDelete) {
|
||||
console.log(
|
||||
`Removing old assignment, it has probably been renamed ${path.join(
|
||||
assignmentsDirectory,
|
||||
file
|
||||
)}`
|
||||
);
|
||||
await fs.unlink(path.join(assignmentsDirectory, file));
|
||||
}
|
||||
};
|
||||
|
||||
const removeOldPages = async (pagesDirectory: string, module: LocalModule) => {
|
||||
const existingFiles = await fs.readdir(pagesDirectory);
|
||||
const pageFilesToDelete = existingFiles.filter((file) => {
|
||||
const pageMarkdownPath = path.join(
|
||||
pagesDirectory,
|
||||
`${file.replace(".md", "")}.md`
|
||||
);
|
||||
return !module.pages.some(
|
||||
(page) =>
|
||||
path.join(pagesDirectory, `${page.name}.md`) === pageMarkdownPath
|
||||
);
|
||||
});
|
||||
|
||||
for (const file of pageFilesToDelete) {
|
||||
console.log(
|
||||
`Removing old page, it has probably been renamed ${path.join(
|
||||
pagesDirectory,
|
||||
file
|
||||
)}`
|
||||
);
|
||||
await fs.unlink(path.join(pagesDirectory, file));
|
||||
}
|
||||
};
|
||||
|
||||
export const courseMarkdownSaver = {
|
||||
async save(course: LocalCourse, previouslyStoredCourse?: LocalCourse) {
|
||||
const courseDirectory = path.join(basePath, course.settings.name);
|
||||
if (!(await directoryExists(courseDirectory))) {
|
||||
await fs.mkdir(courseDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
await saveSettings(course, courseDirectory);
|
||||
await saveModules(course, courseDirectory, previouslyStoredCourse);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user