Skip to content

Commit 339d0e5

Browse files
server download works now
1 parent 9e9906e commit 339d0e5

7 files changed

Lines changed: 331 additions & 26 deletions

File tree

dist/index.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/export/scorm2004.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,5 @@ export async function exporter(argument: Scorm2004ExportArguments, json: any) {
136136

137137
await scormPackager(config, function (msg: string) {
138138
console.log(msg)
139-
process.exit(0)
140139
})
141140
}

src/server/public/app.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -316,14 +316,20 @@ function initializeForm() {
316316
return
317317
}
318318

319-
// Add options
320-
const masteryScore = document.getElementById('masteryScore').value
321-
const pageSize = document.getElementById('pageSize').value
322-
const organization = document.getElementById('organization').value
323-
324-
if (masteryScore) formData.append('option_masteryScore', masteryScore)
325-
if (pageSize) formData.append('option_pageSize', pageSize)
326-
if (organization) formData.append('option_organization', organization)
319+
// Add all options from form elements with name starting with 'option_'
320+
const formElements = form.elements
321+
for (let i = 0; i < formElements.length; i++) {
322+
const element = formElements[i]
323+
if (element.name && element.name.startsWith('option_')) {
324+
if (element.type === 'checkbox') {
325+
if (element.checked) {
326+
formData.append(element.name, 'true')
327+
}
328+
} else if (element.value) {
329+
formData.append(element.name, element.value)
330+
}
331+
}
332+
}
327333

328334
// Submit to API
329335
const response = await fetch('/api/export', {

src/server/public/index.html

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,14 @@ <h3>EPUB</h3>
236236
<p>E-Book</p>
237237
</div>
238238
</label>
239+
240+
<label class="preset-tile">
241+
<input type="radio" name="format" value="rdf" />
242+
<div class="preset-content">
243+
<h3>RDF</h3>
244+
<p>Metadaten</p>
245+
</div>
246+
</label>
239247
</div>
240248
</div>
241249
</section>
@@ -711,6 +719,99 @@ <h3 class="settings-heading">JSON Einstellungen</h3>
711719
</div>
712720
</div>
713721

722+
<!-- RDF Settings -->
723+
<div class="settings-group" data-formats="rdf">
724+
<h3 class="settings-heading">RDF Einstellungen</h3>
725+
726+
<div class="form-group">
727+
<label for="rdfFormat">Ausgabeformat</label>
728+
<select id="rdfFormat" name="option_rdf-format">
729+
<option value="json-ld" selected>JSON-LD</option>
730+
<option value="n-quads">N-Quads</option>
731+
</select>
732+
<span class="hint"
733+
>Format für die RDF-Ausgabe (Standard: JSON-LD)</span
734+
>
735+
</div>
736+
737+
<div class="form-group">
738+
<label>
739+
<input
740+
type="checkbox"
741+
id="rdfPreview"
742+
name="option_rdf-preview"
743+
/>
744+
Preview in Konsole
745+
</label>
746+
<span class="hint">Ausgabe in der Konsole anzeigen</span>
747+
</div>
748+
749+
<div class="form-group">
750+
<label for="rdfUrl">Externe URL</label>
751+
<input
752+
type="url"
753+
id="rdfUrl"
754+
name="option_rdf-url"
755+
placeholder="https://example.com/course"
756+
/>
757+
<span class="hint"
758+
>Externe URL für lokale Projekte (optional)</span
759+
>
760+
</div>
761+
762+
<div class="form-group">
763+
<label for="rdfType">Schema.org Type</label>
764+
<input
765+
type="text"
766+
id="rdfType"
767+
name="option_rdf-type"
768+
placeholder="Course"
769+
/>
770+
<span class="hint"
771+
>Schema.org-Typ (Standard: Course, z.B.
772+
EducationalResource)</span
773+
>
774+
</div>
775+
776+
<div class="form-group">
777+
<label for="rdfTemplate">Template URL/Datei</label>
778+
<input
779+
type="text"
780+
id="rdfTemplate"
781+
name="option_rdf-template"
782+
placeholder="template.json"
783+
/>
784+
<span class="hint"
785+
>URL oder JSON-Datei als Template (optional)</span
786+
>
787+
</div>
788+
789+
<div class="form-group">
790+
<label for="rdfLicense">Lizenz-URL</label>
791+
<input
792+
type="url"
793+
id="rdfLicense"
794+
name="option_rdf-license"
795+
placeholder="https://creativecommons.org/licenses/by/4.0/"
796+
/>
797+
<span class="hint">URL zur Lizenz des Kurses (optional)</span>
798+
</div>
799+
800+
<div class="form-group">
801+
<label for="rdfEducationalLevel">Bildungsniveau</label>
802+
<input
803+
type="text"
804+
id="rdfEducationalLevel"
805+
name="option_rdf-educationalLevel"
806+
placeholder="beginner, intermediate, advanced"
807+
/>
808+
<span class="hint"
809+
>Bildungsniveau (z.B. beginner, intermediate,
810+
advanced)</span
811+
>
812+
</div>
813+
</div>
814+
714815
<!-- No settings message -->
715816
<div class="settings-group no-settings" style="display: none">
716817
<p class="hint">

src/server/public/status.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,7 @@ <h2>Job-ID: ${job.id}</h2>
215215
}
216216

217217
function downloadResult() {
218-
alert('Download-Funktionalität würde hier implementiert werden')
219-
// In production: window.location.href = `/api/download/${jobId}`;
218+
window.location.href = `/api/download/${jobId}`
220219
}
221220
</script>
222221
</body>

src/server/queue/jobQueue.ts

Lines changed: 177 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { EventEmitter } from 'events'
22
import { randomUUID } from 'crypto'
3+
import { spawn } from 'child_process'
4+
import * as path from 'path'
5+
import * as fs from 'fs-extra'
6+
import { tmpdir } from 'os'
37

48
export interface ExportJob {
59
id: string
@@ -26,7 +30,9 @@ export interface ExportJob {
2630
export class JobQueue extends EventEmitter {
2731
private queue: ExportJob[] = []
2832
private currentJob: ExportJob | null = null
33+
private completedJobs: Map<string, ExportJob> = new Map()
2934
private isProcessing = false
35+
private maxCompletedJobs = 100 // Keep last 100 completed jobs
3036

3137
addJob(jobData: Omit<ExportJob, 'id' | 'status' | 'createdAt'>): {
3238
jobId: string
@@ -59,7 +65,11 @@ export class JobQueue extends EventEmitter {
5965
if (this.currentJob?.id === jobId) {
6066
return this.currentJob
6167
}
62-
return this.queue.find((job) => job.id === jobId)
68+
const queuedJob = this.queue.find((job) => job.id === jobId)
69+
if (queuedJob) {
70+
return queuedJob
71+
}
72+
return this.completedJobs.get(jobId)
6373
}
6474

6575
getQueuePosition(jobId: string): number {
@@ -95,6 +105,17 @@ export class JobQueue extends EventEmitter {
95105
this.currentJob.completedAt = new Date()
96106
this.emit('job-failed', this.currentJob)
97107
} finally {
108+
// Store completed job
109+
if (this.currentJob) {
110+
this.completedJobs.set(this.currentJob.id, this.currentJob)
111+
112+
// Cleanup old completed jobs if limit exceeded
113+
if (this.completedJobs.size > this.maxCompletedJobs) {
114+
const firstKey = this.completedJobs.keys().next().value
115+
this.completedJobs.delete(firstKey)
116+
}
117+
}
118+
98119
this.currentJob = null
99120
this.isProcessing = false
100121

@@ -106,17 +127,163 @@ export class JobQueue extends EventEmitter {
106127
}
107128

108129
private async performExport(job: ExportJob): Promise<void> {
109-
// This is where the actual export logic would go
110-
// For now, simulate processing time
111-
return new Promise((resolve) => {
112-
setTimeout(() => {
130+
return new Promise(async (resolve, reject) => {
131+
try {
132+
// Determine input file
133+
let inputFile: string
134+
if (
135+
job.source.type === 'upload' &&
136+
job.source.files &&
137+
job.source.files.length > 0
138+
) {
139+
// Use the first file (README.md or similar)
140+
const readmeFile = job.source.files.find(
141+
(f) =>
142+
f.filename === 'README.md' ||
143+
f.filename.toLowerCase().endsWith('.md')
144+
)
145+
inputFile = readmeFile ? readmeFile.path : job.source.files[0].path
146+
} else if (job.source.type === 'git' && job.source.gitUrl) {
147+
// For git repos, we'd need to clone first - not implemented yet
148+
throw new Error('Git repository export not yet implemented')
149+
} else {
150+
throw new Error('No valid input source')
151+
}
152+
153+
// Determine format based on preset or format
154+
let format: string
155+
if (job.target.preset) {
156+
// Map presets to formats
157+
const presetMap: Record<string, string> = {
158+
moodle: 'scorm2004',
159+
ilias: 'scorm2004',
160+
opal: 'scorm2004',
161+
generic: 'scorm2004',
162+
openolat: 'scorm2004',
163+
openedx: 'scorm2004',
164+
}
165+
format = presetMap[job.target.preset] || 'scorm2004'
166+
} else {
167+
format = job.target.format || 'web'
168+
}
169+
170+
// Create output directory
171+
const outputDir = path.join(tmpdir(), 'liaex-exports', job.id)
172+
await fs.ensureDir(outputDir)
173+
174+
const outputFile = path.join(outputDir, 'export')
175+
176+
// Convert options to proper types and build CLI arguments
177+
const args: string[] = [
178+
'--input',
179+
inputFile,
180+
'--format',
181+
format,
182+
'--output',
183+
outputFile,
184+
]
185+
186+
for (const [key, value] of Object.entries(job.options || {})) {
187+
// Convert option keys back to CLI format (e.g., 'scorm-masteryScore' -> '--scorm-masteryScore')
188+
const cliKey = `--${key.replace(/_/g, '-')}`
189+
190+
// Convert string booleans to actual booleans
191+
if (value === 'true') {
192+
args.push(cliKey)
193+
} else if (value === 'false') {
194+
// Don't add false flags
195+
} else if (value) {
196+
args.push(cliKey, String(value))
197+
}
198+
}
199+
200+
// Run export in separate process using the CLI
201+
// Use the current process's argv[1] which is the path to dist/index.js
202+
const cliPath = process.argv[1]
113203
console.log(
114-
`Exporting job ${job.id} with target ${
115-
job.target.preset || job.target.format
116-
}`
204+
`Starting export process for job ${
205+
job.id
206+
}: node ${cliPath} ${args.join(' ')}`
117207
)
118-
resolve()
119-
}, 5000) // 5 second delay to simulate export
208+
209+
const exportProcess = spawn('node', [cliPath, ...args], {
210+
stdio: ['ignore', 'pipe', 'pipe'],
211+
detached: false,
212+
})
213+
214+
let stdout = ''
215+
let stderr = ''
216+
217+
exportProcess.stdout?.on('data', (data) => {
218+
stdout += data.toString()
219+
console.log(`[Job ${job.id}] ${data.toString().trim()}`)
220+
})
221+
222+
exportProcess.stderr?.on('data', (data) => {
223+
stderr += data.toString()
224+
console.error(`[Job ${job.id}] ${data.toString().trim()}`)
225+
})
226+
227+
exportProcess.on('error', (error) => {
228+
console.error(`Export process error for job ${job.id}:`, error)
229+
reject(error)
230+
})
231+
232+
exportProcess.on('close', async (code) => {
233+
if (code !== 0) {
234+
reject(
235+
new Error(
236+
`Export process exited with code ${code}. stderr: ${stderr}`
237+
)
238+
)
239+
return
240+
}
241+
242+
try {
243+
// Give the export process a moment to finish writing files
244+
await new Promise((resolveTimeout) =>
245+
setTimeout(resolveTimeout, 1000)
246+
)
247+
248+
// Find the generated output file
249+
const files = await fs.readdir(outputDir)
250+
console.log(`Files in output directory: ${files.join(', ')}`)
251+
252+
const outputFileName = files.find(
253+
(f) =>
254+
f.startsWith('export') &&
255+
(f.endsWith('.zip') ||
256+
f.endsWith('.html') ||
257+
f.endsWith('.pdf') ||
258+
f.endsWith('.epub'))
259+
)
260+
261+
if (!outputFileName) {
262+
throw new Error(
263+
`Export completed but no output file was generated. Found files: ${files.join(
264+
', '
265+
)}`
266+
)
267+
}
268+
269+
const outputPath = path.join(outputDir, outputFileName)
270+
271+
// Store result
272+
job.result = {
273+
outputPath: outputPath,
274+
filename: outputFileName,
275+
}
276+
277+
console.log(`Export completed: ${job.id} -> ${outputPath}`)
278+
resolve()
279+
} catch (error) {
280+
reject(error)
281+
}
282+
})
283+
} catch (error) {
284+
console.error(`Export failed for job ${job.id}:`, error)
285+
reject(error)
286+
}
120287
})
121288
}
122289

0 commit comments

Comments
 (0)