github.com/elliott5/community@v0.14.1-0.20160709191136-823126fb026a/plugin-libreoffice/plugin.go (about)

     1  // Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
     2  //
     3  // This software (Documize Community Edition) is licensed under 
     4  // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
     5  //
     6  // You can operate outside the AGPL restrictions by purchasing
     7  // Documize Enterprise Edition and obtaining a commercial license
     8  // by contacting <sales@documize.com>. 
     9  //
    10  // https://documize.com
    11  
    12  // Package main provides a simple Documize plugin for document conversions using libreoffice.
    13  package main
    14  
    15  import (
    16  	"bytes"
    17  	"encoding/base64"
    18  	"errors"
    19  	"flag"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"net"
    23  	"net/rpc"
    24  	"net/rpc/jsonrpc"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  	"runtime"
    29  	"strings"
    30  	"sync"
    31  	"time"
    32  	"unicode"
    33  	"unicode/utf8"
    34  
    35  	"github.com/documize/community/wordsmith/api"
    36  )
    37  
    38  var cmdmtx sync.Mutex // enforce only one conversion at a time
    39  
    40  // LibreOffice provides a peg on which to hang the Convert method.
    41  type LibreOffice struct{}
    42  
    43  var dir *os.File
    44  var outputDir string
    45  
    46  func init() {
    47  	tempDir := os.TempDir()
    48  	if !strings.HasSuffix(tempDir, string(os.PathSeparator)) {
    49  		tempDir += string(os.PathSeparator)
    50  	}
    51  	outputDir = tempDir + "documize-plugin-libreoffice"
    52  	err := os.MkdirAll(outputDir, os.ModePerm)
    53  	if err != nil {
    54  		panic(err)
    55  	}
    56  }
    57  
    58  func createTempDir() *os.File {
    59  	fmt.Println("create temp dir")
    60  	err := os.Mkdir(outputDir, 0777) // make the dir if non-existent, TODO filemode
    61  	if err != nil {
    62  		//fmt.Println("unable to create temp dir")
    63  		panic(err)
    64  	}
    65  	dir, err = os.Open(outputDir)
    66  	if err != nil {
    67  		//fmt.Println("unable to open created temp dir")
    68  		panic(err)
    69  	}
    70  	return dir
    71  }
    72  
    73  func removePrevTempFiles(dir *os.File) error {
    74  	fin, err := dir.Readdirnames(-1)
    75  	if err != nil {
    76  		return err
    77  	}
    78  	for _, nam := range fin { // remove any previous temporary files
    79  		target := outputDir + string(os.PathSeparator) + nam
    80  		//fmt.Println("delete temp file: " + target)
    81  		err = os.Remove(target)
    82  		if err != nil {
    83  			return err
    84  		}
    85  	}
    86  	return nil
    87  }
    88  
    89  func runLibreOffice(inPath string) error {
    90  	var err error
    91  	var cmd = "soffice"
    92  	switch runtime.GOOS {
    93  	case "darwin": // may not be in the path
    94  		cmd = "/Applications/LibreOffice.app/Contents/MacOS/soffice"
    95  	case "windows": // TODO
    96  	}
    97  	cmd, err = exec.LookPath(cmd)
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	args := []string{"--headless",
   103  		"--convert-to", "html", // "html:XHTML Writer File:UTF8",
   104  		"--outdir", outputDir, inPath}
   105  
   106  	fmt.Println("libreoffice args:", args)
   107  
   108  	ecmd := exec.Command(cmd, args...)
   109  	var outBuf, errBuf bytes.Buffer
   110  	ecmd.Stdout, ecmd.Stderr = &outBuf, &errBuf
   111  	over := make(chan error, 1)
   112  	go func() {
   113  		if e := ecmd.Start(); e != nil {
   114  			over <- e
   115  		} else {
   116  			over <- ecmd.Wait()
   117  		}
   118  	}()
   119  	select {
   120  	case err = <-over:
   121  		if err != nil {
   122  			return errors.New(string(outBuf.Bytes()) + string(errBuf.Bytes()) + err.Error())
   123  		}
   124  	case <-time.After(2 * time.Minute):
   125  		ke := ""
   126  		if runtime.GOOS != "windows" { // Process is not available on windows
   127  			err = ecmd.Process.Kill()
   128  			if err != nil {
   129  				ke = ", kill error: " + err.Error()
   130  			}
   131  		}
   132  		return errors.New("libreoffice.Convert() cancelled via timeout" + ke)
   133  	}
   134  	return nil
   135  }
   136  
   137  // Convert converts a file into the Countersoft Documize format.
   138  func (file *LibreOffice) Convert(r api.DocumentConversionRequest, reply *api.DocumentConversionResponse) error {
   139  	var err error
   140  
   141  	cmdmtx.Lock() // enforce only one conversion at a time
   142  	defer cmdmtx.Unlock()
   143  
   144  	dir, err = os.Open(outputDir)
   145  	if err != nil {
   146  		dir = createTempDir()
   147  	}
   148  	defer func() {
   149  		if e := dir.Close(); e != nil {
   150  			fmt.Fprintln(os.Stderr, "Error closing temp dir: "+e.Error())
   151  		}
   152  	}()
   153  
   154  	err = removePrevTempFiles(dir)
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	_, inFileX := filepath.Split(r.Filename)
   160  	inFile := fixName(inFileX)
   161  	if inFile == "" {
   162  		return errors.New("no filename")
   163  	}
   164  
   165  	xtn := filepath.Ext(inFile)
   166  	if xtn == "" {
   167  		return errors.New("invalid filename: " + inFile)
   168  	}
   169  
   170  	inPath := outputDir + string(os.PathSeparator) + inFile
   171  	fmt.Println("writing data to: " + inPath)
   172  	if err = ioutil.WriteFile(inPath, r.Filedata, 0777); err != nil {
   173  		return err
   174  	}
   175  
   176  	err = runLibreOffice(inPath)
   177  	if err != nil {
   178  		return err
   179  	}
   180  
   181  	outPath := strings.TrimSuffix(inPath, xtn) + ".html"
   182  	fmt.Println("output file: " + outPath)
   183  	reply.PagesHTML, err = ioutil.ReadFile(outPath)
   184  	if err != nil {
   185  		return err
   186  	}
   187  	fmt.Printf("%d bytes read from: %s\n", len(reply.PagesHTML), outPath)
   188  
   189  	incorporateImages(reply)
   190  
   191  	return ioutil.WriteFile(outputDir+string(os.PathSeparator)+"debug.html", reply.PagesHTML, 0666)
   192  }
   193  
   194  func incorporateImages(reply *api.DocumentConversionResponse) {
   195  	dir, err := os.Open(outputDir)
   196  	if err == nil {
   197  		names, err := dir.Readdirnames(-1)
   198  		if err == nil {
   199  			for _, nam := range names {
   200  				//fmt.Println("Found file " + nam)
   201  				switch pic := strings.ToLower(filepath.Ext(nam)[1:]); pic {
   202  				case "jpg", "gif", "png", "webp": // TODO others?
   203  					//fmt.Println("Found picture file " + nam)
   204  					enam := fixName(nam)
   205  					buf, err := ioutil.ReadFile(outputDir + string(os.PathSeparator) + nam)
   206  					if err == nil {
   207  						enc := base64.StdEncoding.EncodeToString(buf)
   208  						benam := []byte(`<img src="` + enam + `"`)
   209  						split := bytes.Split(reply.PagesHTML, benam)
   210  						rep := []byte(`<img src="data:image/` + pic + `;base64,` + enc + `"`)
   211  						/*
   212  							fmt.Println("DEBUG read picture file ", nam,
   213  								"size", len(buf),
   214  								"benam", string(benam),
   215  								"len(split)", len(split),
   216  								"replacement size", len(rep))
   217  						*/
   218  						if len(rep)*len(split) > 1000000 {
   219  							fmt.Println("Too large a file increase to replace (>1Mb)")
   220  						} else {
   221  							reply.PagesHTML = bytes.Join(split, rep)
   222  						}
   223  					}
   224  				}
   225  			}
   226  		}
   227  	}
   228  }
   229  
   230  var port string
   231  
   232  func init() {
   233  	flag.StringVar(&port, "port", "", "the port to listen on")
   234  }
   235  
   236  func main() {
   237  	var err error
   238  
   239  	fmt.Println("Documize plugin-libreoffice starting up")
   240  
   241  	fmt.Println("outputDir:" + outputDir)
   242  
   243  	// register all the services that we accept
   244  	fileConverter := new(LibreOffice)
   245  	err = rpc.Register(fileConverter)
   246  	if err != nil {
   247  		panic(err)
   248  	}
   249  
   250  	flag.Parse()
   251  	if port == "" {
   252  		fmt.Fprintln(os.Stderr, "no port specified, please use the '-port' flag")
   253  		return
   254  	}
   255  
   256  	// set up the server
   257  	listener, err := net.Listen("tcp", ":"+port)
   258  	if err != nil {
   259  		panic(err)
   260  	}
   261  
   262  	// start server
   263  	fmt.Println("Documize plugin-libreoffice server up on", listener.Addr())
   264  
   265  	for {
   266  		conn, err := listener.Accept()
   267  		if err != nil {
   268  			panic(err)
   269  		}
   270  
   271  		go jsonrpc.ServeConn(conn) // using JSON encoding
   272  	}
   273  }
   274  
   275  func fixName(s string) string {
   276  	ret := ""
   277  	for _, r := range s {
   278  		if utf8.RuneLen(r) > 1 ||
   279  			unicode.IsSpace(r) ||
   280  			unicode.IsSymbol(r) ||
   281  			r == '%' {
   282  			ret += fmt.Sprintf("%%%x", r)
   283  		} else {
   284  			ret += string(r)
   285  		}
   286  	}
   287  	return ret
   288  }