feat: app-to-app integration framework + OnlyOffice handlers
Generic integration system for connecting deployed apps via toggle UI. First handlers: OnlyOffice→FileBrowser (config.yaml patch) and OnlyOffice→Nextcloud (occ CLI). Lifecycle hooks auto-suspend on stop and re-apply on start. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package integrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// OnlyOfficeFileBrowserHandler enables/disables OnlyOffice document editing in FileBrowser Quantum.
|
||||
type OnlyOfficeFileBrowserHandler struct{}
|
||||
|
||||
func (h *OnlyOfficeFileBrowserHandler) Apply(ac *ApplyContext) error {
|
||||
jwtSecret := ac.ProviderEnv["JWT_SECRET"]
|
||||
if jwtSecret == "" {
|
||||
return fmt.Errorf("OnlyOffice JWT_SECRET nincs beállítva — telepítsd újra az alkalmazást")
|
||||
}
|
||||
|
||||
subdomain := ac.ProviderEnv["SUBDOMAIN"]
|
||||
if subdomain == "" && ac.ProviderMeta != nil {
|
||||
subdomain = ac.ProviderMeta.Subdomain
|
||||
}
|
||||
if subdomain == "" {
|
||||
return fmt.Errorf("OnlyOffice aldomain nem ismert")
|
||||
}
|
||||
|
||||
configPath := filepath.Join(ac.StacksDir, "filebrowser", "config.yaml")
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("FileBrowser config olvasási hiba: %w", err)
|
||||
}
|
||||
|
||||
// Remove any existing integrations section, then append the new one
|
||||
configStr := removeIntegrationsSection(string(configData))
|
||||
|
||||
officeURL := fmt.Sprintf("https://%s.%s", subdomain, ac.Domain)
|
||||
internalURL := "http://onlyoffice:80"
|
||||
|
||||
integrationsBlock := fmt.Sprintf("integrations:\n office:\n url: %q\n internalUrl: %q\n secret: %q\n viewOnly: false\n",
|
||||
officeURL, internalURL, jwtSecret)
|
||||
|
||||
configStr = strings.TrimRight(configStr, "\n") + "\n" + integrationsBlock
|
||||
|
||||
// Atomic write
|
||||
tmpPath := configPath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, []byte(configStr), 0644); err != nil {
|
||||
return fmt.Errorf("config írási hiba: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, configPath); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
return fmt.Errorf("config átnevezési hiba: %w", err)
|
||||
}
|
||||
|
||||
ac.Logger.Printf("[INFO] FileBrowser config updated with OnlyOffice integration")
|
||||
return ac.RestartStack("filebrowser")
|
||||
}
|
||||
|
||||
func (h *OnlyOfficeFileBrowserHandler) Revoke(ac *ApplyContext) error {
|
||||
configPath := filepath.Join(ac.StacksDir, "filebrowser", "config.yaml")
|
||||
configData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("config olvasási hiba: %w", err)
|
||||
}
|
||||
|
||||
cleaned := removeIntegrationsSection(string(configData))
|
||||
if cleaned == string(configData) {
|
||||
return nil // no integrations section, nothing to remove
|
||||
}
|
||||
|
||||
tmpPath := configPath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, []byte(cleaned), 0644); err != nil {
|
||||
return fmt.Errorf("config írási hiba: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, configPath); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
return fmt.Errorf("config átnevezési hiba: %w", err)
|
||||
}
|
||||
|
||||
ac.Logger.Printf("[INFO] FileBrowser config cleaned — OnlyOffice integration removed")
|
||||
return ac.RestartStack("filebrowser")
|
||||
}
|
||||
|
||||
// removeIntegrationsSection strips the integrations: YAML block from a config string.
|
||||
// It removes from the line starting with "integrations:" to the next unindented key or EOF.
|
||||
func removeIntegrationsSection(config string) string {
|
||||
lines := strings.Split(config, "\n")
|
||||
var result []string
|
||||
inBlock := false
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "integrations:") {
|
||||
inBlock = true
|
||||
continue
|
||||
}
|
||||
if inBlock {
|
||||
trimmed := strings.TrimRight(line, " \t\r")
|
||||
if trimmed == "" {
|
||||
continue // skip blank lines within block
|
||||
}
|
||||
if line[0] != ' ' && line[0] != '\t' {
|
||||
// New top-level key — end of integrations block
|
||||
inBlock = false
|
||||
result = append(result, line)
|
||||
}
|
||||
// else: still inside indented block, skip
|
||||
continue
|
||||
}
|
||||
result = append(result, line)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
Reference in New Issue
Block a user