Replace docx text extraction with file attachment upload
Instead of parsing .docx content via mammoth and inserting raw text/HTML, the import flow now saves the file to data/attachments/ (UUID filename) and inserts an <a> link into the item body so students can open/download the doc. - Added GET /api/attachments/:filename (no PIN — students access directly) - Added POST /api/admin/upload-attachment (PIN required) - Removed mammoth dependency from server - Updated admin.js import handler to POST to new endpoint and set link HTML Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
6b72031672
commit
5de4cfa6cf
2 files changed
+33
-32
No files matched your search
+10
-13
@@ -607,7 +607,7 @@ $('#import-go-btn').addEventListener('click', async () => {
|
||||
if (!file) return;
|
||||
const type = $('#ie-type').value;
|
||||
$('#import-go-btn').disabled = true;
|
||||
$('#import-status').textContent = '📄 Extracting text…';
|
||||
$('#import-status').textContent = '📤 Uploading…';
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const bytes = new Uint8Array(arrayBuffer);
|
||||
@@ -616,20 +616,17 @@ $('#import-go-btn').addEventListener('click', async () => {
|
||||
binary += String.fromCharCode(...bytes.subarray(i, Math.min(i + 8192, bytes.length)));
|
||||
}
|
||||
const base64 = btoa(binary);
|
||||
const result = await api('/api/admin/import-docx', { method: 'POST', body: { base64 } });
|
||||
if (!$('#ie-title').value && result.title) $('#ie-title').value = result.title;
|
||||
if (result.body) {
|
||||
if (type === 'assignment') {
|
||||
const tmp = document.createElement('div');
|
||||
tmp.innerHTML = result.body;
|
||||
$('#ie-body-assignment').value = tmp.textContent || tmp.innerText || '';
|
||||
} else if (type === 'lesson') {
|
||||
$('#ie-body-lesson').innerHTML = result.body;
|
||||
}
|
||||
$('#import-status').textContent = '✅ Text extracted. Review and edit before saving.';
|
||||
const result = await api('/api/admin/upload-attachment', {
|
||||
method: 'POST',
|
||||
body: { base64, originalName: file.name },
|
||||
});
|
||||
const link = `<a href="${result.url}" target="_blank">📄 ${result.originalName}</a>`;
|
||||
if (type === 'assignment') {
|
||||
$('#ie-body-assignment').value = link;
|
||||
} else {
|
||||
$('#import-status').textContent = '⚠️ File imported but no content was found.';
|
||||
$('#ie-body-lesson').innerHTML = link;
|
||||
}
|
||||
$('#import-status').textContent = '✅ File uploaded. Link inserted into body.';
|
||||
} catch (err) {
|
||||
$('#import-status').textContent = `❌ ${err.message}`;
|
||||
} finally {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import express from 'express';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { join, dirname, extname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { createHash } from 'node:crypto';
|
||||
import mammoth from 'mammoth';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { db, sha256 } from './db.js';
|
||||
|
||||
const ROOT = dirname(fileURLToPath(import.meta.url));
|
||||
const DATA_DIR = process.env.DATA_DIR || join(ROOT, 'data');
|
||||
const TTS_CACHE = join(DATA_DIR, 'tts-cache');
|
||||
mkdirSync(TTS_CACHE, { recursive: true });
|
||||
const ATTACHMENTS_DIR = join(DATA_DIR, 'attachments');
|
||||
mkdirSync(ATTACHMENTS_DIR, { recursive: true });
|
||||
const PIPER_BIN = process.env.PIPER_BIN || 'piper';
|
||||
const PIPER_MODEL = process.env.PIPER_MODEL || join(ROOT, 'voices', 'en_US-lessac-medium.onnx');
|
||||
const PIPER_OK = existsSync(PIPER_MODEL);
|
||||
@@ -1519,30 +1520,33 @@ app.get('/api/admin/homeschool-files', requirePin, (req, res) => {
|
||||
});
|
||||
|
||||
|
||||
app.post('/api/admin/import-docx', requirePin, async (req, res) => {
|
||||
const { base64: b64 } = req.body;
|
||||
if (!b64) return res.status(400).json({ error: 'base64 required' });
|
||||
// Public: students need to open attachments without a PIN
|
||||
app.get('/api/attachments/:filename', (req, res) => {
|
||||
const filename = req.params.filename;
|
||||
// Only allow safe filenames (UUID + extension)
|
||||
if (!/^[\w-]+\.\w+$/.test(filename)) return res.status(400).json({ error: 'Invalid filename' });
|
||||
const filePath = join(ATTACHMENTS_DIR, filename);
|
||||
if (!existsSync(filePath)) return res.status(404).json({ error: 'Not found' });
|
||||
res.sendFile(filePath);
|
||||
});
|
||||
|
||||
app.post('/api/admin/upload-attachment', requirePin, async (req, res) => {
|
||||
const { base64: b64, originalName } = req.body;
|
||||
if (!b64 || !originalName) return res.status(400).json({ error: 'base64 and originalName required' });
|
||||
|
||||
let buffer;
|
||||
try { buffer = Buffer.from(b64, 'base64'); }
|
||||
catch (err) { return res.status(400).json({ error: 'Invalid base64' }); }
|
||||
|
||||
let html, rawText;
|
||||
const ext = extname(originalName).toLowerCase() || '.docx';
|
||||
const filename = `${randomUUID()}${ext}`;
|
||||
try {
|
||||
const [htmlResult, textResult] = await Promise.all([
|
||||
mammoth.convertToHtml({ buffer }),
|
||||
mammoth.extractRawText({ buffer }),
|
||||
]);
|
||||
html = htmlResult.value.trim();
|
||||
rawText = textResult.value.trim();
|
||||
await writeFile(join(ATTACHMENTS_DIR, filename), buffer);
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: `Could not parse docx: ${err.message}` });
|
||||
return res.status(500).json({ error: `Could not save file: ${err.message}` });
|
||||
}
|
||||
if (!rawText) return res.status(400).json({ error: 'Document appears to be empty' });
|
||||
|
||||
// Use first non-empty line of raw text as the title
|
||||
const firstLine = rawText.split('\n').map(l => l.trim()).find(Boolean) || '';
|
||||
return res.json({ title: firstLine, body: html });
|
||||
return res.json({ url: `/api/attachments/${filename}`, originalName });
|
||||
});
|
||||
|
||||
app.get('/health', (req, res) => res.json({ ok: true }));
|
||||
|
||||
Reference in new issue
Block a user