2016-07-07 18:54:16 -07:00
|
|
|
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
|
|
|
//
|
2016-07-20 15:58:37 +01:00
|
|
|
// This software (Documize Community Edition) is licensed under
|
2016-07-07 18:54:16 -07:00
|
|
|
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
|
|
|
//
|
|
|
|
// You can operate outside the AGPL restrictions by purchasing
|
|
|
|
// Documize Enterprise Edition and obtaining a commercial license
|
2016-07-20 15:58:37 +01:00
|
|
|
// by contacting <sales@documize.com>.
|
2016-07-07 18:54:16 -07:00
|
|
|
//
|
|
|
|
// https://documize.com
|
|
|
|
|
|
|
|
// Package main provides a simple Documize plugin for document conversions using libreoffice.
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/base64"
|
|
|
|
"errors"
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net"
|
|
|
|
"net/rpc"
|
|
|
|
"net/rpc/jsonrpc"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
|
|
|
"runtime"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
"unicode"
|
|
|
|
"unicode/utf8"
|
|
|
|
|
2016-07-20 15:58:37 +01:00
|
|
|
api "github.com/documize/community/core/convapi"
|
2016-07-07 18:54:16 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
var cmdmtx sync.Mutex // enforce only one conversion at a time
|
|
|
|
|
|
|
|
// LibreOffice provides a peg on which to hang the Convert method.
|
|
|
|
type LibreOffice struct{}
|
|
|
|
|
|
|
|
var dir *os.File
|
|
|
|
var outputDir string
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
tempDir := os.TempDir()
|
|
|
|
if !strings.HasSuffix(tempDir, string(os.PathSeparator)) {
|
|
|
|
tempDir += string(os.PathSeparator)
|
|
|
|
}
|
|
|
|
outputDir = tempDir + "documize-plugin-libreoffice"
|
|
|
|
err := os.MkdirAll(outputDir, os.ModePerm)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func createTempDir() *os.File {
|
|
|
|
fmt.Println("create temp dir")
|
|
|
|
err := os.Mkdir(outputDir, 0777) // make the dir if non-existent, TODO filemode
|
|
|
|
if err != nil {
|
|
|
|
//fmt.Println("unable to create temp dir")
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
dir, err = os.Open(outputDir)
|
|
|
|
if err != nil {
|
|
|
|
//fmt.Println("unable to open created temp dir")
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return dir
|
|
|
|
}
|
|
|
|
|
|
|
|
func removePrevTempFiles(dir *os.File) error {
|
|
|
|
fin, err := dir.Readdirnames(-1)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, nam := range fin { // remove any previous temporary files
|
|
|
|
target := outputDir + string(os.PathSeparator) + nam
|
|
|
|
//fmt.Println("delete temp file: " + target)
|
|
|
|
err = os.Remove(target)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func runLibreOffice(inPath string) error {
|
|
|
|
var err error
|
|
|
|
var cmd = "soffice"
|
|
|
|
switch runtime.GOOS {
|
|
|
|
case "darwin": // may not be in the path
|
|
|
|
cmd = "/Applications/LibreOffice.app/Contents/MacOS/soffice"
|
|
|
|
case "windows": // TODO
|
|
|
|
}
|
|
|
|
cmd, err = exec.LookPath(cmd)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
args := []string{"--headless",
|
|
|
|
"--convert-to", "html", // "html:XHTML Writer File:UTF8",
|
|
|
|
"--outdir", outputDir, inPath}
|
|
|
|
|
|
|
|
fmt.Println("libreoffice args:", args)
|
|
|
|
|
|
|
|
ecmd := exec.Command(cmd, args...)
|
|
|
|
var outBuf, errBuf bytes.Buffer
|
|
|
|
ecmd.Stdout, ecmd.Stderr = &outBuf, &errBuf
|
|
|
|
over := make(chan error, 1)
|
|
|
|
go func() {
|
|
|
|
if e := ecmd.Start(); e != nil {
|
|
|
|
over <- e
|
|
|
|
} else {
|
|
|
|
over <- ecmd.Wait()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
select {
|
|
|
|
case err = <-over:
|
|
|
|
if err != nil {
|
|
|
|
return errors.New(string(outBuf.Bytes()) + string(errBuf.Bytes()) + err.Error())
|
|
|
|
}
|
|
|
|
case <-time.After(2 * time.Minute):
|
|
|
|
ke := ""
|
|
|
|
if runtime.GOOS != "windows" { // Process is not available on windows
|
|
|
|
err = ecmd.Process.Kill()
|
|
|
|
if err != nil {
|
|
|
|
ke = ", kill error: " + err.Error()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return errors.New("libreoffice.Convert() cancelled via timeout" + ke)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert converts a file into the Countersoft Documize format.
|
|
|
|
func (file *LibreOffice) Convert(r api.DocumentConversionRequest, reply *api.DocumentConversionResponse) error {
|
|
|
|
var err error
|
|
|
|
|
|
|
|
cmdmtx.Lock() // enforce only one conversion at a time
|
|
|
|
defer cmdmtx.Unlock()
|
|
|
|
|
|
|
|
dir, err = os.Open(outputDir)
|
|
|
|
if err != nil {
|
|
|
|
dir = createTempDir()
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
if e := dir.Close(); e != nil {
|
|
|
|
fmt.Fprintln(os.Stderr, "Error closing temp dir: "+e.Error())
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
err = removePrevTempFiles(dir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, inFileX := filepath.Split(r.Filename)
|
|
|
|
inFile := fixName(inFileX)
|
|
|
|
if inFile == "" {
|
|
|
|
return errors.New("no filename")
|
|
|
|
}
|
|
|
|
|
|
|
|
xtn := filepath.Ext(inFile)
|
|
|
|
if xtn == "" {
|
|
|
|
return errors.New("invalid filename: " + inFile)
|
|
|
|
}
|
|
|
|
|
|
|
|
inPath := outputDir + string(os.PathSeparator) + inFile
|
|
|
|
fmt.Println("writing data to: " + inPath)
|
|
|
|
if err = ioutil.WriteFile(inPath, r.Filedata, 0777); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = runLibreOffice(inPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
outPath := strings.TrimSuffix(inPath, xtn) + ".html"
|
|
|
|
fmt.Println("output file: " + outPath)
|
|
|
|
reply.PagesHTML, err = ioutil.ReadFile(outPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
fmt.Printf("%d bytes read from: %s\n", len(reply.PagesHTML), outPath)
|
|
|
|
|
|
|
|
incorporateImages(reply)
|
|
|
|
|
|
|
|
return ioutil.WriteFile(outputDir+string(os.PathSeparator)+"debug.html", reply.PagesHTML, 0666)
|
|
|
|
}
|
|
|
|
|
|
|
|
func incorporateImages(reply *api.DocumentConversionResponse) {
|
|
|
|
dir, err := os.Open(outputDir)
|
|
|
|
if err == nil {
|
|
|
|
names, err := dir.Readdirnames(-1)
|
|
|
|
if err == nil {
|
|
|
|
for _, nam := range names {
|
|
|
|
//fmt.Println("Found file " + nam)
|
|
|
|
switch pic := strings.ToLower(filepath.Ext(nam)[1:]); pic {
|
|
|
|
case "jpg", "gif", "png", "webp": // TODO others?
|
|
|
|
//fmt.Println("Found picture file " + nam)
|
|
|
|
enam := fixName(nam)
|
|
|
|
buf, err := ioutil.ReadFile(outputDir + string(os.PathSeparator) + nam)
|
|
|
|
if err == nil {
|
|
|
|
enc := base64.StdEncoding.EncodeToString(buf)
|
|
|
|
benam := []byte(`<img src="` + enam + `"`)
|
|
|
|
split := bytes.Split(reply.PagesHTML, benam)
|
|
|
|
rep := []byte(`<img src="data:image/` + pic + `;base64,` + enc + `"`)
|
|
|
|
/*
|
|
|
|
fmt.Println("DEBUG read picture file ", nam,
|
|
|
|
"size", len(buf),
|
|
|
|
"benam", string(benam),
|
|
|
|
"len(split)", len(split),
|
|
|
|
"replacement size", len(rep))
|
|
|
|
*/
|
|
|
|
if len(rep)*len(split) > 1000000 {
|
|
|
|
fmt.Println("Too large a file increase to replace (>1Mb)")
|
|
|
|
} else {
|
|
|
|
reply.PagesHTML = bytes.Join(split, rep)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var port string
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
flag.StringVar(&port, "port", "", "the port to listen on")
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
var err error
|
|
|
|
|
|
|
|
fmt.Println("Documize plugin-libreoffice starting up")
|
|
|
|
|
|
|
|
fmt.Println("outputDir:" + outputDir)
|
|
|
|
|
|
|
|
// register all the services that we accept
|
|
|
|
fileConverter := new(LibreOffice)
|
|
|
|
err = rpc.Register(fileConverter)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
flag.Parse()
|
|
|
|
if port == "" {
|
|
|
|
fmt.Fprintln(os.Stderr, "no port specified, please use the '-port' flag")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// set up the server
|
|
|
|
listener, err := net.Listen("tcp", ":"+port)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// start server
|
|
|
|
fmt.Println("Documize plugin-libreoffice server up on", listener.Addr())
|
|
|
|
|
|
|
|
for {
|
|
|
|
conn, err := listener.Accept()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
go jsonrpc.ServeConn(conn) // using JSON encoding
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func fixName(s string) string {
|
|
|
|
ret := ""
|
|
|
|
for _, r := range s {
|
|
|
|
if utf8.RuneLen(r) > 1 ||
|
|
|
|
unicode.IsSpace(r) ||
|
|
|
|
unicode.IsSymbol(r) ||
|
|
|
|
r == '%' {
|
|
|
|
ret += fmt.Sprintf("%%%x", r)
|
|
|
|
} else {
|
|
|
|
ret += string(r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|