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: {