SPB Git

spb/drive Public

SPB Drive — self-hosted personal cloud drive (files, previews, sharing) on the MacLustr cluster.

JavaScript 82.7% CSS 10.6% Nunjucks 3.6% Shell 1.8% SQL 1.3%
3.4 KB · 83 lines javascript
Raw Blame History
1/**2 * ─────────────────────────────────────────────3 *  SPB Drive — Personal Cloud Drive4 * ─────────────────────────────────────────────5 *  Author  : Simon-Pierre Boucher6 *  Contact : contact@spboucher.ai7 *  File    : src/preview/office.mjs8 *  Purpose : LibreOffice headless → PDF conversion, queued + cached by sha9 *  License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { execFile } from 'node:child_process';14import { existsSync } from 'node:fs';15import { mkdtemp, readdir, rename, rm, symlink } from 'node:fs/promises';16import os from 'node:os';17import path from 'node:path';18import { promisify } from 'node:util';19import { config } from '../config.mjs';20import { blobPath } from '../storage/blobs.mjs';21import { enqueue, jobStatus } from './queue.mjs';22import { hasBin } from './thumbs.mjs';2324const execFileP = promisify(execFile);25const OFFICE_TIMEOUT = 5 * 60_000;2627export const OFFICE_EXTS = new Set(['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'rtf']);2829const officePdf = (sha) => path.join(config.cacheDir, 'office', `${sha}.pdf`);3031/** Locate the LibreOffice binary (macOS app bundle or PATH). */32export async function sofficeBin() {33  const mac = '/Applications/LibreOffice.app/Contents/MacOS/soffice';34  if (existsSync(mac)) return mac;35  if (await hasBin('soffice')) return 'soffice';36  if (await hasBin('libreoffice')) return 'libreoffice';37  return null;38}3940/** Conversion status: ready / processing / queued / unavailable / none. */41export function officeState(sha) {42  if (existsSync(officePdf(sha))) return { state: 'ready', path: officePdf(sha) };43  const job = jobStatus(`office:${sha}`);44  if (job?.status === 'running') return { state: 'processing' };45  if (job?.status === 'queued') return { state: 'queued' };46  if (job?.status === 'error') return { state: 'unavailable' };47  return { state: 'none' };48}4950/**51 * Convert an office document blob to PDF (queued, cached).52 * @param {string} ext original extension so soffice picks the right filter53 * @returns {Promise<string|null>} pdf path, or null when LibreOffice is absent54 */55export async function convertToPdf(sha, ext) {56  const out = officePdf(sha);57  if (existsSync(out)) return out;58  const bin = await sofficeBin();59  if (!bin) return null;6061  return enqueue(`office:${sha}`, async () => {62    if (existsSync(out)) return out;63    const workDir = await mkdtemp(path.join(os.tmpdir(), 'spbdrive-office-'));64    try {65      // soffice needs a real extension to choose an import filter.66      const input = path.join(workDir, `doc.${ext}`);67      await symlink(blobPath(sha), input);68      await execFileP(69        bin,70        ['--headless', '--norestore', `-env:UserInstallation=file://${workDir}/lo-profile`,71          '--convert-to', 'pdf', '--outdir', workDir, input],72        { timeout: OFFICE_TIMEOUT },73      );74      const produced = (await readdir(workDir)).find((f) => f.endsWith('.pdf'));75      if (!produced) throw new Error('LibreOffice produced no PDF');76      await rename(path.join(workDir, produced), out);77      return out;78    } finally {79      await rm(workDir, { recursive: true, force: true });80    }81  });82}83