diff --git a/cozy.example.yaml b/cozy.example.yaml index afe4fc3cbfc..c0466fae644 100644 --- a/cozy.example.yaml +++ b/cozy.example.yaml @@ -133,6 +133,7 @@ jobs: # - "konnector": launching konnectors # - "service": launching services # - "migrations": transforming a VFS with Swift to layout v3 + # - "office-save": saving office documents to the VFS # - "notes-save": saving notes to the VFS # - "push": sending push notifications # - "sms": sending SMS notifications diff --git a/docs/docker.md b/docs/docker.md index 6d2f9f19a3e..78d145b0258 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -68,7 +68,7 @@ $ docker run -it --rm --name=oodev --net=host cozy/onlyoffice-dev and run the stack with: ```bash -$ cozy-stack serve --disable-csp --onlyoffice-url=http://localhost:8000/ --onlyoffice-inbox-secret=inbox_secret --onlyoffice-outbox-secret=outbox_secret +$ cozy-stack serve --disable-csp --onlyoffice-url=http://localhost:8000 --onlyoffice-inbox-secret=inbox_secret --onlyoffice-outbox-secret=outbox_secret ``` If you need to rebuild it, you can do that with: diff --git a/docs/workers.md b/docs/workers.md index 11b21f34da6..1c275570ad6 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -354,6 +354,11 @@ optionaly the old version of this document. The message is composed of a sharing ID and a count of the number of errors (i.e. the number of times this job was retried). +## office-save + +This worker is for the internal usage of the stack. It allows to ask OnlyOffice +to save opened documents to the VFS. + ## notes-save This is another worker for the interal usage of the stack. It allows to write diff --git a/model/office/open.go b/model/office/open.go index 0f17fcc5302..3012edbe65f 100644 --- a/model/office/open.go +++ b/model/office/open.go @@ -175,6 +175,10 @@ func (o *Opener) openLocalDocument(memberIndex int, readOnly bool) (*apiOfficeUR Infof("Cannot add doc to store: %s", err) return nil, ErrInternalServerError } + if err := setupTrigger(o.Inst, key); err != nil { + o.Inst.Logger().WithNamespace("office"). + Warnf("Cannot setup trigger: %s", err) + } publicName, _ := o.Inst.PublicName() doc.PublicName = publicName doc.OO = &onlyOffice{ diff --git a/model/office/trigger.go b/model/office/trigger.go new file mode 100644 index 00000000000..660896aa892 --- /dev/null +++ b/model/office/trigger.go @@ -0,0 +1,111 @@ +package office + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "net/url" + "strings" + + "github.com/cozy/cozy-stack/model/instance" + "github.com/cozy/cozy-stack/model/job" + jwt "github.com/golang-jwt/jwt/v4" + "github.com/labstack/echo/v4" +) + +// SendSaveMessage is used by the trigger for asking OO to save the document in +// the Cozy. +type SendSaveMessage struct { + Key string `json:"key"` +} + +func setupTrigger(inst *instance.Instance, key string) error { + sched := job.System() + infos := job.TriggerInfos{ + Type: "@every", + WorkerType: "office-save", + Arguments: "10m", + } + msg := &SendSaveMessage{Key: key} + t, err := job.NewTrigger(inst, infos, msg) + if err != nil { + return err + } + return sched.AddTrigger(t) +} + +type commandRequest struct { + Command string `json:"c"` + Key string `json:"key"` + Userdata string `json:"userdata"` + Token string `json:"token,omitempty"` +} + +// Valid is required by the jwt.Claims interface +func (c *commandRequest) Valid() error { return nil } + +type commandResponse struct { + Key string `json:"key"` + Error int `json:"error"` +} + +func SendSave(inst *instance.Instance, msg SendSaveMessage) error { + if _, err := GetStore().GetDoc(inst, msg.Key); err != nil { + // By returning the ErrBadTrigger code, the stack will know that it + // must delete the trigger. + return job.ErrBadTrigger{Err: err} + } + cfg := getConfig(inst.ContextName) + + cmd := &commandRequest{ + Command: "forcesave", + Key: msg.Key, + Userdata: "stack", + } + if cfg.InboxSecret != "" { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, cmd) + signed, err := token.SignedString([]byte(cfg.InboxSecret)) + if err != nil { + return err + } + cmd.Token = signed + } + body, err := json.Marshal(cmd) + if err != nil { + return err + } + + u, err := url.Parse(cfg.OnlyOfficeURL) + if err != nil { + return err + } + u.Path = strings.TrimSuffix(u.Path, "/") + u.Path += "/coauthoring/CommandService.ashx" + commandURL := u.String() + + res, err := docserverClient.Post(commandURL, echo.MIMEApplicationJSON, bytes.NewReader(body)) + if err != nil { + return err + } + defer func() { + // Flush the body to allow reusing the connection with Keep-Alive + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() + }() + var cmdRes commandResponse + if err := json.NewDecoder(res.Body).Decode(&cmdRes); err != nil { + return err + } + // 0 means OK to save, and 4 means that the doc has not changed + if cmdRes.Error != 0 && cmdRes.Error != 4 { + inst.Logger().WithNamespace("office"). + Warnf("error for forcesave %s: %d", msg.Key, cmdRes.Error) + } + // 1 and 3 means that something unexpected happens, 1 for OO side, 3 for + // the stack side: in both cases, we delete the invalid trigger + if cmdRes.Error == 1 || cmdRes.Error == 3 { + return job.ErrBadTrigger{Err: errors.New("unexpected state")} + } + return nil +} diff --git a/web/jobs/jobs.go b/web/jobs/jobs.go index 5557d7a6fec..e69e6403cbe 100644 --- a/web/jobs/jobs.go +++ b/web/jobs/jobs.go @@ -36,6 +36,7 @@ import ( _ "github.com/cozy/cozy-stack/worker/moves" _ "github.com/cozy/cozy-stack/worker/notes" _ "github.com/cozy/cozy-stack/worker/oauth" + _ "github.com/cozy/cozy-stack/worker/office" _ "github.com/cozy/cozy-stack/worker/push" _ "github.com/cozy/cozy-stack/worker/share" _ "github.com/cozy/cozy-stack/worker/sms" diff --git a/worker/office/office.go b/worker/office/office.go new file mode 100644 index 00000000000..c5df510093d --- /dev/null +++ b/worker/office/office.go @@ -0,0 +1,29 @@ +package office + +import ( + "runtime" + "time" + + "github.com/cozy/cozy-stack/model/job" + "github.com/cozy/cozy-stack/model/office" +) + +func init() { + job.AddWorker(&job.WorkerConfig{ + WorkerType: "office-save", + Concurrency: runtime.NumCPU(), + MaxExecCount: 2, + Reserved: true, + Timeout: 30 * time.Second, + WorkerFunc: WorkerSave, + }) +} + +// WorkerSave is used for asking OnlyOffice to save a document in the Cozy. +func WorkerSave(ctx *job.WorkerContext) error { + var msg office.SendSaveMessage + if err := ctx.UnmarshalMessage(&msg); err != nil { + return err + } + return office.SendSave(ctx.Instance, msg) +}