From a711bd19737507a035f97bc2591a1b35d6cf1025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tadeu=20Tupinamb=C3=A1=2E?= Date: Fri, 12 Dec 2025 10:25:07 -0300 Subject: [PATCH] feat: add legacy .doc file conversion support Add support for converting legacy .doc files to .docx format using a conversion server powered by LibreOffice. Changes: - Add DOC type constant to document-types.ts - Create documentConverter.js helper module for conversion logic - Update SuperDoc.js with automatic .doc detection and conversion - Add conversion events (onConversionStart, onConversionComplete, onConversionError) - Add modules.conversion config option for server URL - Update file.js to detect .doc files - Update BasicUpload.vue to accept .doc files - Add conversion-server example with Docker support Usage: ```javascript const superdoc = new SuperDoc({ document: docFile, modules: { conversion: { serverUrl: 'http://localhost:3001', }, }, }); ``` Closes #1019 --- examples/conversion-server/.dockerignore | 9 + examples/conversion-server/.gitignore | 3 + examples/conversion-server/Dockerfile | 46 ++++ examples/conversion-server/README.md | 233 ++++++++++++++++ examples/conversion-server/docker-compose.yml | 28 ++ examples/conversion-server/package.json | 16 ++ examples/conversion-server/server.js | 259 ++++++++++++++++++ packages/superdoc/src/core/SuperDoc.js | 88 +++++- .../src/core/helpers/documentConverter.js | 161 +++++++++++ packages/superdoc/src/core/helpers/file.js | 3 +- .../src/dev/components/SuperdocDev.vue | 190 ++++++++++++- shared/common/components/BasicUpload.vue | 2 +- shared/common/document-types.ts | 4 +- 13 files changed, 1025 insertions(+), 17 deletions(-) create mode 100644 examples/conversion-server/.dockerignore create mode 100644 examples/conversion-server/.gitignore create mode 100644 examples/conversion-server/Dockerfile create mode 100644 examples/conversion-server/README.md create mode 100644 examples/conversion-server/docker-compose.yml create mode 100644 examples/conversion-server/package.json create mode 100644 examples/conversion-server/server.js create mode 100644 packages/superdoc/src/core/helpers/documentConverter.js diff --git a/examples/conversion-server/.dockerignore b/examples/conversion-server/.dockerignore new file mode 100644 index 000000000..481a67fad --- /dev/null +++ b/examples/conversion-server/.dockerignore @@ -0,0 +1,9 @@ +node_modules +npm-debug.log +.git +.gitignore +.DS_Store +README.md +*.log +.env +.env.* diff --git a/examples/conversion-server/.gitignore b/examples/conversion-server/.gitignore new file mode 100644 index 000000000..aafcb3456 --- /dev/null +++ b/examples/conversion-server/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.DS_Store +*.log diff --git a/examples/conversion-server/Dockerfile b/examples/conversion-server/Dockerfile new file mode 100644 index 000000000..71c642837 --- /dev/null +++ b/examples/conversion-server/Dockerfile @@ -0,0 +1,46 @@ +# SuperDoc Conversion Server +# Includes LibreOffice for .doc to .docx conversion + +FROM node:20-slim + +# Install LibreOffice and required dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + libreoffice \ + libreoffice-writer \ + fonts-liberation \ + fonts-dejavu \ + fonts-freefont-ttf \ + fontconfig \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && fc-cache -f -v + +# Create app directory +WORKDIR /app + +# Copy package files +COPY package.json ./ + +# Install dependencies +RUN npm install --omit=dev + +# Copy server code +COPY server.js ./ + +# Create temp directory for conversions +RUN mkdir -p /tmp/superdoc-conversions + +# Expose port +EXPOSE 3001 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD node -e "fetch('http://localhost:3001/health').then(r => process.exit(r.ok ? 0 : 1))" || exit 1 + +# Run as non-root user for security +RUN useradd -m -s /bin/bash appuser && \ + chown -R appuser:appuser /app /tmp/superdoc-conversions +USER appuser + +# Start server +CMD ["node", "server.js"] diff --git a/examples/conversion-server/README.md b/examples/conversion-server/README.md new file mode 100644 index 000000000..71aea30e8 --- /dev/null +++ b/examples/conversion-server/README.md @@ -0,0 +1,233 @@ +# SuperDoc Conversion Server + +A simple Node.js server for converting legacy `.doc` files to `.docx` format using LibreOffice. + +## Quick Start with Docker (Recommended) + +The easiest way to run the conversion server is with Docker - no need to install LibreOffice manually. + +### Using Docker Compose + +```bash +cd conversion-server +docker-compose up -d +``` + +### Using Docker directly + +```bash +cd conversion-server + +# Build the image +docker build -t superdoc-conversion-server . + +# Run the container +docker run -d -p 3001:3001 --name superdoc-conversion superdoc-conversion-server +``` + +The server will be available at `http://localhost:3001`. + +### Docker Commands + +```bash +# View logs +docker-compose logs -f + +# Stop the server +docker-compose down + +# Rebuild after changes +docker-compose up -d --build +``` + +--- + +## Manual Setup (Without Docker) + +If you prefer to run without Docker, you'll need to install LibreOffice manually. + +## Prerequisites + +### LibreOffice Installation + +This server requires LibreOffice to be installed on your system. + +#### macOS + +**Option 1: Direct Download** +- Download from: https://www.libreoffice.org/download/download/ + +**Option 2: Homebrew** +```bash +brew install --cask libreoffice +``` + +#### Linux + +**Ubuntu/Debian:** +```bash +sudo apt update +sudo apt install libreoffice +``` + +**Fedora:** +```bash +sudo dnf install libreoffice +``` + +**Arch Linux:** +```bash +sudo pacman -S libreoffice-fresh +``` + +#### Windows + +Download from: https://www.libreoffice.org/download/download/ + +## Setup + +1. Navigate to the conversion-server folder: + ```bash + cd conversion-server + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Start the server: + ```bash + npm start + ``` + + Or for development with auto-reload: + ```bash + npm run dev + ``` + +The server will start on `http://localhost:3001`. + +## API Endpoints + +### `GET /health` +Health check endpoint. + +**Response:** +```json +{ + "status": "ok", + "message": "Conversion server is running" +} +``` + +### `GET /check-libreoffice` +Check if LibreOffice is properly installed. + +**Response (success):** +```json +{ + "status": "ok", + "message": "LibreOffice is installed", + "path": "/Applications/LibreOffice.app/Contents/MacOS/soffice" +} +``` + +**Response (error):** +```json +{ + "status": "error", + "message": "LibreOffice not found. Please install it.", + "instructions": { + "os": "macOS", + "methods": [ + "Download from: https://www.libreoffice.org/download/download/", + "Or via Homebrew: brew install --cask libreoffice" + ] + } +} +``` + +### `POST /convert` +Convert a `.doc` file to `.docx` format. + +**Request:** +- Content-Type: `multipart/form-data` +- Body: `file` - The `.doc` file to convert + +**Response:** +- Content-Type: `application/vnd.openxmlformats-officedocument.wordprocessingml.document` +- Body: The converted `.docx` file + +**Example using curl:** +```bash +curl -X POST -F "file=@document.doc" http://localhost:3001/convert -o document.docx +``` + +## Usage with SuperDoc Dev + +1. Start the conversion server: + ```bash + cd conversion-server + npm start + ``` + +2. In another terminal, start SuperDoc dev: + ```bash + npm run dev + ``` + +3. Open http://localhost:9094 in your browser + +4. Upload a `.doc` file - a dialog will appear offering to convert it + +5. Click "Convert & Edit" to convert and load the document + +## Configuration + +### Port + +Set the `PORT` environment variable to change the server port: +```bash +PORT=3002 npm start +``` + +### CORS + +By default, the server allows requests from: +- `http://localhost:9094` +- `http://localhost:9096` +- `http://127.0.0.1:9094` + +To modify allowed origins, edit the `cors` configuration in `server.js`. + +## Troubleshooting + +### "LibreOffice not found" + +1. Verify LibreOffice is installed: + ```bash + # macOS + ls /Applications/LibreOffice.app + + # Linux + which soffice + ``` + +2. If installed in a non-standard location, add the path to the `possiblePaths` array in `server.js`. + +### "Conversion failed" + +1. Check the server console for error details +2. Ensure the `.doc` file is not corrupted +3. Try converting the file manually with LibreOffice to verify it works + +### CORS errors + +If you're running the dev server on a different port, add it to the CORS `origin` array in `server.js`. + +## Security Notes + +- This server is designed for local development +- For production use, add authentication and rate limiting +- Files are temporarily stored in the system temp directory and deleted after conversion diff --git a/examples/conversion-server/docker-compose.yml b/examples/conversion-server/docker-compose.yml new file mode 100644 index 000000000..c79c11ee2 --- /dev/null +++ b/examples/conversion-server/docker-compose.yml @@ -0,0 +1,28 @@ +version: '3.8' + +services: + conversion-server: + build: + context: . + dockerfile: Dockerfile + container_name: superdoc-conversion-server + ports: + - "3001:3001" + environment: + - PORT=3001 + - NODE_ENV=production + restart: unless-stopped + # Resource limits + deploy: + resources: + limits: + memory: 2G + reservations: + memory: 512M + # Health check + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://localhost:3001/health').then(r => process.exit(r.ok ? 0 : 1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s diff --git a/examples/conversion-server/package.json b/examples/conversion-server/package.json new file mode 100644 index 000000000..3f0101e1c --- /dev/null +++ b/examples/conversion-server/package.json @@ -0,0 +1,16 @@ +{ + "name": "superdoc-conversion-server", + "version": "1.0.0", + "description": "Server for converting legacy .doc files to .docx format", + "main": "server.js", + "type": "module", + "scripts": { + "start": "node server.js", + "dev": "node --watch server.js" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "multer": "^1.4.5-lts.1" + } +} diff --git a/examples/conversion-server/server.js b/examples/conversion-server/server.js new file mode 100644 index 000000000..f48eb317a --- /dev/null +++ b/examples/conversion-server/server.js @@ -0,0 +1,259 @@ +import express from 'express'; +import cors from 'cors'; +import multer from 'multer'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import fs from 'fs/promises'; +import path from 'path'; +import os from 'os'; +import crypto from 'crypto'; + +const execAsync = promisify(exec); + +const app = express(); +const PORT = process.env.PORT || 3001; + +// Configure CORS for local development +app.use(cors({ + origin: ['http://localhost:9094', 'http://localhost:9096', 'http://127.0.0.1:9094'], + methods: ['POST', 'GET', 'OPTIONS'], + allowedHeaders: ['Content-Type'], +})); + +// Configure multer for file uploads +const storage = multer.diskStorage({ + destination: async (req, file, cb) => { + const tempDir = path.join(os.tmpdir(), 'superdoc-conversions'); + await fs.mkdir(tempDir, { recursive: true }); + cb(null, tempDir); + }, + filename: (req, file, cb) => { + // Generate unique filename to avoid collisions + const uniqueId = crypto.randomBytes(8).toString('hex'); + const ext = path.extname(file.originalname); + cb(null, `${uniqueId}${ext}`); + }, +}); + +const upload = multer({ + storage, + limits: { + fileSize: 50 * 1024 * 1024, // 50MB limit + }, + fileFilter: (req, file, cb) => { + const allowedExtensions = ['.doc', '.DOC']; + const ext = path.extname(file.originalname); + if (allowedExtensions.includes(ext)) { + cb(null, true); + } else { + cb(new Error('Only .doc files are allowed')); + } + }, +}); + +// Health check endpoint +app.get('/health', (req, res) => { + res.json({ status: 'ok', message: 'Conversion server is running' }); +}); + +// Check LibreOffice installation +app.get('/check-libreoffice', async (req, res) => { + try { + const libreOfficePath = await findLibreOffice(); + res.json({ + status: 'ok', + message: 'LibreOffice is installed', + path: libreOfficePath + }); + } catch (error) { + res.status(500).json({ + status: 'error', + message: 'LibreOffice not found. Please install it.', + instructions: getInstallInstructions() + }); + } +}); + +// Main conversion endpoint +app.post('/convert', upload.single('file'), async (req, res) => { + if (!req.file) { + return res.status(400).json({ error: 'No file uploaded' }); + } + + const inputPath = req.file.path; + const outputDir = path.dirname(inputPath); + const baseName = path.basename(inputPath, path.extname(inputPath)); + const expectedOutputPath = path.join(outputDir, `${baseName}.docx`); + + try { + // Find LibreOffice + const libreOfficePath = await findLibreOffice(); + + // Convert using LibreOffice + console.log(`Converting: ${inputPath}`); + const command = `"${libreOfficePath}" --headless --convert-to docx --outdir "${outputDir}" "${inputPath}"`; + + await execAsync(command, { timeout: 60000 }); // 60 second timeout + + // Check if output file exists + try { + await fs.access(expectedOutputPath); + } catch { + throw new Error('Conversion completed but output file not found'); + } + + // Read the converted file + const convertedFile = await fs.readFile(expectedOutputPath); + + // Set response headers + const originalName = req.file.originalname.replace(/\.doc$/i, '.docx'); + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); + res.setHeader('Content-Disposition', `attachment; filename="${originalName}"`); + res.setHeader('Content-Length', convertedFile.length); + + // Send the file + res.send(convertedFile); + + // Cleanup files + await cleanupFiles([inputPath, expectedOutputPath]); + + } catch (error) { + console.error('Conversion error:', error); + + // Cleanup input file on error + await cleanupFiles([inputPath, expectedOutputPath]).catch(() => {}); + + if (error.message.includes('not found') || error.message.includes('ENOENT')) { + return res.status(500).json({ + error: 'LibreOffice not found', + instructions: getInstallInstructions() + }); + } + + res.status(500).json({ + error: 'Conversion failed', + details: error.message + }); + } +}); + +// Find LibreOffice installation +async function findLibreOffice() { + const possiblePaths = [ + // macOS + '/Applications/LibreOffice.app/Contents/MacOS/soffice', + '/opt/homebrew/bin/soffice', + '/usr/local/bin/soffice', + // Linux + '/usr/bin/soffice', + '/usr/bin/libreoffice', + '/usr/lib/libreoffice/program/soffice', + '/snap/bin/libreoffice', + // Windows (WSL) + '/mnt/c/Program Files/LibreOffice/program/soffice.exe', + ]; + + for (const p of possiblePaths) { + try { + await fs.access(p, fs.constants.X_OK); + return p; + } catch { + // Continue to next path + } + } + + // Try to find via command line + try { + const { stdout } = await execAsync('which soffice'); + const path = stdout.trim(); + if (path) return path; + } catch { + // Not found via which + } + + throw new Error('LibreOffice not found'); +} + +// Get installation instructions based on OS +function getInstallInstructions() { + const platform = os.platform(); + + if (platform === 'darwin') { + return { + os: 'macOS', + methods: [ + 'Download from: https://www.libreoffice.org/download/download/', + 'Or via Homebrew: brew install --cask libreoffice' + ] + }; + } else if (platform === 'linux') { + return { + os: 'Linux', + methods: [ + 'Ubuntu/Debian: sudo apt install libreoffice', + 'Fedora: sudo dnf install libreoffice', + 'Arch: sudo pacman -S libreoffice-fresh' + ] + }; + } else { + return { + os: 'Windows', + methods: [ + 'Download from: https://www.libreoffice.org/download/download/' + ] + }; + } +} + +// Cleanup temporary files +async function cleanupFiles(files) { + for (const file of files) { + try { + await fs.unlink(file); + } catch { + // Ignore cleanup errors + } + } +} + +// Error handling middleware +app.use((err, req, res, next) => { + console.error('Server error:', err); + + if (err instanceof multer.MulterError) { + if (err.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json({ error: 'File too large. Maximum size is 50MB.' }); + } + return res.status(400).json({ error: err.message }); + } + + res.status(500).json({ error: err.message || 'Internal server error' }); +}); + +// Start server +app.listen(PORT, () => { + console.log(` + ======================================== + SuperDoc Conversion Server + ======================================== + + Server running at: http://localhost:${PORT} + + Endpoints: + GET /health - Health check + GET /check-libreoffice - Check LibreOffice installation + POST /convert - Convert .doc to .docx + + Make sure LibreOffice is installed! + ======================================== + `); + + // Check LibreOffice on startup + findLibreOffice() + .then(path => console.log(` LibreOffice found at: ${path}\n`)) + .catch(() => { + console.log(` WARNING: LibreOffice not found!`); + console.log(` Install instructions:`, getInstallInstructions()); + console.log(''); + }); +}); diff --git a/packages/superdoc/src/core/SuperDoc.js b/packages/superdoc/src/core/SuperDoc.js index c6abffa09..cd08c8bf8 100644 --- a/packages/superdoc/src/core/SuperDoc.js +++ b/packages/superdoc/src/core/SuperDoc.js @@ -4,7 +4,8 @@ import { EventEmitter } from 'eventemitter3'; import { v4 as uuidv4 } from 'uuid'; import { HocuspocusProviderWebsocket } from '@hocuspocus/provider'; -import { DOCX, PDF, HTML } from '@superdoc/common'; +import { DOCX, DOC, PDF, HTML } from '@superdoc/common'; +import { isDocFile, convertDocToDocx } from './helpers/documentConverter.js'; import { SuperToolbar, createZip } from '@harbour-enterprises/super-editor'; import { SuperComments } from '../components/CommentsLayer/commentsList/super-comments-list.js'; import { createSuperdocVueApp } from './create-app.js'; @@ -38,7 +39,7 @@ const DEFAULT_USER = Object.freeze({ */ export class SuperDoc extends EventEmitter { /** @type {Array} */ - static allowedTypes = [DOCX, PDF, HTML]; + static allowedTypes = [DOCX, DOC, PDF, HTML]; /** @type {string} */ version; @@ -103,6 +104,12 @@ export class SuperDoc extends EventEmitter { onListDefinitionsChange: () => null, onTransaction: () => null, onFontsResolved: null, + + // Conversion events (for .doc to .docx conversion) + onConversionStart: () => null, + onConversionComplete: () => null, + onConversionError: () => null, + // Image upload handler // async (file) => url; handleImageUpload: null, @@ -171,8 +178,8 @@ export class SuperDoc extends EventEmitter { this.superdocId = config.superdocId || uuidv4(); this.colors = this.config.colors; - // Preprocess document - this.#initDocuments(); + // Preprocess document (may include .doc to .docx conversion) + await this.#initDocuments(); // Initialize collaboration if configured await this.#initCollaboration(this.config.modules); @@ -247,7 +254,7 @@ export class SuperDoc extends EventEmitter { }; } - #initDocuments() { + async #initDocuments() { const doc = this.config.document; const hasDocumentConfig = !!doc && typeof doc === 'object' && Object.keys(this.config.document)?.length; const hasDocumentUrl = !!doc && typeof doc === 'string' && doc.length > 0; @@ -314,6 +321,77 @@ export class SuperDoc extends EventEmitter { }; }); } + + // Convert any .doc files to .docx if conversion is configured + await this.#convertDocFiles(); + } + + /** + * Convert .doc files to .docx using the configured conversion server + * @private + */ + async #convertDocFiles() { + const conversionConfig = this.config.modules?.conversion; + if (!conversionConfig?.serverUrl) { + // No conversion server configured - check if any .doc files exist + const hasDocFiles = this.config.documents?.some((doc) => doc.data && isDocFile(doc.data)); + if (hasDocFiles) { + this.#log( + '🦋 [superdoc] .doc files detected but no conversion server configured. Set modules.conversion.serverUrl to enable conversion.', + ); + } + return; + } + + if (!Array.isArray(this.config.documents) || this.config.documents.length === 0) { + return; + } + + // Process each document and convert .doc files + const convertedDocuments = await Promise.all( + this.config.documents.map(async (doc) => { + if (!doc.data || !isDocFile(doc.data)) { + return doc; + } + + const fileName = doc.name || doc.data.name || 'document.doc'; + this.#log(`🦋 [superdoc] Converting .doc file: ${fileName}`); + + // Emit conversion start event + this.config.onConversionStart?.({ fileName, document: doc }); + this.emit('conversionStart', { fileName, document: doc }); + + const result = await convertDocToDocx(doc.data, conversionConfig); + + if (result.success && result.file) { + // Emit conversion complete event + this.config.onConversionComplete?.({ fileName, document: doc, convertedFile: result.file }); + this.emit('conversionComplete', { fileName, document: doc, convertedFile: result.file }); + + // Return updated document with converted file + return { + ...doc, + data: result.file, + name: result.file.name, + type: DOCX, + originalName: fileName, + wasConverted: true, + }; + } else { + // Emit conversion error event + const error = result.error || new Error('Unknown conversion error'); + this.config.onConversionError?.({ fileName, document: doc, error }); + this.emit('conversionError', { fileName, document: doc, error }); + + console.error(`🦋 [superdoc] Failed to convert ${fileName}:`, error.message); + + // Return original document - it will fail to load but we don't want to block other documents + return doc; + } + }), + ); + + this.config.documents = convertedDocuments; } #initVueApp() { diff --git a/packages/superdoc/src/core/helpers/documentConverter.js b/packages/superdoc/src/core/helpers/documentConverter.js new file mode 100644 index 000000000..d03fc01db --- /dev/null +++ b/packages/superdoc/src/core/helpers/documentConverter.js @@ -0,0 +1,161 @@ +/* global FormData, AbortController, AbortSignal */ +import { DOC, DOCX } from '@superdoc/common'; + +/** + * @typedef {Object} ConversionConfig + * @property {string} [serverUrl] - URL of the conversion server (e.g., 'http://localhost:3001') + * @property {boolean} [enabled] - Whether conversion is enabled (default: true if serverUrl is provided) + * @property {number} [timeout] - Request timeout in milliseconds (default: 60000) + */ + +/** + * @typedef {Object} ConversionResult + * @property {boolean} success - Whether the conversion succeeded + * @property {File} [file] - The converted .docx file + * @property {Error} [error] - Error if conversion failed + */ + +/** + * Check if a file is a legacy .doc file that needs conversion + * @param {File|Blob} file - The file to check + * @returns {boolean} + */ +export const isDocFile = (file) => { + if (!file) return false; + + // Check by MIME type + if (file.type === DOC || file.type === 'application/msword') { + return true; + } + + // Check by extension (fallback for files without proper MIME type) + const name = file.name || ''; + return name.toLowerCase().endsWith('.doc') && !name.toLowerCase().endsWith('.docx'); +}; + +/** + * Check if a filename indicates a .doc file + * @param {string} filename - The filename to check + * @returns {boolean} + */ +export const isDocFilename = (filename) => { + if (!filename) return false; + const lower = filename.toLowerCase(); + return lower.endsWith('.doc') && !lower.endsWith('.docx'); +}; + +/** + * Convert a .doc file to .docx using the conversion server + * @param {File|Blob} file - The .doc file to convert + * @param {ConversionConfig} config - Conversion configuration + * @returns {Promise} + */ +export const convertDocToDocx = async (file, config) => { + const { serverUrl, timeout = 60000 } = config || {}; + + if (!serverUrl) { + return { + success: false, + error: new Error('Conversion server URL not configured. Set modules.conversion.serverUrl in SuperDoc config.'), + }; + } + + try { + const formData = new FormData(); + formData.append('file', file); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + const response = await fetch(`${serverUrl}/convert`, { + method: 'POST', + body: formData, + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `Conversion failed with status ${response.status}`); + } + + const blob = await response.blob(); + const originalName = file.name || 'document.doc'; + const newName = originalName.replace(/\.doc$/i, '.docx'); + + const convertedFile = new File([blob], newName, { type: DOCX }); + + return { + success: true, + file: convertedFile, + }; + } catch (error) { + if (error.name === 'AbortError') { + return { + success: false, + error: new Error('Conversion timed out. The file may be too large or the server is unavailable.'), + }; + } + + return { + success: false, + error: error instanceof Error ? error : new Error(String(error)), + }; + } +}; + +/** + * Check if the conversion server is available + * @param {string} serverUrl - The conversion server URL + * @returns {Promise} + */ +export const isConversionServerAvailable = async (serverUrl) => { + if (!serverUrl) return false; + + try { + const response = await fetch(`${serverUrl}/health`, { + method: 'GET', + signal: AbortSignal.timeout(5000), + }); + return response.ok; + } catch { + return false; + } +}; + +/** + * Get conversion server status including LibreOffice availability + * @param {string} serverUrl - The conversion server URL + * @returns {Promise<{available: boolean, libreOfficeInstalled: boolean, message: string}>} + */ +export const getConversionServerStatus = async (serverUrl) => { + if (!serverUrl) { + return { + available: false, + libreOfficeInstalled: false, + message: 'No conversion server URL configured', + }; + } + + try { + const response = await fetch(`${serverUrl}/check-libreoffice`, { + method: 'GET', + signal: AbortSignal.timeout(5000), + }); + + const data = await response.json(); + + return { + available: true, + libreOfficeInstalled: response.ok, + message: data.message || (response.ok ? 'Ready' : 'LibreOffice not installed'), + }; + } catch { + return { + available: false, + libreOfficeInstalled: false, + message: 'Conversion server unavailable', + }; + } +}; diff --git a/packages/superdoc/src/core/helpers/file.js b/packages/superdoc/src/core/helpers/file.js index 24a867967..63c39c440 100644 --- a/packages/superdoc/src/core/helpers/file.js +++ b/packages/superdoc/src/core/helpers/file.js @@ -1,4 +1,4 @@ -import { DOCX, PDF, HTML } from '@superdoc/common'; +import { DOC, DOCX, HTML, PDF } from '@superdoc/common'; /** * @typedef {Object} UploadWrapper @@ -60,6 +60,7 @@ export const extractBrowserFile = (input) => { const inferTypeFromName = (name = '') => { const lower = String(name).toLowerCase(); if (lower.endsWith('.docx')) return DOCX; + if (lower.endsWith('.doc')) return DOC; if (lower.endsWith('.pdf')) return PDF; if (lower.endsWith('.html') || lower.endsWith('.htm')) return HTML; if (lower.endsWith('.md') || lower.endsWith('.markdown')) return 'text/markdown'; diff --git a/packages/superdoc/src/dev/components/SuperdocDev.vue b/packages/superdoc/src/dev/components/SuperdocDev.vue index a8b276322..88dd22c4b 100644 --- a/packages/superdoc/src/dev/components/SuperdocDev.vue +++ b/packages/superdoc/src/dev/components/SuperdocDev.vue @@ -1,18 +1,15 @@