v0.331.0: OnlyOffice opens and saves from FileBrowser and Nextcloud; FileBrowser back at once after a restart (R-1018)
gates / gates (push) Successful in 48s

FileBrowser config carries server.internalUrl http://filebrowser:80 (OnlyOffice fetched the public files address and got 403);
Nextcloud's DocumentServerInternalUrl gets its trailing slash (every save was lost: http://onlyoffice:80cache/...);
RestartStack("filebrowser") runs the R-1126 gate at once (files.<domain> was 502 for up to 5 min after the link switched).
Each red-proofed. MinAgent 0.158.1.
This commit is contained in:
2026-10-18 21:05:04 +02:00
parent 615940de35
commit 772ef36f5f
10 changed files with 317 additions and 44 deletions
@@ -29,41 +29,7 @@ func (h *OnlyOfficeNextcloudHandler) Apply(ac *ApplyContext) error {
}
publicURL := fmt.Sprintf("https://%s.%s", subdomain, ac.Domain)
internalURL := "http://onlyoffice:80"
// Install and configure OnlyOffice app in Nextcloud
commands := []struct {
args []string
tolerate string // substring in output to tolerate as success
}{
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "app:install", "onlyoffice"},
tolerate: "already installed",
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "app:enable", "onlyoffice"},
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "DocumentServerUrl", "--value=" + publicURL},
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "DocumentServerInternalUrl", "--value=" + internalURL},
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "jwt_secret", "--value=" + jwtSecret},
},
{
// StorageUrl: tells OO Document Server where to reach Nextcloud internally for file callbacks.
// Trailing slash is critical — NC's OO connector does string replacement of the server URL
// (which ends with /) with StorageUrl. Without trailing slash, "/apps/..." merges into hostname.
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "StorageUrl", "--value=http://nextcloud/"},
},
{
// Add internal hostname to trusted_domains so OO Document Server callbacks
// (arriving with Host: nextcloud) are not rejected by Nextcloud.
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:system:set", "trusted_domains", "10", "--value=nextcloud"},
},
}
commands := nextcloudApplyCommands(publicURL, jwtSecret)
for _, cmd := range commands {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
@@ -107,3 +73,52 @@ func (h *OnlyOfficeNextcloudHandler) Revoke(ac *ApplyContext) error {
ac.Logger.Printf("[INFO] [integrations] OnlyOffice integration revoked from Nextcloud")
return nil
}
// occCommand is one occ call of the apply; tolerate is a substring of its output that counts as success.
type occCommand struct {
args []string
tolerate string
}
// The two addresses OnlyOffice and Nextcloud use to reach each other inside the box. BOTH end with "/": Nextcloud's
// OnlyOffice connector appends paths to them („converter", „cache/files/…", „apps/onlyoffice/track"). R-1018,
// measured on demo-hp 2026-10-18: DocumentServerInternalUrl without the slash made Nextcloud fetch
// http://onlyoffice:80cache/files/… — every save failed („Track: … status 2 error") while the editor said
// „All changes saved". Pinned by TestNextcloudApply_R1018_InternalAddressesTakeAPath.
const (
ncDocServerInternalURL = "http://onlyoffice:80/"
ncStorageURL = "http://nextcloud/"
)
// nextcloudApplyCommands installs and configures the OnlyOffice app in Nextcloud.
func nextcloudApplyCommands(publicURL, jwtSecret string) []occCommand {
return []occCommand{
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "app:install", "onlyoffice"},
tolerate: "already installed",
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "app:enable", "onlyoffice"},
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "DocumentServerUrl", "--value=" + publicURL},
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "DocumentServerInternalUrl", "--value=" + ncDocServerInternalURL},
},
{
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "jwt_secret", "--value=" + jwtSecret},
},
{
// StorageUrl: tells OO Document Server where to reach Nextcloud internally for file callbacks.
// Trailing slash is critical — NC's OO connector does string replacement of the server URL
// (which ends with /) with StorageUrl. Without trailing slash, "/apps/..." merges into hostname.
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:app:set", "onlyoffice", "StorageUrl", "--value=" + ncStorageURL},
},
{
// Add internal hostname to trusted_domains so OO Document Server callbacks
// (arriving with Host: nextcloud) are not rejected by Nextcloud.
args: []string{"docker", "exec", "-u", "www-data", "nextcloud", "php", "occ", "config:system:set", "trusted_domains", "10", "--value=nextcloud"},
},
}
}
@@ -0,0 +1,44 @@
package integrations
import (
"net/url"
"strings"
"testing"
)
// R-1018: Nextcloud's OnlyOffice connector builds its server-to-server requests by APPENDING a path to the two inside
// addresses the apply sets. The consequence pinned: the address the apply really sends, plus such a path, is a request
// to the right container. Measured on demo-hp 2026-10-18: without the slash Nextcloud asked
// http://onlyoffice:80cache/files/… and every save was lost. RED-PROOF: drop the slash from ncDocServerInternalURL →
// the cache URL no longer parses (port "80cache") and this fails.
func TestNextcloudApply_R1018_InternalAddressesTakeAPath(t *testing.T) {
values := map[string]string{}
for _, c := range nextcloudApplyCommands("https://office.example.hu", "secret") {
if len(c.args) >= 11 && c.args[7] == "config:app:set" {
values[c.args[9]] = strings.TrimPrefix(c.args[10], "--value=")
}
}
for _, tc := range []struct {
key, path, host string
}{
{"DocumentServerInternalUrl", "cache/files/data/2153553391_5872/output.docx/output.docx", "onlyoffice:80"},
{"DocumentServerInternalUrl", "converter?shardkey=thumb_1", "onlyoffice:80"},
{"StorageUrl", "apps/onlyoffice/track?doc=x", "nextcloud"},
} {
base, ok := values[tc.key]
if !ok {
t.Fatalf("the apply does not set %s (set: %v)", tc.key, values)
}
u, err := url.Parse(base + tc.path)
if err != nil {
t.Errorf("%s %q + %q is not a URL: %v", tc.key, base, tc.path, err)
continue
}
if u.Host != tc.host || !strings.HasPrefix(u.Path, "/"+strings.SplitN(tc.path, "?", 2)[0][:4]) {
t.Errorf("%s %q + %q reaches host %q path %q, want host %q", tc.key, base, tc.path, u.Host, u.Path, tc.host)
}
}
if values["DocumentServerUrl"] != "https://office.example.hu" {
t.Errorf("DocumentServerUrl = %q, want the public office address", values["DocumentServerUrl"])
}
}