feat(backend): Verwerking van leerobjecten in ZIP-formaat.
This commit is contained in:
		
							parent
							
								
									509dd6bfab
								
							
						
					
					
						commit
						86ba4ea11e
					
				
					 4 changed files with 192 additions and 1 deletions
				
			
		|  | @ -23,6 +23,8 @@ | |||
|         "@mikro-orm/postgresql": "6.4.12", | ||||
|         "@mikro-orm/reflection": "6.4.12", | ||||
|         "@mikro-orm/sqlite": "6.4.12", | ||||
|         "@types/mime-types": "^2.1.4", | ||||
|         "@types/unzipper": "^0.10.11", | ||||
|         "axios": "^1.8.2", | ||||
|         "cors": "^2.8.5", | ||||
|         "cross": "^1.0.0", | ||||
|  | @ -37,8 +39,10 @@ | |||
|         "jwks-rsa": "^3.1.0", | ||||
|         "loki-logger-ts": "^1.0.2", | ||||
|         "marked": "^15.0.7", | ||||
|         "mime-types": "^3.0.1", | ||||
|         "response-time": "^2.3.3", | ||||
|         "swagger-ui-express": "^5.0.1", | ||||
|         "unzipper": "^0.12.3", | ||||
|         "uuid": "^11.1.0", | ||||
|         "winston": "^3.17.0", | ||||
|         "winston-loki": "^6.1.3" | ||||
|  |  | |||
|  | @ -2,7 +2,13 @@ import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provid | |||
| import { LearningObjectProvider } from './learning-object-provider.js'; | ||||
| import { envVars, getEnvVar } from '../../util/envVars.js'; | ||||
| import databaseLearningObjectProvider from './database-learning-object-provider.js'; | ||||
| import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| import { | ||||
|     FilteredLearningObject, | ||||
|     LearningObjectIdentifierDTO, | ||||
|     LearningPathIdentifier | ||||
| } from '@dwengo-1/common/interfaces/learning-content'; | ||||
| import {getLearningObjectRepository} from "../../data/repositories"; | ||||
| import {processLearningObjectZip} from "./learning-object-zip-processing-service"; | ||||
| 
 | ||||
| function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { | ||||
|     if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { | ||||
|  | @ -42,6 +48,21 @@ const learningObjectService = { | |||
|     async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise<string | null> { | ||||
|         return getProvider(id).getLearningObjectHTML(id); | ||||
|     }, | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Store the learning object in the given zip file in the database. | ||||
|      */ | ||||
|     async storeLearningObject(learningObjectPath: string): Promise<void> { | ||||
|         const learningObjectRepository = getLearningObjectRepository(); | ||||
|         const learningObject = await processLearningObjectZip(learningObjectPath); | ||||
| 
 | ||||
|         if (!learningObject.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { | ||||
|             throw Error("Learning object name must start with the user content prefix!"); | ||||
|         } | ||||
| 
 | ||||
|         await learningObjectRepository.save(learningObject, {preventOverwrite: true}); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| export default learningObjectService; | ||||
|  |  | |||
|  | @ -0,0 +1,63 @@ | |||
| import unzipper from 'unzipper'; | ||||
| import mime from 'mime-types'; | ||||
| import {LearningObjectMetadata} from "@dwengo-1/common/dist/interfaces/learning-content"; | ||||
| import {LearningObject} from "../../entities/content/learning-object.entity"; | ||||
| import {getAttachmentRepository, getLearningObjectRepository} from "../../data/repositories"; | ||||
| 
 | ||||
| /** | ||||
|  * Process an uploaded zip file and construct a LearningObject from its contents. | ||||
|  * @param filePath Path of the zip file to process. | ||||
|  */ | ||||
| export async function processLearningObjectZip(filePath: string): Promise<LearningObject> { | ||||
|     const learningObjectRepo = getLearningObjectRepository(); | ||||
|     const attachmentRepo = getAttachmentRepository(); | ||||
| 
 | ||||
|     const zip = await unzipper.Open.file(filePath); | ||||
| 
 | ||||
|     let metadata: LearningObjectMetadata | null = null; | ||||
|     const attachments: {name: string, content: Buffer}[] = []; | ||||
|     let content: Buffer | null = null; | ||||
| 
 | ||||
|     for (const file of zip.files) { | ||||
|         if (file.type === "Directory") { | ||||
|             throw Error("The learning object zip file should not contain directories."); | ||||
|         } else if (file.path === "metadata.json") { | ||||
|             metadata = await processMetadataJson(file); | ||||
|         } else if (file.path.startsWith("index.")) { | ||||
|             content = await processFile(file); | ||||
|         } else { | ||||
|             attachments.push({ | ||||
|                 name: file.path, | ||||
|                 content: await processFile(file) | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     if (!metadata) { | ||||
|         throw Error("Missing metadata.json file"); | ||||
|     } | ||||
|     if (!content) { | ||||
|         throw Error("Missing index file"); | ||||
|     } | ||||
| 
 | ||||
|     const learningObject = learningObjectRepo.create(metadata); | ||||
|     const attachmentEntities = attachments.map(it => attachmentRepo.create({ | ||||
|         name: it.name, | ||||
|         content: it.content, | ||||
|         mimeType: mime.lookup(it.name) || "text/plain", | ||||
|         learningObject | ||||
|     })) | ||||
|     learningObject.attachments.push(...attachmentEntities); | ||||
| 
 | ||||
|     return learningObject; | ||||
| } | ||||
| 
 | ||||
| async function processMetadataJson(file: unzipper.File): LearningObjectMetadata { | ||||
|     const buf = await file.buffer(); | ||||
|     const content = buf.toString(); | ||||
|     return JSON.parse(content); | ||||
| } | ||||
| 
 | ||||
| async function processFile(file: unzipper.File): Promise<Buffer> { | ||||
|     return await file.buffer(); | ||||
| } | ||||
							
								
								
									
										103
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										103
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -36,6 +36,8 @@ | |||
|                 "@mikro-orm/postgresql": "6.4.12", | ||||
|                 "@mikro-orm/reflection": "6.4.12", | ||||
|                 "@mikro-orm/sqlite": "6.4.12", | ||||
|                 "@types/mime-types": "^2.1.4", | ||||
|                 "@types/unzipper": "^0.10.11", | ||||
|                 "axios": "^1.8.2", | ||||
|                 "cors": "^2.8.5", | ||||
|                 "cross": "^1.0.0", | ||||
|  | @ -50,8 +52,10 @@ | |||
|                 "jwks-rsa": "^3.1.0", | ||||
|                 "loki-logger-ts": "^1.0.2", | ||||
|                 "marked": "^15.0.7", | ||||
|                 "mime-types": "^3.0.1", | ||||
|                 "response-time": "^2.3.3", | ||||
|                 "swagger-ui-express": "^5.0.1", | ||||
|                 "unzipper": "^0.12.3", | ||||
|                 "uuid": "^11.1.0", | ||||
|                 "winston": "^3.17.0", | ||||
|                 "winston-loki": "^6.1.3" | ||||
|  | @ -1716,6 +1720,12 @@ | |||
|             "version": "1.3.5", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/mime-types": { | ||||
|             "version": "2.1.4", | ||||
|             "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", | ||||
|             "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/@types/ms": { | ||||
|             "version": "2.1.0", | ||||
|             "license": "MIT" | ||||
|  | @ -1784,6 +1794,15 @@ | |||
|             "license": "MIT", | ||||
|             "optional": true | ||||
|         }, | ||||
|         "node_modules/@types/unzipper": { | ||||
|             "version": "0.10.11", | ||||
|             "resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.11.tgz", | ||||
|             "integrity": "sha512-D25im2zjyMCcgL9ag6N46+wbtJBnXIr7SI4zHf9eJD2Dw2tEB5e+p5MYkrxKIVRscs5QV0EhtU9rgXSPx90oJg==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "@types/node": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@types/web-bluetooth": { | ||||
|             "version": "0.0.21", | ||||
|             "license": "MIT" | ||||
|  | @ -2711,6 +2730,12 @@ | |||
|                 "readable-stream": "^3.4.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/bluebird": { | ||||
|             "version": "3.7.2", | ||||
|             "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", | ||||
|             "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/body-parser": { | ||||
|             "version": "2.2.0", | ||||
|             "license": "MIT", | ||||
|  | @ -3259,6 +3284,12 @@ | |||
|                 "url": "https://github.com/sponsors/mesqueeb" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/core-util-is": { | ||||
|             "version": "1.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", | ||||
|             "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/cors": { | ||||
|             "version": "2.8.5", | ||||
|             "license": "MIT", | ||||
|  | @ -3515,6 +3546,45 @@ | |||
|                 "node": ">= 0.4" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/duplexer2": { | ||||
|             "version": "0.1.4", | ||||
|             "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", | ||||
|             "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", | ||||
|             "license": "BSD-3-Clause", | ||||
|             "dependencies": { | ||||
|                 "readable-stream": "^2.0.2" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/duplexer2/node_modules/readable-stream": { | ||||
|             "version": "2.3.8", | ||||
|             "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", | ||||
|             "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "core-util-is": "~1.0.0", | ||||
|                 "inherits": "~2.0.3", | ||||
|                 "isarray": "~1.0.0", | ||||
|                 "process-nextick-args": "~2.0.0", | ||||
|                 "safe-buffer": "~5.1.1", | ||||
|                 "string_decoder": "~1.1.1", | ||||
|                 "util-deprecate": "~1.0.1" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/duplexer2/node_modules/safe-buffer": { | ||||
|             "version": "5.1.2", | ||||
|             "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", | ||||
|             "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/duplexer2/node_modules/string_decoder": { | ||||
|             "version": "1.1.1", | ||||
|             "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", | ||||
|             "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "safe-buffer": "~5.1.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/dwengo-1-docs": { | ||||
|             "resolved": "docs", | ||||
|             "link": true | ||||
|  | @ -5053,6 +5123,12 @@ | |||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/isarray": { | ||||
|             "version": "1.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", | ||||
|             "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/isexe": { | ||||
|             "version": "2.0.0", | ||||
|             "license": "ISC" | ||||
|  | @ -5820,6 +5896,8 @@ | |||
|         }, | ||||
|         "node_modules/mime-types": { | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", | ||||
|             "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "mime-db": "^1.54.0" | ||||
|  | @ -6165,6 +6243,12 @@ | |||
|                 "node": ">=6" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/node-int64": { | ||||
|             "version": "0.4.0", | ||||
|             "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", | ||||
|             "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/node-releases": { | ||||
|             "version": "2.0.19", | ||||
|             "dev": true, | ||||
|  | @ -6843,6 +6927,12 @@ | |||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/process-nextick-args": { | ||||
|             "version": "2.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", | ||||
|             "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", | ||||
|             "license": "MIT" | ||||
|         }, | ||||
|         "node_modules/promise-inflight": { | ||||
|             "version": "1.0.1", | ||||
|             "license": "ISC", | ||||
|  | @ -8411,6 +8501,19 @@ | |||
|                 "node": ">= 0.8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/unzipper": { | ||||
|             "version": "0.12.3", | ||||
|             "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", | ||||
|             "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", | ||||
|             "license": "MIT", | ||||
|             "dependencies": { | ||||
|                 "bluebird": "~3.7.2", | ||||
|                 "duplexer2": "~0.1.4", | ||||
|                 "fs-extra": "^11.2.0", | ||||
|                 "graceful-fs": "^4.2.2", | ||||
|                 "node-int64": "^0.4.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/update-browserslist-db": { | ||||
|             "version": "1.1.3", | ||||
|             "dev": true, | ||||
|  |  | |||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger