208 lines
7.5 KiB
Go
208 lines
7.5 KiB
Go
package main
|
|
|
|
// Conversion before printing (novox/hq ADR 0468 clause 7, issue 1600). The printer takes PDF and JPEG. A stored
|
|
// document of a type the module only-office converts (seat document-converter) is turned into a PDF first: the device
|
|
// process signs a GET link on the document and a PUT link on <sha>.pdf with the binding's key, calls
|
|
// document-converter.convert, checks that the PDF the converter says it put is the PDF the bucket holds, and prints
|
|
// that. Any other type is refused, naming the types taken.
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/objectlink"
|
|
)
|
|
|
|
// ConvertFollow is how long the device process follows one conversion, in calls of under 25 s each.
|
|
const ConvertFollow = 3 * time.Minute
|
|
|
|
// ConvertTypes are the types document-converter takes (clause 7).
|
|
var ConvertTypes = []string{"docx", "doc", "xlsx", "xls", "pptx", "ppt", "odt", "ods", "odp", "rtf", "txt", "html"}
|
|
|
|
// contentTypes are the media types of a fetched document, by the type the converter is asked for.
|
|
var contentTypes = map[string]string{
|
|
"text/html": "html", "application/xhtml+xml": "html", "text/plain": "txt", "application/rtf": "rtf", "text/rtf": "rtf",
|
|
"application/msword": "doc", "application/vnd.ms-excel": "xls", "application/vnd.ms-powerpoint": "ppt",
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
|
"application/vnd.oasis.opendocument.text": "odt",
|
|
"application/vnd.oasis.opendocument.spreadsheet": "ods",
|
|
"application/vnd.oasis.opendocument.presentation": "odp",
|
|
}
|
|
|
|
// the first bytes of an OLE compound file (the legacy Word, Excel and PowerPoint formats), and the stream names that
|
|
// tell them apart, as UTF-16 (MS-CFB: directory entry names are UTF-16LE).
|
|
var oleMagic = []byte{0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1}
|
|
|
|
func utf16le(s string) []byte {
|
|
var b []byte
|
|
for _, r := range s {
|
|
b = append(b, byte(r), byte(r>>8))
|
|
}
|
|
return b
|
|
}
|
|
|
|
// ConvertibleType is the type a document is, among the converter's twelve, told by its bytes and, for a document that
|
|
// came from a web server, by its Content-Type; "" when it is none of them. A document that is a PDF or a JPEG is
|
|
// not asked about: the printer takes it as it is.
|
|
func ConvertibleType(doc []byte, contentType string) string {
|
|
if i := strings.IndexByte(contentType, ';'); i >= 0 {
|
|
contentType = contentType[:i]
|
|
}
|
|
byHeader := contentTypes[strings.ToLower(strings.TrimSpace(contentType))]
|
|
switch {
|
|
case bytes.HasPrefix(doc, []byte("PK\x03\x04")):
|
|
if t := zipType(doc); t != "" {
|
|
return t
|
|
}
|
|
return ""
|
|
case bytes.HasPrefix(doc, oleMagic):
|
|
for name, t := range map[string]string{"WordDocument": "doc", "Workbook": "xls", "PowerPoint Document": "ppt"} {
|
|
if bytes.Contains(doc, utf16le(name)) {
|
|
return t
|
|
}
|
|
}
|
|
return ""
|
|
case bytes.HasPrefix(doc, []byte(`{\rtf`)):
|
|
return "rtf"
|
|
}
|
|
if byHeader == "html" || looksLikeHTML(doc) {
|
|
if utf8.Valid(doc) && !bytes.Contains(doc, []byte{0}) {
|
|
return "html"
|
|
}
|
|
return ""
|
|
}
|
|
if utf8.Valid(doc) && !bytes.Contains(doc, []byte{0}) && len(doc) > 0 {
|
|
return "txt"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func looksLikeHTML(doc []byte) bool {
|
|
head := doc
|
|
if len(head) > 512 {
|
|
head = head[:512]
|
|
}
|
|
s := strings.ToLower(strings.TrimLeft(strings.TrimPrefix(string(head), "\xef\xbb\xbf"), " \t\r\n"))
|
|
return strings.HasPrefix(s, "<!doctype html") || strings.HasPrefix(s, "<html")
|
|
}
|
|
|
|
// zipType tells the packages apart by their parts: OOXML by its main part (ECMA-376 Part 2), OpenDocument by its
|
|
// first file `mimetype` (ODF 1.2 Part 3 §3.3).
|
|
func zipType(doc []byte) string {
|
|
zr, err := zip.NewReader(bytes.NewReader(doc), int64(len(doc)))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
for _, f := range zr.File {
|
|
switch f.Name {
|
|
case "word/document.xml":
|
|
return "docx"
|
|
case "xl/workbook.xml":
|
|
return "xlsx"
|
|
case "ppt/presentation.xml":
|
|
return "pptx"
|
|
case "mimetype":
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
mt, _ := io.ReadAll(io.LimitReader(rc, 256))
|
|
rc.Close()
|
|
switch strings.TrimSpace(string(mt)) {
|
|
case "application/vnd.oasis.opendocument.text":
|
|
return "odt"
|
|
case "application/vnd.oasis.opendocument.spreadsheet":
|
|
return "ods"
|
|
case "application/vnd.oasis.opendocument.presentation":
|
|
return "odp"
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// toPDF turns a stored document of a converter type into a stored PDF and answers the PDF and its key. contentType is
|
|
// the media type a web server gave, or "".
|
|
func (w *deviceWorker) toPDF(ctx context.Context, objects Objects, key string, doc []byte, contentType string) ([]byte, string, error) {
|
|
typ := ConvertibleType(doc, contentType)
|
|
if typ == "" {
|
|
return nil, "", fmt.Errorf("the document is neither a PDF nor a JPEG, nor a type the converter takes (%s): nothing is converted or printed",
|
|
strings.Join(ConvertTypes, ", "))
|
|
}
|
|
signer, ok := objects.(Presigner)
|
|
if !ok {
|
|
return nil, "", errors.New("the bucket cannot sign a link, so the document cannot be converted")
|
|
}
|
|
if w.Convert == nil {
|
|
return nil, "", errors.New("this process has no way to call the document converter")
|
|
}
|
|
sum := sha256Hex(doc)
|
|
pdfKey := sum + ".pdf"
|
|
source, err := signer.Link(http.MethodGet, key, objectlink.MaxExpires)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
target, err := signer.Link(http.MethodPut, pdfKey, objectlink.MaxExpires)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
raw, err := w.Convert(map[string]any{"source": source, "type": typ, "target": target, "sha256": sum})
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("the document (%s) could not be converted: %w", typ, err)
|
|
}
|
|
// The converter answers within 25 s, the tool runner cutting every call at 30 s; a conversion not done by then is
|
|
// answered as converting with its ask id, and is read again with convert {ask}, each call under 25 s, for as long
|
|
// as the device process follows a conversion.
|
|
follow := w.ConvertFollow
|
|
if follow <= 0 {
|
|
follow = ConvertFollow
|
|
}
|
|
until := time.Now().Add(follow)
|
|
var ans struct {
|
|
SHA256 string `json:"sha256"`
|
|
Error string `json:"error"`
|
|
State string `json:"state"`
|
|
Ask string `json:"ask"`
|
|
}
|
|
for {
|
|
ans.State, ans.Ask, ans.SHA256, ans.Error = "", "", "", ""
|
|
if err := json.Unmarshal(raw, &ans); err != nil {
|
|
return nil, "", fmt.Errorf("the converter's answer is not JSON: %w", err)
|
|
}
|
|
if ans.State != "converting" {
|
|
break
|
|
}
|
|
if ans.Ask == "" || !time.Now().Before(until) {
|
|
return nil, "", fmt.Errorf("the document (%s) was still converting after %s (ask %s): nothing is printed", typ, follow, ans.Ask)
|
|
}
|
|
if raw, err = w.Convert(map[string]any{"ask": ans.Ask}); err != nil {
|
|
return nil, "", fmt.Errorf("the document (%s) could not be converted: %w", typ, err)
|
|
}
|
|
}
|
|
if ans.Error != "" {
|
|
return nil, "", fmt.Errorf("the document (%s) could not be converted: %s", typ, ans.Error)
|
|
}
|
|
pdf, err := objects.Get(ctx, pdfKey)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("the converter answered, but no PDF is kept as %s: %w", pdfKey, err)
|
|
}
|
|
if got := sha256Hex(pdf); !strings.EqualFold(got, ans.SHA256) {
|
|
return nil, "", fmt.Errorf("the converter says it put a PDF of SHA-256 %s, but the bucket holds one of %s as %s: nothing is printed", ans.SHA256, got, pdfKey)
|
|
}
|
|
if _, _, err := SniffFormat(pdf); err != nil {
|
|
return nil, "", fmt.Errorf("the converter's result %s: %w", pdfKey, err)
|
|
}
|
|
return pdf, pdfKey, nil
|
|
}
|