From a0353445d9f80bd323f412f66d60efc32f381a97 Mon Sep 17 00:00:00 2001 From: Elijah Date: Sun, 24 May 2026 12:41:31 -0700 Subject: [PATCH] Fix OnlyOffice: use internal IP for server-side URLs, create valid docx/pptx files --- backend/handlers/onlyoffice.go | 136 ++++++++++++++++++- frontend/src/components/OnlyOfficeEditor.tsx | 17 +-- 2 files changed, 139 insertions(+), 14 deletions(-) diff --git a/backend/handlers/onlyoffice.go b/backend/handlers/onlyoffice.go index c6e09a0..ce721a4 100644 --- a/backend/handlers/onlyoffice.go +++ b/backend/handlers/onlyoffice.go @@ -1,6 +1,7 @@ package handlers import ( + "archive/zip" "fmt" "io" "net/http" @@ -117,12 +118,10 @@ func (h *OnlyOfficeHandler) CreateBlank(c *fiber.Ctx) error { counter++ } - // Create an empty file. OnlyOffice will initialize it when opened. - f, err := os.Create(finalPath) - if err != nil { + // Create a valid Office Open XML file (docx/pptx are ZIP archives) + if err := createBlankOfficeFile(finalPath, ext); err != nil { return c.Status(500).JSON(fiber.Map{"error": "failed to create file"}) } - f.Close() // Return the relative path relativePath, _ := filepath.Rel(h.Config.StorageDir, finalPath) @@ -133,6 +132,135 @@ func (h *OnlyOfficeHandler) CreateBlank(c *fiber.Ctx) error { }) } +// createBlankOfficeFile creates a minimal valid .docx or .pptx file. +// These formats are ZIP archives containing XML files following the +// Office Open XML (OOXML) standard. +func createBlankOfficeFile(path string, ext string) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + w := zip.NewWriter(f) + defer w.Close() + + if ext == ".pptx" { + return writePptxContents(w) + } + return writeDocxContents(w) +} + +func writeDocxContents(w *zip.Writer) error { + files := map[string]string{ + "[Content_Types].xml": ` + + + + +`, + "_rels/.rels": ` + + +`, + "word/document.xml": ` + + + + +`, + } + for name, content := range files { + fw, err := w.Create(name) + if err != nil { + return err + } + if _, err := fw.Write([]byte(content)); err != nil { + return err + } + } + return nil +} + +func writePptxContents(w *zip.Writer) error { + files := map[string]string{ + "[Content_Types].xml": ` + + + + + + + +`, + "_rels/.rels": ` + + +`, + "ppt/presentation.xml": ` + + + + + +`, + "ppt/_rels/presentation.xml.rels": ` + + + +`, + "ppt/slideMasters/slideMaster1.xml": ` + + + + + +`, + "ppt/slideMasters/_rels/slideMaster1.xml.rels": ` + + +`, + "ppt/slideLayouts/slideLayout1.xml": ` + + + + +`, + "ppt/slideLayouts/_rels/slideLayout1.xml.rels": ` + + +`, + "ppt/slides/slide1.xml": ` + + + + +`, + "ppt/slides/_rels/slide1.xml.rels": ` + + +`, + } + for name, content := range files { + fw, err := w.Create(name) + if err != nil { + return err + } + if _, err := fw.Write([]byte(content)); err != nil { + return err + } + } + return nil +} + // SignConfig takes the OnlyOffice configuration payload and signs it with the JWT secret func (h *OnlyOfficeHandler) SignConfig(c *fiber.Ctx) error { if h.Config.OnlyOfficeJWT == "" { diff --git a/frontend/src/components/OnlyOfficeEditor.tsx b/frontend/src/components/OnlyOfficeEditor.tsx index 33f9cf2..0ab1e75 100644 --- a/frontend/src/components/OnlyOfficeEditor.tsx +++ b/frontend/src/components/OnlyOfficeEditor.tsx @@ -37,17 +37,14 @@ export default function OnlyOfficeEditor({ file, type, onClose }: OnlyOfficeEdit useEffect(() => { if (!downloadToken) return; - const getApiBase = () => { - if (process.env.NEXT_PUBLIC_API_URL) return process.env.NEXT_PUBLIC_API_URL; - if (typeof window !== 'undefined') return `${window.location.origin}/api`; - return 'http://localhost:8080'; - }; - - const API_BASE = getApiBase(); + // The OnlyOffice Document Server fetches these URLs server-side (not from the browser). + // It's on the default bridge network and can reach the Unraid host IP, but NOT + // the public domain (hairpin NAT) or Docker DNS names (different network). + // Port 5827 is the frontend container (which proxies /api/* to the backend). + const INTERNAL_URL = 'http://192.168.50.81:5827/api'; - // Use the public API_BASE which correctly routes through the Nginx proxy to the Next.js frontend - const fileUrl = `${API_BASE}/files/download/${encodeURIComponent(file.path)}?token=${downloadToken}`; - const callbackUrl = `${API_BASE}/public/onlyoffice/callback?path=${encodeURIComponent(file.path)}&token=${downloadToken}`; + const fileUrl = `${INTERNAL_URL}/files/download/${encodeURIComponent(file.path)}?token=${downloadToken}`; + const callbackUrl = `${INTERNAL_URL}/public/onlyoffice/callback?path=${encodeURIComponent(file.path)}&token=${downloadToken}`; const baseConfig: any = { document: {