Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
194670c3bf | ||
|
|
c4222e0981 | ||
|
|
61dc4b8bb8 | ||
|
|
df357c1008 | ||
|
|
871b52a288 | ||
|
|
8a4a043e9f | ||
|
|
c0297aa061 | ||
|
|
2aaa2eeaff | ||
|
|
5f62e5bacf | ||
|
|
ab78559710 | ||
|
|
0d75bb1d3e | ||
|
|
6441062e7f | ||
|
|
4827a45f22 | ||
|
|
40eef457ab | ||
|
|
c22a4d109b | ||
|
|
571c3e04c4 | ||
|
|
f9fb97a572 | ||
|
|
e6484bb2d5 | ||
|
|
19f51fdc1f | ||
|
|
25f7ba3ee6 | ||
|
|
a3cd1ff4e1 | ||
|
|
f62c717f79 | ||
|
|
3ed99133d4 | ||
|
|
ab25cc331d | ||
|
|
2648992a9b | ||
|
|
7bcc3d081e | ||
|
|
affd1c45ca | ||
|
|
8c7458812d | ||
|
|
053908c772 | ||
|
|
d623736f9f | ||
|
|
a3e416ff68 | ||
|
|
8bf207b9f7 | ||
|
|
e1520d5e7e | ||
|
|
4f89c9f7c6 | ||
|
|
d34fb92211 | ||
|
|
3f7b006d16 | ||
|
|
a15cf33691 | ||
|
|
4c1699df6c | ||
|
|
404535c512 | ||
|
|
2cedcecc38 | ||
|
|
ba7b02d8d4 | ||
|
|
6aa3c16646 | ||
|
|
5a03e17e00 | ||
|
|
417d145b43 | ||
|
|
e5636a2afa | ||
|
|
5291d2d129 | ||
|
|
c6f6a87499 | ||
|
|
1ef21c6ed6 | ||
|
|
6ee1932cbf | ||
|
|
83cf43a314 | ||
|
|
52c14a79a1 | ||
|
|
152db0551d | ||
|
|
3a2467ccfe | ||
|
|
5a28f3597c | ||
|
|
9ce179f91b |
7
.github/CONTRIBUTING.md
vendored
@@ -1,6 +1,6 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Hi! Thank your for considering contributing to Gotenberg. You'll
|
Hi! Thank you for considering contributing to Gotenberg. You'll
|
||||||
find below useful information about how to contribute to the Gotenberg project.
|
find below useful information about how to contribute to the Gotenberg project.
|
||||||
|
|
||||||
## Contributing code
|
## Contributing code
|
||||||
@@ -8,8 +8,9 @@ find below useful information about how to contribute to the Gotenberg project.
|
|||||||
### Install from sources
|
### Install from sources
|
||||||
|
|
||||||
1. Install and run the latest version of Docker
|
1. Install and run the latest version of Docker
|
||||||
2. Fork this repository
|
2. Verify your Go version (>= 1.12)
|
||||||
3. Clone it to your local Go environment (requires *Go* >= 1.11)
|
3. Fork this repository
|
||||||
|
4. Clone it outside of your `GOPATH` (we're using Go modules)
|
||||||
|
|
||||||
### Working with git
|
### Working with git
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,15 @@ env:
|
|||||||
- secure: CmLh169xwt3OC7l19lDUwdZYfw+l/+XXKjTGxnuJiXHa9hAH+nc4mEUSmqHMHwjquyYE6LnSVGZ94LH8sCaGed/TMm0Lj3DZtZ0VsjYu3sKsNrVJn5WdYEah+GvNJ8i8uzvHnGk1L2fAagLpXFUB2K9UraA1CFiHs1fF5Fua7Ra0mE7lMz0MFvyvzcmc9u8nMz71K2YVsXqn+I/rv8onJCTtc/Ds0hVrBXwG5Fh0T3xHqjOhME9ZI2hRHnnQVdBojSY3USifQ//qAKowfBVc0pebEtgFpyXx/I17A6xCm0KVtnA2tNqu1+XFkjWaBzryaLE9ujhJcTRC/YK0JUZ+QxfYbD5C7jVy9Jlx1lOEjJ42B/ImNxPP+9p3wi2MWNbcbZJPxIe635fnT17lI7CLK2Qg1xjfTQ4WTEKW5IhnYMiyfRKiripFh4PCkDm8cJlTbpntsX1n0uheydbsgf/11vS7o1WGhVQ8+Jelc4DvIbBHCqienQEOLJDBwm7FiK5JbwZJ6ZAGeDM/IhGU1h9PNjVbHUyJhUxYoPYUE9QsZFT7ENCrRQpq49KOzXBRfffyJNJRh+Ep9X5YoW0dUp0MNu5B7z2+6w9X9n/C+loa11xF42bFzniCvvbSOQ37J+EngDinVxJkNPeyivR4fye2ENhIAjAEjouTJLOSXL9nAOU=
|
- secure: CmLh169xwt3OC7l19lDUwdZYfw+l/+XXKjTGxnuJiXHa9hAH+nc4mEUSmqHMHwjquyYE6LnSVGZ94LH8sCaGed/TMm0Lj3DZtZ0VsjYu3sKsNrVJn5WdYEah+GvNJ8i8uzvHnGk1L2fAagLpXFUB2K9UraA1CFiHs1fF5Fua7Ra0mE7lMz0MFvyvzcmc9u8nMz71K2YVsXqn+I/rv8onJCTtc/Ds0hVrBXwG5Fh0T3xHqjOhME9ZI2hRHnnQVdBojSY3USifQ//qAKowfBVc0pebEtgFpyXx/I17A6xCm0KVtnA2tNqu1+XFkjWaBzryaLE9ujhJcTRC/YK0JUZ+QxfYbD5C7jVy9Jlx1lOEjJ42B/ImNxPP+9p3wi2MWNbcbZJPxIe635fnT17lI7CLK2Qg1xjfTQ4WTEKW5IhnYMiyfRKiripFh4PCkDm8cJlTbpntsX1n0uheydbsgf/11vS7o1WGhVQ8+Jelc4DvIbBHCqienQEOLJDBwm7FiK5JbwZJ6ZAGeDM/IhGU1h9PNjVbHUyJhUxYoPYUE9QsZFT7ENCrRQpq49KOzXBRfffyJNJRh+Ep9X5YoW0dUp0MNu5B7z2+6w9X9n/C+loa11xF42bFzniCvvbSOQ37J+EngDinVxJkNPeyivR4fye2ENhIAjAEjouTJLOSXL9nAOU=
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- lint
|
- tests
|
||||||
- publish
|
- publish
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
- stage: lint
|
- stage: tests
|
||||||
script: make lint
|
script: make lint
|
||||||
|
- stage: tests
|
||||||
|
script: make tests
|
||||||
- stage: publish
|
- stage: publish
|
||||||
if: tag IS present
|
if: tag IS present
|
||||||
script: ./scripts/publish.sh $TRAVIS_TAG $DOCKER_USER $DOCKER_PASS
|
script: make publish VERSION=$TRAVIS_TAG DOCKER_USER=$DOCKER_USER DOCKER_PASSWORD=$DOCKER_PASS
|
||||||
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2018 TheCodingMachine
|
Copyright (c) 2019 TheCodingMachine
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
14
Makefile
@@ -1,13 +1,17 @@
|
|||||||
|
GOLANG_VERSION=1.12
|
||||||
VERSION=snapshot
|
VERSION=snapshot
|
||||||
GOLANG_VERSION=1.11.2
|
DOCKER_USER=
|
||||||
|
DOCKER_PASSWORD=
|
||||||
|
|
||||||
# generate documentation.
|
# generate documentation.
|
||||||
doc:
|
doc:
|
||||||
static-docs --in build/docs --out docs --theme gotenberg --title Gotenberg --subtitle "A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF."
|
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:docs -f build/docs/Dockerfile .
|
||||||
|
docker run --rm -it -v "$(PWD):/docs" thecodingmachine/gotenberg:docs
|
||||||
|
|
||||||
# gofmt and goimports all go files.
|
# gofmt and goimports all go files.
|
||||||
fmt:
|
fmt:
|
||||||
go fmt ./...
|
go fmt ./...
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
# run all linters.
|
# run all linters.
|
||||||
lint:
|
lint:
|
||||||
@@ -27,4 +31,8 @@ image:
|
|||||||
|
|
||||||
# start the API using previously built Docker image.
|
# start the API using previously built Docker image.
|
||||||
gotenberg:
|
gotenberg:
|
||||||
docker run -it --rm -p "3000:3000" thecodingmachine/gotenberg:$(VERSION)
|
docker run -it --rm -p "3000:3000" thecodingmachine/gotenberg:$(VERSION)
|
||||||
|
|
||||||
|
# publish Gotenberg images according to version.
|
||||||
|
publish:
|
||||||
|
./scripts/publish.sh $(GOLANG_VERSION) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)
|
||||||
15
README.md
@@ -1,8 +1,9 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://user-images.githubusercontent.com/8983173/49701110-4c8b9280-fbe8-11e8-895c-a4b9c7d2515b.png" alt="Gotenberg logo" width="250" height="250" />
|
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="250" height="250" />
|
||||||
</p>
|
</p>
|
||||||
<h3 align="center">Gotenberg</h3>
|
<h3 align="center">Gotenberg</h3>
|
||||||
<p align="center">A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</p>
|
<p align="center">A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</p>
|
||||||
|
<p align="center"><a href="https://thecodingmachine.github.io/gotenberg">Documentation</a> · <a href="/.github/CONTRIBUTING.md">Contributing</a></p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,18 +12,18 @@ At TheCodingMachine, we build a lot of web applications (intranets, extranets an
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* HTML and Markdown conversions using Google Chrome headless
|
* HTML and Markdown conversions using Google Chrome headless
|
||||||
* Office conversions (.docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
|
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
|
||||||
* Performance :zap:: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
|
* Performance :zap:: Google Chrome and LibreOffice (unoconv) started once in the background thanks to PM2
|
||||||
* Failure prevention :broken_heart:: PM2 automatically restarts previous processes if they fail
|
* Failure prevention :broken_heart:: PM2 automatically restarts previous processes if they fail
|
||||||
* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
||||||
* Easily interact with the API using our [Go](/pkg) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) (**WIP**) libraries
|
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Open a terminal and run the following command:
|
Open a terminal and run the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:3
|
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:5
|
||||||
```
|
```
|
||||||
|
|
||||||
The API is now available on your host at `http://localhost:3000`.
|
The API is now available on your host at `http://localhost:3000`.
|
||||||
@@ -32,9 +33,9 @@ to learn how to interact with it!
|
|||||||
|
|
||||||
## Badges
|
## Badges
|
||||||
|
|
||||||
[](https://microbadger.com/images/thecodingmachine/gotenberg:3)
|
[](https://microbadger.com/images/thecodingmachine/gotenberg:5)
|
||||||
[](https://travis-ci.org/thecodingmachine/gotenberg)
|
[](https://travis-ci.org/thecodingmachine/gotenberg)
|
||||||
[](https://godoc.org/github.com/thecodingmachine/gotenberg?status.svg)
|
[](https://godoc.org/github.com/thecodingmachine/gotenberg)
|
||||||
[](https://goreportcard.com/report/thecodingmachine/gotenberg)
|
[](https://goreportcard.com/report/thecodingmachine/gotenberg)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
FROM debian:stretch-slim
|
FROM thecodingmachine/gotenberg:3.2.0 AS hack
|
||||||
|
FROM debian:9.5-slim
|
||||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | Common libraries
|
# | Common libraries
|
||||||
@@ -11,7 +10,7 @@ LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
|||||||
|
|
||||||
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
|
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
|
||||||
apt-get update &&\
|
apt-get update &&\
|
||||||
apt-get install -y curl wget
|
apt-get install -y curl wget python3-pip ttf-mscorefonts-installer
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | PM2
|
# | PM2
|
||||||
@@ -21,25 +20,85 @@ RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free"
|
|||||||
# | recovering. In our case: Chrome (headless) and Office (headless).
|
# | recovering. In our case: Chrome (headless) and Office (headless).
|
||||||
# |
|
# |
|
||||||
|
|
||||||
RUN curl -sL https://raw.githubusercontent.com/Unitech/pm2/master/packager/setup.deb.sh | bash -
|
# Yep, this is dirty. The following script does not work anymore (see https://github.com/Unitech/pm2/issues/4127):
|
||||||
|
#RUN curl -sL https://raw.githubusercontent.com/Unitech/pm2/master/packager/setup.deb.sh | bash -
|
||||||
|
|
||||||
|
# Installing PM2 with Node.js and npm breaks something which prevents Google Chrome to work as expected.
|
||||||
|
# So we just copy all PM2 related files from a previous version of Gotenberg.
|
||||||
|
|
||||||
|
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - &&\
|
||||||
|
apt-get install -y nodejs
|
||||||
|
|
||||||
|
COPY --from=hack /usr/bin/pm2 /usr/bin/pm2
|
||||||
|
COPY --from=hack /usr/share/pm2 /usr/share/pm2
|
||||||
|
COPY --from=hack /etc/default/pm2 /etc/default/pm2
|
||||||
|
COPY --from=hack /etc/systemd/system/pm2.service /etc/systemd/system/pm2.service
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | Chrome
|
# | Chrome
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# |
|
# |
|
||||||
# | Installs Chrome for HTML and Markdown conversions.
|
# | Installs Chrome.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
|
RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\
|
||||||
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\
|
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\
|
||||||
apt-get update &&\
|
apt-get update &&\
|
||||||
apt-get -y install google-chrome-stable
|
apt-get -y --allow-unauthenticated install google-chrome-stable
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | Unoconv
|
# | Unoconv
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# |
|
# |
|
||||||
# | Installs MS fonts and unoconv for Office documents conversions.
|
# | Installs unoconv and LibreOffice.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
RUN apt-get -y install ttf-mscorefonts-installer unoconv
|
RUN pip3 install unoconv &&\
|
||||||
|
# https://github.com/nextcloud/docker/issues/380
|
||||||
|
mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
|
||||||
|
apt-get -y install libreoffice
|
||||||
|
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# | PDFtk
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# |
|
||||||
|
# | Installs PDFtk as an alternative to pdfcpu for merging PDFs.
|
||||||
|
# | https://github.com/thecodingmachine/gotenberg/issues/29
|
||||||
|
# |
|
||||||
|
|
||||||
|
RUN apt-get -y install pdftk
|
||||||
|
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# | Fonts
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# |
|
||||||
|
# | Installs a handful of fonts.
|
||||||
|
# | Note: ttf-mscorefonts-installer are installed on top of this Dockerfile.
|
||||||
|
# |
|
||||||
|
|
||||||
|
# Credits:
|
||||||
|
# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile
|
||||||
|
# https://help.accusoft.com/PrizmDoc/v12.1/HTML/Installing_Asian_Fonts_on_Ubuntu_and_Debian.html
|
||||||
|
RUN apt-get install -y \
|
||||||
|
culmus \
|
||||||
|
fonts-beng \
|
||||||
|
fonts-hosny-amiri \
|
||||||
|
fonts-lklug-sinhala \
|
||||||
|
fonts-lohit-guru \
|
||||||
|
fonts-lohit-knda \
|
||||||
|
fonts-samyak-gujr \
|
||||||
|
fonts-samyak-mlym \
|
||||||
|
fonts-samyak-taml \
|
||||||
|
fonts-sarai \
|
||||||
|
fonts-sil-abyssinica \
|
||||||
|
fonts-sil-padauk \
|
||||||
|
fonts-telu \
|
||||||
|
fonts-thai-tlwg \
|
||||||
|
ttf-liberation \
|
||||||
|
ttf-wqy-zenhei \
|
||||||
|
fonts-arphic-uming \
|
||||||
|
fonts-ipafont-mincho \
|
||||||
|
fonts-ipafont-gothic \
|
||||||
|
fonts-unfonts-core
|
||||||
|
|
||||||
|
COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
|
||||||
|
|||||||
29
build/base/fonts.conf
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version='1.0'?>
|
||||||
|
<!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
|
||||||
|
<fontconfig>
|
||||||
|
<match target="font">
|
||||||
|
<edit mode="assign" name="rgba">
|
||||||
|
<const>rgb</const>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
<match target="font">
|
||||||
|
<edit mode="assign" name="hinting">
|
||||||
|
<bool>true</bool>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
<match target="font">
|
||||||
|
<edit mode="assign" name="hintstyle">
|
||||||
|
<const>hintslight</const>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
<match target="font">
|
||||||
|
<edit mode="assign" name="antialias">
|
||||||
|
<bool>true</bool>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
<match target="font">
|
||||||
|
<edit mode="assign" name="lcdfilter">
|
||||||
|
<const>lcddefault</const>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
</fontconfig>
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
---
|
|
||||||
title: HTML
|
|
||||||
---
|
|
||||||
|
|
||||||
Gotenberg provides the endpoint `/convert/html` for HTML conversions.
|
|
||||||
|
|
||||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
|
||||||
|
|
||||||
## Basic
|
|
||||||
|
|
||||||
The only requirement is to send a file named `index.html`: it is the file
|
|
||||||
which will be converted to PDF.
|
|
||||||
|
|
||||||
For instance:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>My PDF</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Hello world!</h1>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://localhost:3000/convert/html \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@index.html
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Header and footer
|
|
||||||
|
|
||||||
You may also add a header and/or a footer in the resulting PDF.
|
|
||||||
Respectively, a file named `header.html` and `footer.html`.
|
|
||||||
|
|
||||||
Each of them **has to be a complete HTML document**:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-size: 8rem;
|
|
||||||
margin: 4rem auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>
|
|
||||||
<span class="pageNumber"></span> of <span class="totalPages"></span>
|
|
||||||
</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
The following classes allow you to inject printing values:
|
|
||||||
|
|
||||||
* `date`: formatted print date
|
|
||||||
* `title`: document title
|
|
||||||
* `pageNumber`: current page number
|
|
||||||
* `totalPage`: total pages in the document
|
|
||||||
|
|
||||||
> **Attention:** the CSS properties are independant of the ones used in the `index.html` file.
|
|
||||||
> Also, `footer.html` CSS properties override the ones from `header.html`.
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://localhost:3000/convert/html \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@index.html
|
|
||||||
--form files=@header.html
|
|
||||||
--form files=@footer.html
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
Options: &gotenberg.HTMLOptions{
|
|
||||||
HeaderFilePath: "header.html",
|
|
||||||
FooterFilePath: "footer.html",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Assets
|
|
||||||
|
|
||||||
You may also send additional files. For instance, images, fonts, stylesheets and so on.
|
|
||||||
|
|
||||||
The only requirement is to make sure that their paths
|
|
||||||
are on the same level as the `index.html` file.
|
|
||||||
|
|
||||||
In others words, this will work:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>My PDF</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Hello world!</h1>
|
|
||||||
<img src="img.png">
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
But this won't:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>My PDF</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Hello world!</h1>
|
|
||||||
<img src="/foo/img.png">
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
You may also use *remote* paths for Google fonts, images and so on.
|
|
||||||
|
|
||||||
> If you want to install fonts directly in the Gotenberg Docker image,
|
|
||||||
> see to the [fonts section](#office.fonts).
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://localhost:3000/convert/html \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@index.html
|
|
||||||
--form files=@style.css
|
|
||||||
--form files=@img.png
|
|
||||||
--form files=@font.woff
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
AssetFilePaths: []string{
|
|
||||||
"style.css",
|
|
||||||
"img.png",
|
|
||||||
"font.woff",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Paper size, margins, orientation
|
|
||||||
|
|
||||||
You may also customize the resulting PDF paper.
|
|
||||||
|
|
||||||
By default, it will be rendered in `A4` size with `1 inch` margins and `portrait` orientation.
|
|
||||||
|
|
||||||
> Paper size and margins have to be provided in `inches`.
|
|
||||||
> Also, you have to set both `paperWidth` and `paperHeight`. Same for margins.
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://localhost:3000/convert/html \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@index.html
|
|
||||||
--form paperWidth=8.27
|
|
||||||
--form paperHeight=11.27
|
|
||||||
--form marginTop=0
|
|
||||||
--form marginBottom=0
|
|
||||||
--form marginLeft=0
|
|
||||||
--form marginRight=0
|
|
||||||
--form landscape=true
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
Options: &gotenberg.HTMLOptions{
|
|
||||||
PaperSize: gotenberg.A4,
|
|
||||||
PaperMargins: gotenberg.NoMargins,
|
|
||||||
Landscape: true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
---
|
|
||||||
title: Office
|
|
||||||
---
|
|
||||||
|
|
||||||
Gotenberg provides the endpoint `/convert/office` for Office documents conversions.
|
|
||||||
|
|
||||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
|
||||||
|
|
||||||
## Basic
|
|
||||||
|
|
||||||
You may send one or more Office documents. Following file extensions are accepted:
|
|
||||||
|
|
||||||
* `.doc`
|
|
||||||
* `.docx`
|
|
||||||
* `.odt`
|
|
||||||
* `.xls`
|
|
||||||
* `.xlsx`
|
|
||||||
* `.ods`
|
|
||||||
* `.ppt`
|
|
||||||
* `.pptx`
|
|
||||||
* `.odp`
|
|
||||||
|
|
||||||
All files will be merged into a single resulting PDF.
|
|
||||||
|
|
||||||
> **Attention:** currently, `unoconv` cannot perform concurrent conversions.
|
|
||||||
> That's why for Office conversions, the API does only one conversion at a time.
|
|
||||||
> See the [scalability section](#scalability) to find how to mitigate this issue.
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://localhost:3000/convert/office \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@document.docx
|
|
||||||
--form files=@document2.docx
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.OfficeRequest{
|
|
||||||
FilePaths: []string{
|
|
||||||
"document.docx",
|
|
||||||
"document2.docx",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Fonts
|
|
||||||
|
|
||||||
By default, only `ttf-mscorefonts` fonts are installed.
|
|
||||||
|
|
||||||
If you wish to use more fonts, you will have to create your own image:
|
|
||||||
|
|
||||||
```Dockerfile
|
|
||||||
FROM thecodingmachine/gotenberg:3
|
|
||||||
|
|
||||||
RUN apt-get -y install yourfonts
|
|
||||||
```
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
title: Merge
|
|
||||||
---
|
|
||||||
|
|
||||||
Gotenberg provides the endpoint `/merge` for merging PDFs.
|
|
||||||
|
|
||||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
|
||||||
|
|
||||||
## Basic
|
|
||||||
|
|
||||||
Nothing special here: you may send one or more PDF files and the API
|
|
||||||
will merge them and return the resulting PDF file.
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://localhost:3000/merge \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@file.pdf
|
|
||||||
--form files=@file2.pdf
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.MergeRequest{
|
|
||||||
FilePaths: []string{
|
|
||||||
"file.pdf",
|
|
||||||
"file2.pdf",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
title: Webhook
|
|
||||||
---
|
|
||||||
|
|
||||||
All endpoints accept a form field named `webhookURL`.
|
|
||||||
|
|
||||||
If provided, the API will send the resulting PDF file in a `POST` request with the `application/pdf` Content-Type
|
|
||||||
to given URL.
|
|
||||||
|
|
||||||
By doing so, your requests to the API will be done before the conversions are actually done!
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Guzzle
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ curl --request POST \
|
|
||||||
--url http://localhost:3000/convert/html \
|
|
||||||
--header 'Content-Type: multipart/form-data' \
|
|
||||||
--form files=@index.html
|
|
||||||
--form webhookURL='http://myapp.com/webhook/'
|
|
||||||
> result.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
Options: &gotenberg.HTMLOptions{
|
|
||||||
WebHookURL: "http://myapp.com/webhook/",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
|
||||||
resp, err := c.Post(req)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP
|
|
||||||
|
|
||||||
TODO
|
|
||||||
24
build/docs/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
ARG GOLANG_VERSION
|
||||||
|
|
||||||
|
FROM golang:${GOLANG_VERSION}-stretch
|
||||||
|
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# | static
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# |
|
||||||
|
# | Installs the static site anti-framework – general-purpose library,
|
||||||
|
# | purpose-built commands for various domains.
|
||||||
|
# |
|
||||||
|
|
||||||
|
RUN go get github.com/apex/static/cmd/static-docs
|
||||||
|
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# | Final touch
|
||||||
|
# |--------------------------------------------------------------------------
|
||||||
|
# |
|
||||||
|
# | Last instructions of this build.
|
||||||
|
# |
|
||||||
|
|
||||||
|
WORKDIR /docs
|
||||||
|
|
||||||
|
CMD [ "static-docs", "--in", "build/docs/content", "--out", "docs", "--theme", "gotenberg", "--title", "Gotenberg", "--subtitle", "A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF." ]
|
||||||
@@ -5,8 +5,8 @@ title: Introduction
|
|||||||
[Gotenberg](https://github.com/thecodingmachine/gotenberg/) is a Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.
|
[Gotenberg](https://github.com/thecodingmachine/gotenberg/) is a Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.
|
||||||
|
|
||||||
* HTML and Markdown conversions using Google Chrome headless
|
* HTML and Markdown conversions using Google Chrome headless
|
||||||
* Office conversions (.docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
|
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
|
||||||
* Performance: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
|
* Performance: Google Chrome and LibreOffice (unoconv) started once in the background thanks to PM2
|
||||||
* Failure prevention: PM2 automatically restarts previous processes if they fail
|
* Failure prevention: PM2 automatically restarts previous processes if they fail
|
||||||
* Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
* Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
|
||||||
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg/pkg) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
|
||||||
@@ -7,12 +7,14 @@ Gotenberg is shipped within a Docker image.
|
|||||||
You may start it with:
|
You may start it with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:3
|
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:5
|
||||||
```
|
```
|
||||||
|
|
||||||
> The API will be available at [http://localhost:3000](http://localhost:3000).
|
> The API will be available at [http://localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
Or add it in your Docker Compose stack:
|
## Docker Compose
|
||||||
|
|
||||||
|
You may also add it in your Docker Compose stack:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3'
|
version: '3'
|
||||||
@@ -22,32 +24,19 @@ services:
|
|||||||
# your others services
|
# your others services
|
||||||
|
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: thecodingmachine/gotenberg:3
|
image: thecodingmachine/gotenberg:5
|
||||||
```
|
```
|
||||||
|
|
||||||
> The API will be available under `gotenberg:3000` in your Docker Compose network.
|
> The API will be available under `gotenberg:3000` in your Docker Compose network.
|
||||||
|
|
||||||
|
## Kubernetes
|
||||||
|
|
||||||
It may also be deployed with Kubernetes.
|
It may also be deployed with Kubernetes.
|
||||||
|
|
||||||
|
Make sure to provide enough memory and CPU requests (for instance `512Mi` and `0.2` CPU).
|
||||||
|
Otherwise the API will not be able to launch Google Chrome and LibreOffice (unoconv).
|
||||||
|
|
||||||
|
> The more resources are granted, the quicker will be the conversions.
|
||||||
|
|
||||||
In the following examples, we will assume your
|
In the following examples, we will assume your
|
||||||
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
|
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
|
||||||
|
|
||||||
## Go client
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ go get -u github.com/thecodingmachine/gotenberg
|
|
||||||
```
|
|
||||||
|
|
||||||
## PHP client
|
|
||||||
|
|
||||||
Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ composer require php-http/guzzle6-adapter
|
|
||||||
```
|
|
||||||
|
|
||||||
Then the PHP client:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ composer require thecodingmachine/gotenberg-php-client
|
|
||||||
```
|
|
||||||
25
build/docs/content/02-clients.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: Clients
|
||||||
|
---
|
||||||
|
|
||||||
|
We provide clients in various languages for easing the interactions with the API.
|
||||||
|
|
||||||
|
## Go client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v5
|
||||||
|
```
|
||||||
|
|
||||||
|
## PHP client
|
||||||
|
|
||||||
|
Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ composer require php-http/guzzle6-adapter
|
||||||
|
```
|
||||||
|
|
||||||
|
Then the PHP client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ composer require thecodingmachine/gotenberg-php-client
|
||||||
|
```
|
||||||
33
build/docs/content/03-environment-variables.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
title: Environment variables
|
||||||
|
---
|
||||||
|
|
||||||
|
You may customize the API behaviour thanks to environment variables.
|
||||||
|
|
||||||
|
## Disable Google Chrome
|
||||||
|
|
||||||
|
In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME`.
|
||||||
|
|
||||||
|
It takes the strings `"0"` or `"1"` as value.
|
||||||
|
|
||||||
|
> If Google Chrome is disabled, the following conversions will **not** be available anymore:
|
||||||
|
> [HTML](#html), [URL](#url) and [Markdown](#markdown)
|
||||||
|
|
||||||
|
|
||||||
|
## Disable LibreOffice (unoconv)
|
||||||
|
|
||||||
|
You may also disable LibreOffice (unoconv) with `DISABLE_UNOCONV`.
|
||||||
|
|
||||||
|
> If LibreOffice (unoconv) is disabled, the following conversion will **not** be available anymore:
|
||||||
|
> [Office](#office)
|
||||||
|
|
||||||
|
## Default wait timeout
|
||||||
|
|
||||||
|
By default, the API will wait 10 seconds before it considers the conversion to be unsuccessful.
|
||||||
|
|
||||||
|
You may customize this timeout thanks to the environment variable `DEFAULT_WAIT_TIMEOUT`.
|
||||||
|
|
||||||
|
It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds).
|
||||||
|
|
||||||
|
> The default timeout may also be overridden per request thanks to the form field `waitTimeout`.
|
||||||
|
> See the [timeout section](#timeout).
|
||||||
342
build/docs/content/04-html.md
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
---
|
||||||
|
title: HTML
|
||||||
|
---
|
||||||
|
|
||||||
|
Gotenberg provides the endpoint `/convert/html` for HTML conversions.
|
||||||
|
|
||||||
|
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||||
|
|
||||||
|
## Basic
|
||||||
|
|
||||||
|
The only requirement is to send a file named `index.html`: it is the file
|
||||||
|
which will be converted to PDF.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>My PDF</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello world!</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/html \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@index.html \
|
||||||
|
-o result.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
|
dest := "result.pdf"
|
||||||
|
c.Store(req, dest)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TheCodingMachine\Gotenberg\Client;
|
||||||
|
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||||
|
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||||
|
|
||||||
|
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||||
|
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||||
|
$request = new HTMLRequest($index);
|
||||||
|
$dirPath = "/foo";
|
||||||
|
$filename = $client->store($request, $dirPath);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Header and footer
|
||||||
|
|
||||||
|
You may also add a header and/or a footer in the resulting PDF.
|
||||||
|
Respectively, a file named `header.html` and `footer.html`.
|
||||||
|
|
||||||
|
Each of them **has to be a complete HTML document**:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-size: 8rem;
|
||||||
|
margin: 4rem auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
<span class="pageNumber"></span> of <span class="totalPages"></span>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
The following classes allow you to inject printing values:
|
||||||
|
|
||||||
|
* `date`: formatted print date
|
||||||
|
* `title`: document title
|
||||||
|
* `pageNumber`: current page number
|
||||||
|
* `totalPage`: total pages in the document
|
||||||
|
|
||||||
|
> **Attention:** the CSS properties are independant of the ones used in the `index.html` file.
|
||||||
|
> Also, `footer.html` CSS properties override the ones from `header.html`.
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/html \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@index.html \
|
||||||
|
--form files=@header.html \
|
||||||
|
--form files=@footer.html \
|
||||||
|
-o result.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
|
req.Header("header.html")
|
||||||
|
req.Footer("footer.html")
|
||||||
|
dest := "result.pdf"
|
||||||
|
c.Store(req, dest)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TheCodingMachine\Gotenberg\Client;
|
||||||
|
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||||
|
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||||
|
|
||||||
|
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||||
|
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||||
|
$header = DocumentFactory::makeFromPath('header.html', 'header.html');
|
||||||
|
$footer = DocumentFactory::makeFromPath('footer.html', 'footer.html');
|
||||||
|
$request = new HTMLRequest($index);
|
||||||
|
$request->setHeader($header);
|
||||||
|
$request->setFooter($footer);
|
||||||
|
$dirPath = "/foo";
|
||||||
|
$filename = $client->store($request, $dirPath);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
You may also send additional files. For instance: images, fonts, stylesheets and so on.
|
||||||
|
|
||||||
|
The only requirement is to make sure that their paths
|
||||||
|
are on the same level as the `index.html` file.
|
||||||
|
|
||||||
|
In others words, this will work:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>My PDF</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello world!</h1>
|
||||||
|
<img src="img.png">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
But this won't:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>My PDF</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello world!</h1>
|
||||||
|
<img src="/foo/img.png">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
You may also use *remote* paths for Google fonts, images and so on.
|
||||||
|
|
||||||
|
> If you want to install fonts directly in the Gotenberg Docker image,
|
||||||
|
> see to the [fonts section](#fonts).
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/html \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@index.html \
|
||||||
|
--form files=@style.css \
|
||||||
|
--form files=@img.png \
|
||||||
|
--form files=@font.woff \
|
||||||
|
-o result.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
|
req.Assets("font.woff", "img.gif", "style.css")
|
||||||
|
dest := "result.pdf"
|
||||||
|
c.Store(req, dest)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TheCodingMachine\Gotenberg\Client;
|
||||||
|
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||||
|
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||||
|
|
||||||
|
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||||
|
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||||
|
$assets = [
|
||||||
|
DocumentFactory::makeFromPath('style.css', 'style.css'),
|
||||||
|
DocumentFactory::makeFromPath('img.png', 'img.png'),
|
||||||
|
DocumentFactory::makeFromPath('font.woff', 'font.woff'),
|
||||||
|
];
|
||||||
|
$request = new HTMLRequest($index);
|
||||||
|
$request->setAssets($assets);
|
||||||
|
$dest = "result.pdf";
|
||||||
|
$client->store($request, $dest);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Paper size, margins, orientation
|
||||||
|
|
||||||
|
You may also customize the resulting PDF format.
|
||||||
|
|
||||||
|
By default, it will be rendered with `A4` size, `1 inch` margins and `portrait` orientation.
|
||||||
|
|
||||||
|
> Paper size and margins have to be provided in `inches`. Same for margins.
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/html \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@index.html \
|
||||||
|
--form paperWidth=8.27 \
|
||||||
|
--form paperHeight=11.27 \
|
||||||
|
--form marginTop=0 \
|
||||||
|
--form marginBottom=0 \
|
||||||
|
--form marginLeft=0 \
|
||||||
|
--form marginRight=0 \
|
||||||
|
--form landscape=true \
|
||||||
|
-o result.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
|
req.PaperSize(gotenberg.A4)
|
||||||
|
req.Margins(gotenberg.NoMargins)
|
||||||
|
req.Landscape(true)
|
||||||
|
dest := "result.pdf"
|
||||||
|
c.Store(req, dest)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TheCodingMachine\Gotenberg\Client;
|
||||||
|
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||||
|
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||||
|
use TheCodingMachine\Gotenberg\Request;
|
||||||
|
|
||||||
|
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||||
|
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||||
|
$request = new HTMLRequest($index);
|
||||||
|
$request->setPaperSize(Request::A4);
|
||||||
|
$request->setMargins(Request::NO_MARGINS);
|
||||||
|
$request->setLandscape(true);
|
||||||
|
$dest = "result.pdf";
|
||||||
|
$client->store($request, $dest);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wait delay
|
||||||
|
|
||||||
|
In some cases, you may want to wait a certain amount of time to make sure the
|
||||||
|
page you're trying to generate is fully rendered.
|
||||||
|
|
||||||
|
> The wait delay is a duration in **seconds** (e.g `2.5` for 2.5 seconds).
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/html \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@index.html \
|
||||||
|
--form waitDelay=5.5 \
|
||||||
|
-o result.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
|
req.WaitDelay(5.5)
|
||||||
|
dest := "result.pdf"
|
||||||
|
c.Store(req, dest)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TheCodingMachine\Gotenberg\Client;
|
||||||
|
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||||
|
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||||
|
use TheCodingMachine\Gotenberg\Request;
|
||||||
|
|
||||||
|
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||||
|
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||||
|
$request = new HTMLRequest($index);
|
||||||
|
$request->setWaitDelay(5.5);
|
||||||
|
$dest = "result.pdf";
|
||||||
|
$client->store($request, $dest);
|
||||||
|
```
|
||||||
56
build/docs/content/05-url.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
title: URL
|
||||||
|
---
|
||||||
|
|
||||||
|
Gotenberg provides the endpoint `/convert/url` for remote URL conversions.
|
||||||
|
|
||||||
|
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||||
|
|
||||||
|
## Basic
|
||||||
|
|
||||||
|
This endpoint does not accept an `index.html` file nor assets files but a form field
|
||||||
|
named `remoteURL` instead. Otherwise, URL conversions work the same as HTML conversions.
|
||||||
|
|
||||||
|
> **Attention:** when converting a website to PDF, you should remove all margins.
|
||||||
|
> If not, some of the content of the page might be hidden.
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/url \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form remoteURL=https://google.com \
|
||||||
|
--form marginTop=0 \
|
||||||
|
--form marginBottom=0 \
|
||||||
|
--form marginLeft=0 \
|
||||||
|
--form marginRight=0 \
|
||||||
|
-o result.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req := gotenberg.NewURLRequest("https://google.com")
|
||||||
|
req.Margins(gotenberg.NoMargins)
|
||||||
|
dest := "result.pdf"
|
||||||
|
c.Store(req, dest)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TheCodingMachine\Gotenberg\Client;
|
||||||
|
use TheCodingMachine\Gotenberg\URLRequest;
|
||||||
|
|
||||||
|
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||||
|
$request = new URLRequest('https://google.com');
|
||||||
|
$request->setMargins(Request::NO_MARGINS);
|
||||||
|
$dest = "result.pdf";
|
||||||
|
$client->store($request, $dest);
|
||||||
|
```
|
||||||
@@ -11,7 +11,7 @@ It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
|||||||
Markdown conversions work the same as HTML conversions.
|
Markdown conversions work the same as HTML conversions.
|
||||||
|
|
||||||
Only difference is that you have access to the Go template function `toHTML`
|
Only difference is that you have access to the Go template function `toHTML`
|
||||||
in the file `index.html` which will convert a given markdown file to HTML.
|
in the file `index.html`. This function will convert a given markdown file to HTML.
|
||||||
|
|
||||||
For instance:
|
For instance:
|
||||||
|
|
||||||
@@ -28,30 +28,25 @@ For instance:
|
|||||||
</html>
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Guzzle
|
### cURL
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ curl --request POST \
|
$ curl --request POST \
|
||||||
--url http://localhost:3000/convert/markdown \
|
--url http://localhost:3000/convert/markdown \
|
||||||
--header 'Content-Type: multipart/form-data' \
|
--header 'Content-Type: multipart/form-data' \
|
||||||
--form files=@index.html
|
--form files=@index.html \
|
||||||
--form files=@file.md
|
--form files=@file.md \
|
||||||
> result.pdf
|
-o result.pdf
|
||||||
```
|
```
|
||||||
|
|
||||||
### Go
|
### Go
|
||||||
|
|
||||||
```golang
|
```golang
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
req := &gotenberg.MarkdownRequest{
|
req, _ := gotenberg.NewMarkdownRequest("index.html", "file.md")
|
||||||
IndexFilePath: "index.html",
|
|
||||||
MarkdownFilePaths: []string{
|
|
||||||
"file.md",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "result.pdf"
|
dest := "result.pdf"
|
||||||
c.Store(req, dest)
|
c.Store(req, dest)
|
||||||
}
|
}
|
||||||
@@ -59,4 +54,17 @@ func main() {
|
|||||||
|
|
||||||
### PHP
|
### PHP
|
||||||
|
|
||||||
TODO
|
```php
|
||||||
|
use TheCodingMachine\Gotenberg\Client;
|
||||||
|
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||||
|
use TheCodingMachine\Gotenberg\MarkdownRequest;
|
||||||
|
|
||||||
|
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||||
|
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||||
|
$markdowns = [
|
||||||
|
DocumentFactory::makeFromPath('file.md', 'file.md'),
|
||||||
|
];
|
||||||
|
$request = new MarkdownRequest($index, $markdowns);
|
||||||
|
$dest = "result.pdf";
|
||||||
|
$client->store($request, $dest);
|
||||||
|
```
|
||||||
119
build/docs/content/07-office.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
---
|
||||||
|
title: Office
|
||||||
|
---
|
||||||
|
|
||||||
|
Gotenberg provides the endpoint `/convert/office` for Office document conversions.
|
||||||
|
|
||||||
|
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||||
|
|
||||||
|
## Basic
|
||||||
|
|
||||||
|
You may send one or more Office documents. Following file extensions are accepted:
|
||||||
|
|
||||||
|
* `.txt`
|
||||||
|
* `.rtf`
|
||||||
|
* `.fodt`
|
||||||
|
* `.doc`
|
||||||
|
* `.docx`
|
||||||
|
* `.odt`
|
||||||
|
* `.xls`
|
||||||
|
* `.xlsx`
|
||||||
|
* `.ods`
|
||||||
|
* `.ppt`
|
||||||
|
* `.pptx`
|
||||||
|
* `.odp`
|
||||||
|
|
||||||
|
All files will be merged into a single resulting PDF.
|
||||||
|
|
||||||
|
> **Attention:** currently, `unoconv` cannot perform concurrent conversions.
|
||||||
|
> That's why for Office conversions, the API does only one conversion at a time.
|
||||||
|
> See the [scalability section](#scalability) to find how to mitigate this issue.
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/office \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@document.docx \
|
||||||
|
--form files=@document2.docx \
|
||||||
|
-o result.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewOfficeRequest("document.docx", "document2.docx")
|
||||||
|
dest := "result.pdf"
|
||||||
|
c.Store(req, dest)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TheCodingMachine\Gotenberg\Client;
|
||||||
|
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||||
|
use TheCodingMachine\Gotenberg\OfficeRequest;
|
||||||
|
|
||||||
|
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||||
|
$files = [
|
||||||
|
DocumentFactory::makeFromPath('document.docx', 'document.docx'),
|
||||||
|
DocumentFactory::makeFromPath('document2.docx', 'document2.docx'),
|
||||||
|
];
|
||||||
|
$request = new OfficeRequest($files);
|
||||||
|
$dest = "result.pdf";
|
||||||
|
$client->store($request, $dest);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Orientation
|
||||||
|
|
||||||
|
You may also customize the resulting PDF format.
|
||||||
|
|
||||||
|
By default, it will be rendered with `portrait` orientation.
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/office \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@document.docx \
|
||||||
|
--form landscape=true \
|
||||||
|
-o result.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewOfficeRequest("document.docx")
|
||||||
|
req.Landscape(true)
|
||||||
|
dest := "result.pdf"
|
||||||
|
c.Store(req, dest)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TheCodingMachine\Gotenberg\Client;
|
||||||
|
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||||
|
use TheCodingMachine\Gotenberg\OfficeRequest;
|
||||||
|
|
||||||
|
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||||
|
$files = [
|
||||||
|
DocumentFactory::makeFromPath('document.docx', 'document.docx'),
|
||||||
|
];
|
||||||
|
$request = new OfficeRequest($files);
|
||||||
|
$request->setLandscape(true);
|
||||||
|
$dest = "result.pdf";
|
||||||
|
$client->store($request, $dest);
|
||||||
|
```
|
||||||
55
build/docs/content/08-merge.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
title: Merge
|
||||||
|
---
|
||||||
|
|
||||||
|
Gotenberg provides the endpoint `/convert/merge` for merging PDFs.
|
||||||
|
|
||||||
|
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||||
|
|
||||||
|
## Basic
|
||||||
|
|
||||||
|
Nothing fancy here: you may send one or more PDF files and the API
|
||||||
|
will merge them and return the resulting PDF file.
|
||||||
|
|
||||||
|
> **Attention:** Gotenberg merges the PDF files alphabetically.
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/merge \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@file.pdf \
|
||||||
|
--form files=@file2.pdf \
|
||||||
|
-o result.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewMergeRequest("file.pdf", "file2.pdf")
|
||||||
|
dest := "result.pdf"
|
||||||
|
c.Store(req, dest)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TheCodingMachine\Gotenberg\Client;
|
||||||
|
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||||
|
use TheCodingMachine\Gotenberg\MergeRequest;
|
||||||
|
|
||||||
|
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||||
|
$files = [
|
||||||
|
DocumentFactory::makeFromPath('file.pdf', 'file.pdf'),
|
||||||
|
DocumentFactory::makeFromPath('file2.pdf', 'file2.pdf'),
|
||||||
|
];
|
||||||
|
$request = new MergeRequest($files);
|
||||||
|
$dest = "result.pdf";
|
||||||
|
$client->store($request, $dest);
|
||||||
|
```
|
||||||
52
build/docs/content/09-timeout.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
title: Timeout
|
||||||
|
---
|
||||||
|
|
||||||
|
All endpoints accept a form field named `waitTimeout`.
|
||||||
|
|
||||||
|
The API will wait the given **seconds** before it considers the conversion to be unsucessful.
|
||||||
|
|
||||||
|
It takes a float as value (e.g `2.5` for 2.5 seconds).
|
||||||
|
|
||||||
|
> You may also define this value globally: see the [environment variables](#environment_variables.default_wait_timeout) section.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/html \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@index.html \
|
||||||
|
--form waitTimeout=2.5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
|
req.WaitTimeout(2.5)
|
||||||
|
resp, _ := c.Post(req)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TheCodingMachine\Gotenberg\Client;
|
||||||
|
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||||
|
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||||
|
use TheCodingMachine\Gotenberg\Request;
|
||||||
|
|
||||||
|
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||||
|
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||||
|
$request = new HTMLRequest($index);
|
||||||
|
$request->setWaitTimeout(2.5);
|
||||||
|
$dest = "result.pdf";
|
||||||
|
$client->store($request, $dest);
|
||||||
|
```
|
||||||
49
build/docs/content/10-webhook.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
title: Webhook
|
||||||
|
---
|
||||||
|
|
||||||
|
All endpoints accept a form field named `webhookURL`.
|
||||||
|
|
||||||
|
If provided, the API will send the resulting PDF file in a `POST` request with the `application/pdf` Content-Type
|
||||||
|
to given URL.
|
||||||
|
|
||||||
|
By doing so, your requests to the API will be over before the conversions are actually done!
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/html \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@index.html \
|
||||||
|
--form webhookURL='http://myapp.com/webhook/'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
|
req.WebhookURL("http://myapp.com/webhook/")
|
||||||
|
resp, _ := c.Post(req)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TheCodingMachine\Gotenberg\Client;
|
||||||
|
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||||
|
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||||
|
|
||||||
|
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||||
|
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||||
|
$request = new HTMLRequest($index);
|
||||||
|
$request->setWebhookURL('http://myapp.com/webhook/');
|
||||||
|
$resp = $client->post($request);
|
||||||
|
```
|
||||||
50
build/docs/content/11-result-filename.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
title: Result filename
|
||||||
|
---
|
||||||
|
|
||||||
|
All endpoints accept a form field named `resultFilename`.
|
||||||
|
|
||||||
|
If provided, the API will return the resulting PDF file with the given filename.
|
||||||
|
Otherwise a random filename is used.
|
||||||
|
|
||||||
|
> **Attention:** this feature does not work if the form field `webhookURL` is given.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ curl --request POST \
|
||||||
|
--url http://localhost:3000/convert/html \
|
||||||
|
--header 'Content-Type: multipart/form-data' \
|
||||||
|
--form files=@index.html \
|
||||||
|
--form resultFilename='foo.pdf'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Go
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||||
|
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||||
|
req.ResultFilename("foo.pdf")
|
||||||
|
resp, _ := c.Post(req)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PHP
|
||||||
|
|
||||||
|
```php
|
||||||
|
use TheCodingMachine\Gotenberg\Client;
|
||||||
|
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||||
|
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||||
|
use TheCodingMachine\Gotenberg\Request;
|
||||||
|
|
||||||
|
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||||
|
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||||
|
$request = new HTMLRequest($index);
|
||||||
|
$request->setResultFilename('foo.pdf');
|
||||||
|
$resp = $client->post($request);
|
||||||
|
```
|
||||||
@@ -14,7 +14,7 @@ services:
|
|||||||
# your others services
|
# your others services
|
||||||
|
|
||||||
gotenberg:
|
gotenberg:
|
||||||
image: thecodingmachine/gotenberg:3
|
image: thecodingmachine/gotenberg:5
|
||||||
```
|
```
|
||||||
|
|
||||||
You may now launch your services using:
|
You may now launch your services using:
|
||||||
11
build/docs/content/13-ping.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
title: Ping
|
||||||
|
---
|
||||||
|
|
||||||
|
Gotenberg provides the endpoint `/ping` for checking the API availability with
|
||||||
|
a simple `GET` request.
|
||||||
|
|
||||||
|
This feature is especially useful for liveness/readiness probes in Kubernetes:
|
||||||
|
|
||||||
|
* [Pod lifecycle](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes)
|
||||||
|
* [Configure Liveness and Readiness Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/)
|
||||||
13
build/docs/content/14-fonts.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
title: Fonts
|
||||||
|
---
|
||||||
|
|
||||||
|
By default, a handful of fonts are installed. Asian characters are also supported out of the box.
|
||||||
|
|
||||||
|
If you wish to use more fonts, you will have to create your own image:
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
FROM thecodingmachine/gotenberg:5
|
||||||
|
|
||||||
|
RUN apt-get -y install yourfonts
|
||||||
|
```
|
||||||
@@ -2,8 +2,6 @@ ARG GOLANG_VERSION
|
|||||||
|
|
||||||
FROM golang:${GOLANG_VERSION}-stretch
|
FROM golang:${GOLANG_VERSION}-stretch
|
||||||
|
|
||||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | GolangCI-Lint
|
# | GolangCI-Lint
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
@@ -12,7 +10,7 @@ LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
|||||||
# | than gometalinter.
|
# | than gometalinter.
|
||||||
# |
|
# |
|
||||||
|
|
||||||
ENV GOLANGCI_LINT_VERSION 1.12.3
|
ENV GOLANGCI_LINT_VERSION 1.16.0
|
||||||
|
|
||||||
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /usr/local/bin v${GOLANGCI_LINT_VERSION} &&\
|
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /usr/local/bin v${GOLANGCI_LINT_VERSION} &&\
|
||||||
golangci-lint --version
|
golangci-lint --version
|
||||||
@@ -34,4 +32,4 @@ COPY go.sum .
|
|||||||
# Install module dependencies.
|
# Install module dependencies.
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=lll", "--disable=errcheck", "--disable=gosec", "--disable=gochecknoglobals", "--disable=gochecknoinits" ]
|
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl" ]
|
||||||
@@ -4,8 +4,6 @@ FROM golang:${GOLANG_VERSION}-stretch AS golang
|
|||||||
|
|
||||||
FROM thecodingmachine/gotenberg:base
|
FROM thecodingmachine/gotenberg:base
|
||||||
|
|
||||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
|
||||||
|
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
# | Common libraries
|
# | Common libraries
|
||||||
# |--------------------------------------------------------------------------
|
# |--------------------------------------------------------------------------
|
||||||
@@ -47,4 +45,4 @@ COPY go.sum .
|
|||||||
# Install module dependencies.
|
# Install module dependencies.
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
ENTRYPOINT ["build/tests/docker-entrypoint.sh"]
|
ENTRYPOINT [ "build/tests/docker-entrypoint.sh" ]
|
||||||
@@ -5,20 +5,13 @@ set -xe
|
|||||||
# Testing PM2 processes launch separatly for avoiding
|
# Testing PM2 processes launch separatly for avoiding
|
||||||
# spending to much time on each tests depending on
|
# spending to much time on each tests depending on
|
||||||
# them.
|
# them.
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeLaunch
|
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeStart
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvLaunch
|
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvStart
|
||||||
|
|
||||||
# Running others tests.
|
# Running others tests.
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api
|
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/printer
|
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/rand
|
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/rand
|
||||||
|
|
||||||
# Finally testing processes shutdown.
|
# Finally testing processes shutdown.
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeShutdown
|
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeShutdown
|
||||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvShutdown
|
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvShutdown
|
||||||
|
|
||||||
# Testing Go client.
|
|
||||||
go build -o /usr/local/bin/gotenberg cmd/gotenberg/main.go
|
|
||||||
gotenberg &
|
|
||||||
sleep 10
|
|
||||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/pkg
|
|
||||||
@@ -1,21 +1,125 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/app/api"
|
"github.com/thecodingmachine/gotenberg/internal/app/api"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// version will be set on build time.
|
// version will be set on build time.
|
||||||
|
// nolint: gochecknoglobals
|
||||||
var version = "snapshot"
|
var version = "snapshot"
|
||||||
|
|
||||||
func main() {
|
const (
|
||||||
notify.Println(fmt.Sprintf("Gotenberg %s", version))
|
defaultWaitTimeoutEnvVar = "DEFAULT_WAIT_TIMEOUT"
|
||||||
if err := api.Start(); err != nil {
|
disableGoogleChromeEnvVar = "DISABLE_GOOGLE_CHROME"
|
||||||
notify.ErrPrintln(err)
|
disableUnoconvEnvVar = "DISABLE_UNOCONV"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustParseEnvVar() *api.Options {
|
||||||
|
opts := api.DefaultOptions()
|
||||||
|
if os.Getenv(defaultWaitTimeoutEnvVar) != "" {
|
||||||
|
defaultWaitTimeout, err := strconv.ParseFloat(os.Getenv(defaultWaitTimeoutEnvVar), 64)
|
||||||
|
if err != nil {
|
||||||
|
notify.ErrPrint(fmt.Errorf("%s: wrong value: want float got %v", defaultWaitTimeoutEnvVar, err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
opts.DefaultWaitTimeout = defaultWaitTimeout
|
||||||
|
}
|
||||||
|
if v, ok := os.LookupEnv(disableGoogleChromeEnvVar); ok {
|
||||||
|
if v != "1" && v != "0" {
|
||||||
|
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", defaultWaitTimeoutEnvVar, v))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
opts.EnableChromeEndpoints = v != "1"
|
||||||
|
}
|
||||||
|
if v, ok := os.LookupEnv(disableUnoconvEnvVar); ok {
|
||||||
|
if v != "1" && v != "0" {
|
||||||
|
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", disableUnoconvEnvVar, v))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
opts.EnableUnoconvEndpoints = v != "1"
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustStartProcesses(opts *api.Options) []pm2.Process {
|
||||||
|
var processes []pm2.Process
|
||||||
|
if opts.EnableChromeEndpoints {
|
||||||
|
processes = append(processes, pm2.NewChrome())
|
||||||
|
}
|
||||||
|
if opts.EnableUnoconvEndpoints {
|
||||||
|
processes = append(processes, pm2.NewUnoconv())
|
||||||
|
}
|
||||||
|
for _, p := range processes {
|
||||||
|
notify.Printf("starting %s with PM2...", p.Fullname())
|
||||||
|
if err := p.Start(); err != nil {
|
||||||
|
notify.ErrPrint(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return processes
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustStartAPI(srv *echo.Echo) {
|
||||||
|
notify.Print("http server started on port 3000")
|
||||||
|
if err := srv.Start(":3000"); err != nil {
|
||||||
|
if err != http.ErrServerClosed {
|
||||||
|
notify.ErrPrint(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustShutdownProcesses(processes []pm2.Process) {
|
||||||
|
for _, p := range processes {
|
||||||
|
notify.Printf("shutting down %s with PM2... (Ctrl+C to force)", p.Fullname())
|
||||||
|
if err := p.Shutdown(); err != nil {
|
||||||
|
notify.ErrPrint(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustShutdownAPI(srv *echo.Echo) {
|
||||||
|
// create a deadline to wait for.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
// doesn't block if no connections, but will otherwise wait
|
||||||
|
// until the timeout deadline.
|
||||||
|
notify.Print("shutting down http server... (Ctrl+C to force)")
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
notify.ErrPrint(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
notify.Printf("Gotenberg %s", version)
|
||||||
|
opts := mustParseEnvVar()
|
||||||
|
srv := api.New(opts)
|
||||||
|
processes := mustStartProcesses(opts)
|
||||||
|
// run our API in a goroutine so that it doesn't block.s
|
||||||
|
go func() {
|
||||||
|
mustStartAPI(srv)
|
||||||
|
}()
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
||||||
|
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
|
||||||
|
signal.Notify(quit, os.Interrupt)
|
||||||
|
// block until we receive our signal.
|
||||||
|
<-quit
|
||||||
|
mustShutdownAPI(srv)
|
||||||
|
mustShutdownProcesses(processes)
|
||||||
|
notify.Print("bye!")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|||||||
922
docs/index.html
BIN
docs/theme/gotenberg/img/apple-touch-icon-114x114.png
vendored
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-120x120.png
vendored
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-144x144.png
vendored
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-152x152.png
vendored
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-57x57.png
vendored
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-60x60.png
vendored
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-72x72.png
vendored
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-76x76.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/theme/gotenberg/img/favicon-128.png
vendored
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/theme/gotenberg/img/favicon-16x16.png
vendored
|
Before Width: | Height: | Size: 923 B After Width: | Height: | Size: 917 B |
BIN
docs/theme/gotenberg/img/favicon-196x196.png
vendored
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/theme/gotenberg/img/favicon-32x32.png
vendored
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
BIN
docs/theme/gotenberg/img/favicon-96x96.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/theme/gotenberg/img/favicon.ico
vendored
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 34 KiB |
BIN
docs/theme/gotenberg/img/mstile-144x144.png
vendored
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/theme/gotenberg/img/mstile-150x150.png
vendored
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
docs/theme/gotenberg/img/mstile-310x150.png
vendored
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
docs/theme/gotenberg/img/mstile-310x310.png
vendored
Normal file
|
After Width: | Height: | Size: 372 KiB |
BIN
docs/theme/gotenberg/img/mstile-70x70.png
vendored
Normal file
|
After Width: | Height: | Size: 24 KiB |
24
docs/theme/gotenberg/views/index.html
vendored
@@ -1,12 +1,32 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-10196481-9"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', 'UA-10196481-9');
|
||||||
|
</script>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{{.Title}} · {{.Subtitle}}</title>
|
<title>{{.Title}} · {{.Subtitle}}</title>
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="theme/gotenberg/img/apple-touch-icon-57x57.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="theme/gotenberg/img/apple-touch-icon-114x114.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="theme/gotenberg/img/apple-touch-icon-72x72.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="theme/gotenberg/img/apple-touch-icon-144x144.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="theme/gotenberg/img/apple-touch-icon-60x60.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="theme/gotenberg/img/apple-touch-icon-120x120.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="theme/gotenberg/img/apple-touch-icon-76x76.png" />
|
||||||
|
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="theme/gotenberg/img/apple-touch-icon-152x152.png" />
|
||||||
|
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-196x196.png" sizes="196x196" />
|
||||||
|
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-96x96.png" sizes="96x96" />
|
||||||
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-32x32.png" sizes="32x32" />
|
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-32x32.png" sizes="32x32" />
|
||||||
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-16x16.png" sizes="16x16" />
|
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-16x16.png" sizes="16x16" />
|
||||||
|
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-128.png" sizes="128x128" />
|
||||||
<link rel="stylesheet" href="theme/gotenberg/css/index.css">
|
<link rel="stylesheet" href="theme/gotenberg/css/index.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -14,7 +34,7 @@
|
|||||||
<div class="Container">
|
<div class="Container">
|
||||||
<div class="Header">
|
<div class="Header">
|
||||||
<div class="Title center">
|
<div class="Title center">
|
||||||
<img src="https://user-images.githubusercontent.com/8983173/49701110-4c8b9280-fbe8-11e8-895c-a4b9c7d2515b.png" alt="Gotenberg logo" width="250" height="250" />
|
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="250" height="250" />
|
||||||
<span class="text">{{.Title}}</span>
|
<span class="text">{{.Title}}</span>
|
||||||
<span class="subtext">{{.Subtitle}}</span>
|
<span class="subtext">{{.Subtitle}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
24
go.mod
@@ -1,21 +1,23 @@
|
|||||||
module github.com/thecodingmachine/gotenberg
|
module github.com/thecodingmachine/gotenberg
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/derekparker/delve v1.1.0
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/go-cmp v0.2.0 // indirect
|
github.com/google/go-cmp v0.2.0 // indirect
|
||||||
github.com/gorilla/websocket v1.4.0 // indirect
|
github.com/gorilla/websocket v1.4.0 // indirect
|
||||||
github.com/hhrutter/pdfcpu v0.1.18
|
github.com/labstack/echo/v4 v4.0.0
|
||||||
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73
|
|
||||||
github.com/labstack/gommon v0.2.8
|
github.com/labstack/gommon v0.2.8
|
||||||
github.com/mafredri/cdp v0.20.0
|
github.com/mafredri/cdp v0.22.0
|
||||||
|
github.com/mattn/go-colorable v0.1.1 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.7 // indirect
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1
|
github.com/microcosm-cc/bluemonday v1.0.1
|
||||||
github.com/pkg/errors v0.8.0 // indirect
|
|
||||||
github.com/russross/blackfriday/v2 v2.0.1
|
github.com/russross/blackfriday/v2 v2.0.1
|
||||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
|
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
|
||||||
github.com/stretchr/objx v0.1.1 // indirect
|
github.com/stretchr/testify v1.3.0
|
||||||
github.com/stretchr/testify v1.2.2
|
github.com/valyala/fasttemplate v1.0.1 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc
|
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc // indirect
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f
|
||||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect
|
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
46
go.sum
@@ -1,53 +1,53 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/derekparker/delve v1.1.0 h1:icd65nMp7s2HiLz6y/6RCVXBdoED3xxYLwX09EMaRCc=
|
|
||||||
github.com/derekparker/delve v1.1.0/go.mod h1:pMSZMfp0Nhbm8qdZJkuE/yPGOkLpGXLS1I4poXQpuJU=
|
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/hhrutter/pdfcpu v0.1.18 h1:GX3d6tZxmQlmB5X9VUyOtx7+HZZpR+8ISLqBineP6dc=
|
github.com/labstack/echo/v4 v4.0.0 h1:q1GH+caIXPP7H2StPIdzy/ez9CO0EepqYeUg6vi9SWM=
|
||||||
github.com/hhrutter/pdfcpu v0.1.18/go.mod h1:iaCmXGnOXPIvoGLELZZc4m/GpIopVsvLQ00thrbu8LU=
|
github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno=
|
||||||
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73 h1:oQvA3Xy8274W5Ldgo6Nogy4lX8XO5AaIBsjF5Ggk0IU=
|
|
||||||
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73/go.mod h1:rS0D1UPvC8/3sXjhSwEq+K1olh7ipbDhjDWATN2KSgA=
|
|
||||||
github.com/labstack/echo v3.3.5+incompatible h1:9PfxPUmasKzeJor9uQTaXLT6WUG/r+vSTmvXxvv3JO4=
|
|
||||||
github.com/labstack/echo v3.3.5+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
|
||||||
github.com/labstack/gommon v0.2.7/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
|
||||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
||||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||||
github.com/mafredri/cdp v0.20.0 h1:dRq16FwXCAAbolR4W2iyRNsL14Bp6jFHLbRp2EkICVs=
|
github.com/mafredri/cdp v0.22.0 h1:BV17j8hXLDWczo2SZIAFuOjMpQMIOq5DOcd9sgB2hv0=
|
||||||
github.com/mafredri/cdp v0.20.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
github.com/mafredri/cdp v0.22.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
||||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
|
||||||
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
|
||||||
|
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
|
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
|
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
|
||||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
|
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
|
||||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
||||||
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
|
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc h1:4gbWbmmPFp4ySWICouJl6emP0MyS31yy9SrTlAGFT+g=
|
||||||
|
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
|||||||
@@ -1,125 +1,48 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"github.com/labstack/echo/v4"
|
||||||
"fmt"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/labstack/echo/middleware"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start starts the API server on port 3000.
|
// Options allows to customize the behaviour
|
||||||
func Start() error {
|
// of the API.
|
||||||
e := setup()
|
type Options struct {
|
||||||
// start Chrome headless and
|
DefaultWaitTimeout float64
|
||||||
// unoconv listener with PM2.
|
EnableChromeEndpoints bool
|
||||||
chrome := &pm2.Chrome{}
|
EnableUnoconvEndpoints bool
|
||||||
unoconv := &pm2.Unoconv{}
|
|
||||||
if err := chrome.Launch(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := unoconv.Launch(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// run our API in a goroutine so that it doesn't block.
|
|
||||||
go func() {
|
|
||||||
notify.Println("http server started on port 3000")
|
|
||||||
if err := e.Start(":3000"); err != nil {
|
|
||||||
e.Logger.Fatalf("%v", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
quit := make(chan os.Signal, 1)
|
|
||||||
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
|
||||||
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
|
|
||||||
signal.Notify(quit, os.Interrupt)
|
|
||||||
// block until we receive our signal.
|
|
||||||
<-quit
|
|
||||||
// create a deadline to wait for.
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
// doesn't block if no connections, but will otherwise wait
|
|
||||||
// until the timeout deadline.
|
|
||||||
notify.Println("shutting down http server... (Ctrl+C to force)")
|
|
||||||
return e.Shutdown(ctx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup() *echo.Echo {
|
// DefaultOptions returns default options.
|
||||||
e := echo.New()
|
func DefaultOptions() *Options {
|
||||||
e.HideBanner = true
|
return &Options{
|
||||||
e.HidePort = true
|
DefaultWaitTimeout: 10,
|
||||||
e.Use(middleware.Logger())
|
EnableChromeEndpoints: true,
|
||||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
EnableUnoconvEndpoints: true,
|
||||||
return func(c echo.Context) error {
|
}
|
||||||
if err := next(c); err != nil {
|
|
||||||
// TODO should return a better HTTP status code
|
|
||||||
// than 500 for some cases.
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("%v", err))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
})
|
|
||||||
e.POST("/merge", merge)
|
|
||||||
g := e.Group("/convert")
|
|
||||||
g.POST("/html", convertHTML)
|
|
||||||
g.POST("/markdown", convertMarkdown)
|
|
||||||
g.POST("/office", convertOffice)
|
|
||||||
return e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newContext(r *resource) (context.Context, context.CancelFunc) {
|
// New returns an API.
|
||||||
webhookURL := r.webhookURL()
|
func New(opts *Options) *echo.Echo {
|
||||||
if webhookURL == "" {
|
api := echo.New()
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
api.HideBanner = true
|
||||||
return ctx, cancel
|
api.HidePort = true
|
||||||
|
api.Use(middleware.Logger())
|
||||||
|
api.GET("/ping", func(c echo.Context) error { return nil })
|
||||||
|
g := api.Group("/convert")
|
||||||
|
g.Use(handleContext(opts))
|
||||||
|
g.Use(handleError())
|
||||||
|
g.POST("/merge", merge)
|
||||||
|
if !opts.EnableChromeEndpoints && !opts.EnableUnoconvEndpoints {
|
||||||
|
return api
|
||||||
}
|
}
|
||||||
return context.Background(), nil
|
if opts.EnableChromeEndpoints {
|
||||||
}
|
g.POST("/html", convertHTML)
|
||||||
|
g.POST("/url", convertURL)
|
||||||
func print(c echo.Context, p printer.Printer, r *resource) error {
|
g.POST("/markdown", convertMarkdown)
|
||||||
baseFilename, err := rand.Get()
|
}
|
||||||
if err != nil {
|
if opts.EnableUnoconvEndpoints {
|
||||||
return fmt.Errorf("getting result file name: %v", err)
|
g.POST("/office", convertOffice)
|
||||||
}
|
}
|
||||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
return api
|
||||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
|
||||||
if r.webhookURL() == "" {
|
|
||||||
// if no webhook URL given, run conversion
|
|
||||||
// and directly return the resulting PDF file
|
|
||||||
// or and error.
|
|
||||||
if err := p.Print(fpath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.Attachment(fpath, filename)
|
|
||||||
}
|
|
||||||
// as a webhook URL has been given, we
|
|
||||||
// run the following lines in a goroutine so that
|
|
||||||
// it doesn't block.
|
|
||||||
go func() {
|
|
||||||
if err := p.Print(fpath); err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f, err := os.Open(fpath)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
128
internal/app/api/api_test.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultWaitTimeout(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.DefaultWaitTimeout = 0
|
||||||
|
srv := New(opts)
|
||||||
|
// testing if timeout.
|
||||||
|
body, contentType := test.URLTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||||
|
// testing if no timeout.
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "10"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisableChromeEndpoints(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.EnableChromeEndpoints = false
|
||||||
|
srv := New(opts)
|
||||||
|
// Ping.
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Merge.
|
||||||
|
body, contentType := test.PDFTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// HTML.
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
// Markdown.
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
// URL.
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
// Office.
|
||||||
|
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisableUnoconvEndpoints(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.EnableUnoconvEndpoints = false
|
||||||
|
srv := New(opts)
|
||||||
|
// Ping.
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Merge.
|
||||||
|
body, contentType := test.PDFTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// HTML.
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Markdown.
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// URL.
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Office.
|
||||||
|
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
}
|
||||||
|
func TestDisableChromeAndUnoconvEndpoints(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.EnableChromeEndpoints = false
|
||||||
|
opts.EnableUnoconvEndpoints = false
|
||||||
|
srv := New(opts)
|
||||||
|
// Ping.
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Merge.
|
||||||
|
body, contentType := test.PDFTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// HTML.
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
// Markdown.
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
// URL.
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
// Office.
|
||||||
|
body, contentType = test.OfficeTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusNotFound, srv, req)
|
||||||
|
}
|
||||||
@@ -1,9 +1,2 @@
|
|||||||
/*
|
// Package api helps managing the HTTP server behind Gotenberg.
|
||||||
Package api starts a HTTP server on port 3000.
|
|
||||||
|
|
||||||
It accepts POST requests with a multipart/form-data Content-Type
|
|
||||||
for converting HTML, Markdown and Office documents to PDF.
|
|
||||||
|
|
||||||
It is also able to merge a list of PDF files.
|
|
||||||
*/
|
|
||||||
package api
|
package api
|
||||||
|
|||||||
157
internal/app/api/handler.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
type errBadRequest struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *errBadRequest) Error() string {
|
||||||
|
return e.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func merge(c echo.Context) error {
|
||||||
|
ctx := c.(*resourceContext)
|
||||||
|
opts, err := ctx.resource.mergePrinterOptions()
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
fpaths, err := ctx.resource.fpaths(".pdf")
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
p := printer.NewMerge(fpaths, opts)
|
||||||
|
return convert(ctx, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertHTML(c echo.Context) error {
|
||||||
|
ctx := c.(*resourceContext)
|
||||||
|
opts, err := ctx.resource.chromePrinterOptions()
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
fpath, err := ctx.resource.fpath("index.html")
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
p := printer.NewHTML(fpath, opts)
|
||||||
|
return convert(ctx, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertMarkdown(c echo.Context) error {
|
||||||
|
ctx := c.(*resourceContext)
|
||||||
|
opts, err := ctx.resource.chromePrinterOptions()
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
fpath, err := ctx.resource.fpath("index.html")
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
p, err := printer.NewMarkdown(fpath, opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return convert(ctx, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertURL(c echo.Context) error {
|
||||||
|
ctx := c.(*resourceContext)
|
||||||
|
opts, err := ctx.resource.chromePrinterOptions()
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
remote, err := ctx.resource.get(remoteURL)
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
p := printer.NewURL(remote, opts)
|
||||||
|
return convert(ctx, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertOffice(c echo.Context) error {
|
||||||
|
ctx := c.(*resourceContext)
|
||||||
|
opts, err := ctx.resource.officePrinterOptions()
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
fpaths, err := ctx.resource.fpaths(
|
||||||
|
".txt",
|
||||||
|
".rtf",
|
||||||
|
".fodt",
|
||||||
|
".doc",
|
||||||
|
".docx",
|
||||||
|
".odt",
|
||||||
|
".xls",
|
||||||
|
".xlsx",
|
||||||
|
".ods",
|
||||||
|
".ppt",
|
||||||
|
".pptx",
|
||||||
|
".odp",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
p := printer.NewOffice(fpaths, opts)
|
||||||
|
return convert(ctx, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convert(ctx *resourceContext, p printer.Printer) error {
|
||||||
|
baseFilename, err := rand.Get()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
||||||
|
fpath := fmt.Sprintf("%s/%s", ctx.resource.formFilesDirPath, filename)
|
||||||
|
// if no webhook URL given, run conversion
|
||||||
|
// and directly return the resulting PDF file
|
||||||
|
// or an error.
|
||||||
|
if !ctx.resource.has(webhookURL) {
|
||||||
|
if err := p.Print(fpath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ctx.resource.has(resultFilename) {
|
||||||
|
filename, err = ctx.resource.get(resultFilename)
|
||||||
|
if err != nil {
|
||||||
|
return &errBadRequest{err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx.Attachment(fpath, filename)
|
||||||
|
}
|
||||||
|
// as a webhook URL has been given, we
|
||||||
|
// run the following lines in a goroutine so that
|
||||||
|
// it doesn't block.
|
||||||
|
go func() {
|
||||||
|
defer ctx.resource.close() // nolint: errcheck
|
||||||
|
if err := p.Print(fpath); err != nil {
|
||||||
|
ctx.Logger().Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f, err := os.Open(fpath)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Logger().Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close() // nolint: errcheck
|
||||||
|
webhook, err := ctx.resource.get(webhookURL)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Logger().Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := http.Post(webhook, "application/pdf", f) /* #nosec */
|
||||||
|
if err != nil {
|
||||||
|
ctx.Logger().Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close() // nolint: errcheck
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
360
internal/app/api/handler_test.go
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/thecodingmachine/gotenberg/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMerge(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
// OK.
|
||||||
|
body, contentType := test.PDFTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Bad request.
|
||||||
|
body, contentType = test.PDFTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
// Timeout.
|
||||||
|
body, contentType = test.PDFTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTML(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
// OK.
|
||||||
|
body, contentType := test.HTMLTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Bad request.
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
// Timeout.
|
||||||
|
body, contentType = test.HTMLTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarkdown(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
// OK.
|
||||||
|
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Bad request.
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
// Timeout.
|
||||||
|
body, contentType = test.MarkdownTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestURL(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
// OK.
|
||||||
|
body, contentType := test.URLTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Bad request.
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitDelay: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{paperWidth: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{paperHeight: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginTop: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginBottom: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginLeft: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{marginRight: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
// Timeout.
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOffice(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
// OK.
|
||||||
|
body, contentType := test.OfficeTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
// Bad request.
|
||||||
|
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{waitTimeout: "not a float"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{landscape: "not a bool"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
body, contentType = test.URLTestMultipartForm(t, nil)
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusBadRequest, srv, req)
|
||||||
|
// Timeout.
|
||||||
|
body, contentType = test.OfficeTestMultipartForm(t, map[string]string{waitTimeout: "0"})
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusRequestTimeout, srv, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrent(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
opts.DefaultWaitTimeout = 30
|
||||||
|
srv := New(opts)
|
||||||
|
// Merge.
|
||||||
|
test.AssertConcurrent(
|
||||||
|
t,
|
||||||
|
func() error {
|
||||||
|
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
// HTML.
|
||||||
|
test.AssertConcurrent(
|
||||||
|
t,
|
||||||
|
func() error {
|
||||||
|
body, contentType := test.HTMLTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
// Markdown.
|
||||||
|
test.AssertConcurrent(
|
||||||
|
t,
|
||||||
|
func() error {
|
||||||
|
body, contentType := test.MarkdownTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
// URL.
|
||||||
|
test.AssertConcurrent(
|
||||||
|
t,
|
||||||
|
func() error {
|
||||||
|
body, contentType := test.URLTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/url", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
// Office.
|
||||||
|
test.AssertConcurrent(
|
||||||
|
t,
|
||||||
|
func() error {
|
||||||
|
body, contentType := test.OfficeTestMultipartForm(t, nil)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
return fmt.Errorf("wrong status code: want %d got %d", http.StatusOK, rec.Code)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebhook(t *testing.T) {
|
||||||
|
status := make(chan error, 2)
|
||||||
|
rcv := echo.New()
|
||||||
|
rcv.POST("/foo", func(c echo.Context) error {
|
||||||
|
if c.Request().Header.Get("Content-type") != "application/pdf" {
|
||||||
|
status <- fmt.Errorf("wrong Content-type: got %s want %s", c.Request().Header.Get("Content-type"), "application/pdf")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
body, err := ioutil.ReadAll(c.Request().Body)
|
||||||
|
if err != nil {
|
||||||
|
status <- err
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if body == nil || len(body) == 0 {
|
||||||
|
status <- errors.New("empty body")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
status <- nil
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
go func() {
|
||||||
|
rcv.Start(":3001")
|
||||||
|
}()
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
body, contentType := test.PDFTestMultipartForm(t, map[string]string{webhookURL: "http://localhost:3001/foo"})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
test.AssertStatusCode(t, http.StatusOK, srv, req)
|
||||||
|
err := <-status
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultFilename(t *testing.T) {
|
||||||
|
opts := DefaultOptions()
|
||||||
|
srv := New(opts)
|
||||||
|
body, contentType := test.PDFTestMultipartForm(t, map[string]string{resultFilename: "foo.pdf"})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/convert/merge", body)
|
||||||
|
req.Header.Set(echo.HeaderContentType, contentType)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rec, req)
|
||||||
|
assert.Equal(t, "attachment; filename=\"foo.pdf\"", rec.Header().Get("Content-Disposition"))
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func convertHTML(c echo.Context) error {
|
|
||||||
r, err := newResource(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer r.removeAll()
|
|
||||||
ctx, cancel := newContext(r)
|
|
||||||
if cancel != nil {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
p := &printer.HTML{Context: ctx}
|
|
||||||
indexPath, err := r.filePath("index.html")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.WithLocalURL(indexPath)
|
|
||||||
headerPath, _ := r.filePath("header.html")
|
|
||||||
if err := p.WithHeaderFile(headerPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
footerPath, _ := r.filePath("footer.html")
|
|
||||||
if err := p.WithFooterFile(footerPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
paperSize, err := r.paperSize()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.PaperWidth = paperSize[0]
|
|
||||||
p.PaperHeight = paperSize[1]
|
|
||||||
paperMargins, err := r.paperMargins()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.MarginTop = paperMargins[0]
|
|
||||||
p.MarginBottom = paperMargins[1]
|
|
||||||
p.MarginLeft = paperMargins[2]
|
|
||||||
p.MarginRight = paperMargins[3]
|
|
||||||
landscape, err := r.landscape()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.Landscape = landscape
|
|
||||||
return print(c, p, r)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHTML(t *testing.T) {
|
|
||||||
body, contentType := test.HTMLMultipartForm(t)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e := echo.New()
|
|
||||||
c := e.NewContext(req, rec)
|
|
||||||
assert.NoError(t, convertHTML(c))
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func convertMarkdown(c echo.Context) error {
|
|
||||||
r, err := newResource(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer r.removeAll()
|
|
||||||
ctx, cancel := newContext(r)
|
|
||||||
if cancel != nil {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
indexPath, err := r.filePath("index.html")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p := &printer.Markdown{Context: ctx, TemplatePath: indexPath}
|
|
||||||
headerPath, _ := r.filePath("header.html")
|
|
||||||
if err := p.WithHeaderFile(headerPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
footerPath, _ := r.filePath("footer.html")
|
|
||||||
if err := p.WithFooterFile(footerPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
paperSize, err := r.paperSize()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.PaperWidth = paperSize[0]
|
|
||||||
p.PaperHeight = paperSize[1]
|
|
||||||
paperMargins, err := r.paperMargins()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.MarginTop = paperMargins[0]
|
|
||||||
p.MarginBottom = paperMargins[1]
|
|
||||||
p.MarginLeft = paperMargins[2]
|
|
||||||
p.MarginRight = paperMargins[3]
|
|
||||||
landscape, err := r.landscape()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.Landscape = landscape
|
|
||||||
return print(c, p, r)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMarkdown(t *testing.T) {
|
|
||||||
body, contentType := test.MarkdownMultipartForm(t)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e := echo.New()
|
|
||||||
c := e.NewContext(req, rec)
|
|
||||||
assert.NoError(t, convertMarkdown(c))
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
func merge(c echo.Context) error {
|
|
||||||
r, err := newResource(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer r.removeAll()
|
|
||||||
fpaths, err := r.filePaths([]string{".pdf"})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
baseFilename, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting result file name: %v", err)
|
|
||||||
}
|
|
||||||
filename := fmt.Sprintf("%s.pdf", baseFilename)
|
|
||||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
|
||||||
if r.webhookURL() == "" {
|
|
||||||
// if no webhook URL given, run merge
|
|
||||||
// and directly return the resulting PDF file
|
|
||||||
// or and error.
|
|
||||||
if err := printer.Merge(fpaths, fpath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.Attachment(fpath, filename)
|
|
||||||
}
|
|
||||||
// as a webhook URL has been given, we
|
|
||||||
// run the following lines in a goroutine so that
|
|
||||||
// it doesn't block.
|
|
||||||
go func() {
|
|
||||||
if err := printer.Merge(fpaths, fpath); err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
f, err := os.Open(fpath)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger().Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
|
||||||
body, contentType := test.PDFMultipartForm(t)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/merge", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e := echo.New()
|
|
||||||
c := e.NewContext(req, rec)
|
|
||||||
assert.NoError(t, merge(c))
|
|
||||||
}
|
|
||||||
61
internal/app/api/middleware.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleContext(opts *Options) echo.MiddlewareFunc {
|
||||||
|
// middleware for extending default context with our
|
||||||
|
// custom constext.
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
ctx := &resourceContext{c, opts, nil}
|
||||||
|
r, err := newResource(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if resourceErr := r.close(); resourceErr != nil {
|
||||||
|
c.Logger().Error(resourceErr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.resource = r
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleError() echo.MiddlewareFunc {
|
||||||
|
// middleware for handling errors and removing resources
|
||||||
|
// once the request has been handled.
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c echo.Context) error {
|
||||||
|
err := next(c)
|
||||||
|
ctx := c.(*resourceContext)
|
||||||
|
// if a webhookURL has been given,
|
||||||
|
// do not remove the resources here because
|
||||||
|
// we don't know if the result file has been
|
||||||
|
// generated or sent.
|
||||||
|
if !ctx.resource.has(webhookURL) {
|
||||||
|
if resourceErr := ctx.resource.close(); resourceErr != nil {
|
||||||
|
c.Logger().Error(resourceErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(*echo.HTTPError); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, ok := err.(*errBadRequest); ok {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) {
|
||||||
|
return echo.NewHTTPError(http.StatusRequestTimeout)
|
||||||
|
}
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
|
||||||
)
|
|
||||||
|
|
||||||
var officeExts = []string{
|
|
||||||
".doc",
|
|
||||||
".docx",
|
|
||||||
".odt",
|
|
||||||
".xls",
|
|
||||||
".xlsx",
|
|
||||||
".ods",
|
|
||||||
".ppt",
|
|
||||||
".pptx",
|
|
||||||
".odp",
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertOffice(c echo.Context) error {
|
|
||||||
r, err := newResource(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer r.removeAll()
|
|
||||||
ctx, cancel := newContext(r)
|
|
||||||
if cancel != nil {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
fpaths, err := r.filePaths(officeExts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(fpaths) == 0 {
|
|
||||||
return errors.New("no suitable office documents to convert")
|
|
||||||
}
|
|
||||||
p := &printer.Office{Context: ctx, FilePaths: fpaths}
|
|
||||||
return print(c, p, r)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestOffice(t *testing.T) {
|
|
||||||
body, contentType := test.OfficeMultipartForm(t)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
|
||||||
req.Header.Set(echo.HeaderContentType, contentType)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
e := echo.New()
|
|
||||||
c := e.NewContext(req, rec)
|
|
||||||
assert.NoError(t, convertOffice(c))
|
|
||||||
}
|
|
||||||
@@ -3,93 +3,262 @@ package api
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
webhookURL string = "webhookURL"
|
resultFilename string = "resultFilename"
|
||||||
paperWidth string = "paperWidth"
|
waitTimeout string = "waitTimeout"
|
||||||
paperHeight string = "paperHeight"
|
webhookURL string = "webhookURL"
|
||||||
marginTop string = "marginTop"
|
remoteURL string = "remoteURL"
|
||||||
marginBottom string = "marginBottom"
|
waitDelay string = "waitDelay"
|
||||||
marginLeft string = "marginLeft"
|
paperWidth string = "paperWidth"
|
||||||
marginRight string = "marginRight"
|
paperHeight string = "paperHeight"
|
||||||
landscape string = "landscape"
|
marginTop string = "marginTop"
|
||||||
|
marginBottom string = "marginBottom"
|
||||||
|
marginLeft string = "marginLeft"
|
||||||
|
marginRight string = "marginRight"
|
||||||
|
landscape string = "landscape"
|
||||||
)
|
)
|
||||||
|
|
||||||
// resource facilitates storing and accessing
|
|
||||||
// data from a multipart/form-data request.
|
|
||||||
type resource struct {
|
type resource struct {
|
||||||
values map[string]string
|
formValues map[string]string
|
||||||
dirPath string
|
formFilesDirPath string
|
||||||
|
opts *Options
|
||||||
}
|
}
|
||||||
|
|
||||||
func newResource(c echo.Context) (*resource, error) {
|
type resourceContext struct {
|
||||||
v := make(map[string]string)
|
echo.Context
|
||||||
v[webhookURL] = c.FormValue(webhookURL)
|
opts *Options
|
||||||
v[paperWidth] = c.FormValue(paperWidth)
|
resource *resource
|
||||||
v[paperHeight] = c.FormValue(paperHeight)
|
}
|
||||||
v[marginTop] = c.FormValue(marginTop)
|
|
||||||
v[marginBottom] = c.FormValue(marginBottom)
|
func newResource(ctx *resourceContext) (*resource, error) {
|
||||||
v[marginLeft] = c.FormValue(marginLeft)
|
r := &resource{
|
||||||
v[marginRight] = c.FormValue(marginRight)
|
formValues: formValues(ctx),
|
||||||
v[landscape] = c.FormValue(landscape)
|
opts: ctx.opts,
|
||||||
|
}
|
||||||
dirPath, err := rand.Get()
|
dirPath, err := rand.Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return r, err
|
||||||
}
|
}
|
||||||
|
r.formFilesDirPath = dirPath
|
||||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||||
return nil, fmt.Errorf("%s: making directory: %v", dirPath, err)
|
return nil, fmt.Errorf("%s: making directory: %v", dirPath, err)
|
||||||
}
|
}
|
||||||
r := &resource{values: v, dirPath: dirPath}
|
if err := formFiles(ctx, dirPath); err != nil {
|
||||||
form, err := c.MultipartForm()
|
return r, err
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formValues(ctx *resourceContext) map[string]string {
|
||||||
|
v := make(map[string]string)
|
||||||
|
v[resultFilename] = ctx.FormValue(resultFilename)
|
||||||
|
v[waitTimeout] = ctx.FormValue(waitTimeout)
|
||||||
|
v[webhookURL] = ctx.FormValue(webhookURL)
|
||||||
|
v[remoteURL] = ctx.FormValue(remoteURL)
|
||||||
|
v[waitDelay] = ctx.FormValue(waitDelay)
|
||||||
|
v[paperWidth] = ctx.FormValue(paperWidth)
|
||||||
|
v[paperHeight] = ctx.FormValue(paperHeight)
|
||||||
|
v[marginTop] = ctx.FormValue(marginTop)
|
||||||
|
v[marginBottom] = ctx.FormValue(marginBottom)
|
||||||
|
v[marginLeft] = ctx.FormValue(marginLeft)
|
||||||
|
v[marginRight] = ctx.FormValue(marginRight)
|
||||||
|
v[landscape] = ctx.FormValue(landscape)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func formFiles(ctx *resourceContext, dirPath string) error {
|
||||||
|
form, err := ctx.MultipartForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting multipart form: %v", err)
|
return fmt.Errorf("getting multipart form: %v", err)
|
||||||
}
|
}
|
||||||
for _, files := range form.File {
|
for _, files := range form.File {
|
||||||
for _, fh := range files {
|
for _, fh := range files {
|
||||||
in, err := fh.Open()
|
in, err := fh.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s: opening file: %v", fh.Filename, err)
|
return fmt.Errorf("%s: opening file: %v", fh.Filename, err)
|
||||||
}
|
}
|
||||||
defer in.Close()
|
defer in.Close() // nolint: errcheck
|
||||||
if err := r.writeFile(fh.Filename, in); err != nil {
|
fpath := fmt.Sprintf("%s/%s", dirPath, fh.Filename)
|
||||||
return nil, err
|
out, err := os.Create(fpath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: creating new file: %v", fpath, err)
|
||||||
|
}
|
||||||
|
defer out.Close() // nolint: errcheck
|
||||||
|
if err := out.Chmod(0644); err != nil {
|
||||||
|
return fmt.Errorf("%s: changing file mode: %v", fpath, err)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(out, in); err != nil {
|
||||||
|
return fmt.Errorf("%s: writing file: %v", fpath, err)
|
||||||
|
}
|
||||||
|
if _, err := out.Seek(0, 0); err != nil {
|
||||||
|
return fmt.Errorf("%s: resetting read pointer: %v", fpath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) writeFile(filename string, in io.Reader) error {
|
|
||||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
|
||||||
out, err := os.Create(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: creating new file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
if err := out.Chmod(0644); err != nil {
|
|
||||||
return fmt.Errorf("%s: changing file mode: %v", fpath, err)
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(out, in); err != nil {
|
|
||||||
return fmt.Errorf("%s: writing file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
if _, err := out.Seek(0, 0); err != nil {
|
|
||||||
return fmt.Errorf("%s: resetting read pointer: %v", fpath, err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resource) filePath(filename string) (string, error) {
|
func (r *resource) close() error {
|
||||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
if _, err := os.Stat(r.formFilesDirPath); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.RemoveAll(r.formFilesDirPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
||||||
|
|
||||||
|
func (r *resource) chromePrinterOptions() (*printer.ChromeOptions, error) {
|
||||||
|
timeout, err := r.float64(waitTimeout, r.opts.DefaultWaitTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
delay, err := r.float64(waitDelay, 0.0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
header, err := r.content("header.html", defaultHeaderFooterHTML)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
footer, err := r.content("footer.html", defaultHeaderFooterHTML)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
width, err := r.float64(paperWidth, 8.27)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
height, err := r.float64(paperHeight, 11.7)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
top, err := r.float64(marginTop, 1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bottom, err := r.float64(marginBottom, 1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
left, err := r.float64(marginLeft, 1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
right, err := r.float64(marginRight, 1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
landscape, err := r.bool(landscape, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &printer.ChromeOptions{
|
||||||
|
WaitTimeout: timeout,
|
||||||
|
WaitDelay: delay,
|
||||||
|
HeaderHTML: header,
|
||||||
|
FooterHTML: footer,
|
||||||
|
PaperWidth: width,
|
||||||
|
PaperHeight: height,
|
||||||
|
MarginTop: top,
|
||||||
|
MarginBottom: bottom,
|
||||||
|
MarginLeft: left,
|
||||||
|
MarginRight: right,
|
||||||
|
Landscape: landscape,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) officePrinterOptions() (*printer.OfficeOptions, error) {
|
||||||
|
timeout, err := r.float64(waitTimeout, r.opts.DefaultWaitTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
landscape, err := r.bool(landscape, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &printer.OfficeOptions{
|
||||||
|
WaitTimeout: timeout,
|
||||||
|
Landscape: landscape,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) mergePrinterOptions() (*printer.MergeOptions, error) {
|
||||||
|
timeout, err := r.float64(waitTimeout, r.opts.DefaultWaitTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &printer.MergeOptions{
|
||||||
|
WaitTimeout: timeout,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) has(key string) bool {
|
||||||
|
v, ok := r.formValues[key]
|
||||||
|
if ok {
|
||||||
|
ok = v != ""
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) hasFile(filename string) bool {
|
||||||
|
fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename)
|
||||||
|
_, err := os.Stat(fpath)
|
||||||
|
return !os.IsNotExist(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) get(key string) (string, error) {
|
||||||
|
v, ok := r.formValues[key]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("form value %s does not exist", key)
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) float64(key string, defaultValue float64) (float64, error) {
|
||||||
|
if !r.has(key) {
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
v, err := r.get(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0.0, err
|
||||||
|
}
|
||||||
|
f, err := strconv.ParseFloat(v, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0.0, fmt.Errorf("form value %s: %v", key, err)
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) bool(key string, defaultValue bool) (bool, error) {
|
||||||
|
if !r.has(key) {
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
v, err := r.get(key)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
b, err := strconv.ParseBool(v)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("form value %s: %v", key, err)
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) fpath(filename string) (string, error) {
|
||||||
|
fpath := fmt.Sprintf("%s/%s", r.formFilesDirPath, filename)
|
||||||
_, err := os.Stat(fpath)
|
_, err := os.Stat(fpath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return "", fmt.Errorf("%s: file does not exist", filename)
|
return "", fmt.Errorf("%s: form file does not exist", filename)
|
||||||
}
|
}
|
||||||
absPath, err := filepath.Abs(fpath)
|
absPath, err := filepath.Abs(fpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -98,13 +267,28 @@ func (r *resource) filePath(filename string) (string, error) {
|
|||||||
return absPath, nil
|
return absPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resource) filePaths(exts []string) ([]string, error) {
|
func (r *resource) content(filename string, defaultValue string) (string, error) {
|
||||||
|
if !r.hasFile(filename) {
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
fpath, err := r.fpath(filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
b, err := ioutil.ReadFile(fpath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("%s: reading form file: %v", fpath, err)
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) fpaths(exts ...string) ([]string, error) {
|
||||||
var fpaths []string
|
var fpaths []string
|
||||||
err := filepath.Walk(r.dirPath, func(path string, info os.FileInfo, _ error) error {
|
err := filepath.Walk(r.formFilesDirPath, func(path string, info os.FileInfo, _ error) error {
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
fpath, err := r.filePath(info.Name())
|
fpath, err := r.fpath(info.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -119,66 +303,8 @@ func (r *resource) filePaths(exts []string) ([]string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(fpaths) == 0 {
|
||||||
|
return nil, fmt.Errorf("no form files found for extensions: %v", exts)
|
||||||
|
}
|
||||||
return fpaths, nil
|
return fpaths, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *resource) paperSize() ([2]float64, error) {
|
|
||||||
defaultSize := [2]float64{8.27, 11.7}
|
|
||||||
widthStr := r.values[paperWidth]
|
|
||||||
heightStr := r.values[paperHeight]
|
|
||||||
if widthStr == "" || heightStr == "" {
|
|
||||||
return defaultSize, nil
|
|
||||||
}
|
|
||||||
width, err := strconv.ParseFloat(widthStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultSize, fmt.Errorf("paper width: %v", err)
|
|
||||||
}
|
|
||||||
height, err := strconv.ParseFloat(heightStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultSize, fmt.Errorf("paper height: %v", err)
|
|
||||||
}
|
|
||||||
return [2]float64{width, height}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) paperMargins() ([4]float64, error) {
|
|
||||||
defaultMargins := [4]float64{1, 1, 1, 1}
|
|
||||||
topStr := r.values[marginTop]
|
|
||||||
bottomStr := r.values[marginBottom]
|
|
||||||
leftStr := r.values[marginLeft]
|
|
||||||
rightStr := r.values[marginRight]
|
|
||||||
if topStr == "" || bottomStr == "" || leftStr == "" || rightStr == "" {
|
|
||||||
return defaultMargins, nil
|
|
||||||
}
|
|
||||||
top, err := strconv.ParseFloat(topStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultMargins, fmt.Errorf("margin top: %v", err)
|
|
||||||
}
|
|
||||||
bottom, err := strconv.ParseFloat(bottomStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultMargins, fmt.Errorf("margin bottom: %v", err)
|
|
||||||
}
|
|
||||||
left, err := strconv.ParseFloat(leftStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultMargins, fmt.Errorf("margin left: %v", err)
|
|
||||||
}
|
|
||||||
right, err := strconv.ParseFloat(rightStr, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultMargins, fmt.Errorf("margin right: %v", err)
|
|
||||||
}
|
|
||||||
return [4]float64{top, bottom, left, right}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) landscape() (bool, error) {
|
|
||||||
landscapeStr := r.values[landscape]
|
|
||||||
if landscapeStr == "" {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
landscape, err := strconv.ParseBool(landscapeStr)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("landscape: %v", err)
|
|
||||||
}
|
|
||||||
return landscape, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *resource) webhookURL() string { return r.values[webhookURL] }
|
|
||||||
func (r *resource) removeAll() error { return os.RemoveAll(r.dirPath) }
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Package notify is used across the application
|
Package notify helps displaying nice outputs
|
||||||
to display nice outputs to the user.
|
to the user.
|
||||||
*/
|
*/
|
||||||
package notify
|
package notify
|
||||||
|
|||||||
@@ -7,24 +7,29 @@ import (
|
|||||||
"github.com/labstack/gommon/color"
|
"github.com/labstack/gommon/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// Print prints a message to stdout.
|
||||||
stdout *color.Color
|
func Print(message string) {
|
||||||
stderr *color.Color
|
stdout := color.New()
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
stdout = color.New()
|
|
||||||
stdout.SetOutput(os.Stdout)
|
stdout.SetOutput(os.Stdout)
|
||||||
stderr = color.New()
|
|
||||||
stderr.SetOutput(os.Stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Println prints a message to stdout.
|
|
||||||
func Println(message string) {
|
|
||||||
stdout.Printf("⇨ %s\n", message)
|
stdout.Printf("⇨ %s\n", message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrPrintln prints an error to stderr.
|
// Printf prints a formatted message to stdout.
|
||||||
func ErrPrintln(err error) {
|
func Printf(format string, a ...interface{}) {
|
||||||
|
message := fmt.Sprintf(format, a...)
|
||||||
|
Print(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WarnPrint prints a warning to stderr.
|
||||||
|
func WarnPrint(err error) {
|
||||||
|
stderr := color.New()
|
||||||
|
stderr.SetOutput(os.Stderr)
|
||||||
|
stderr.Printf("%s\n", color.Yellow(fmt.Sprintf("⇨ warn: %v", err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrPrint prints an error to stderr.
|
||||||
|
func ErrPrint(err error) {
|
||||||
|
stderr := color.New()
|
||||||
|
stderr.SetOutput(os.Stderr)
|
||||||
stderr.Printf("%s\n", color.Red(fmt.Sprintf("⇨ error: %v", err)))
|
stderr.Printf("%s\n", color.Red(fmt.Sprintf("⇨ error: %v", err)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,30 +2,36 @@ package pm2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mafredri/cdp/devtool"
|
"github.com/mafredri/cdp/devtool"
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Chrome facilitates starting or shutting down
|
type chrome struct {
|
||||||
// Chrome headless with PM2.
|
manager *processManager
|
||||||
type Chrome struct{}
|
|
||||||
|
|
||||||
// Launch starts Chrome headless with PM2.
|
|
||||||
func (c *Chrome) Launch() error {
|
|
||||||
return launch(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown stops Chrome headless and
|
// NewChrome retruns a Google Chrome
|
||||||
// removes it from the list of PM2
|
// headless process.
|
||||||
// processes.
|
func NewChrome() Process {
|
||||||
func (c *Chrome) Shutdown() error {
|
return &chrome{
|
||||||
return shutdown(c)
|
manager: &processManager{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chrome) getArgs() []string {
|
func (p *chrome) Fullname() string {
|
||||||
|
return "Google Chrome headless"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chrome) Start() error {
|
||||||
|
return p.manager.start(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chrome) Shutdown() error {
|
||||||
|
return p.manager.shutdown(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chrome) args() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"--no-sandbox",
|
"--no-sandbox",
|
||||||
"--headless",
|
"--headless",
|
||||||
@@ -44,27 +50,23 @@ func (c *Chrome) getArgs() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chrome) getName() string {
|
func (p *chrome) name() string {
|
||||||
return "google-chrome-stable"
|
return "google-chrome-stable"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chrome) getFullname() string {
|
func (p *chrome) viable() bool {
|
||||||
return "Chrome headless"
|
// check if Google Chrome is correctly running.
|
||||||
}
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
func (c *Chrome) isViable() bool {
|
_, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||||
// check if Chrome is correctly running.
|
|
||||||
devt := devtool.New("http://127.0.0.1:9222")
|
|
||||||
_, err := devt.Create(context.TODO())
|
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chrome) warmup() {
|
func (p *chrome) warmup() {
|
||||||
notify.Println(fmt.Sprintf("warming-up %s", c.getFullname()))
|
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Process(new(Chrome))
|
_ = Process(new(chrome))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestChromeLaunch(t *testing.T) {
|
func TestChromeStart(t *testing.T) {
|
||||||
p := &Chrome{}
|
p := NewChrome()
|
||||||
err := p.Launch()
|
err := p.Start()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChromeShutdown(t *testing.T) {
|
func TestChromeShutdown(t *testing.T) {
|
||||||
p := &Chrome{}
|
p := NewChrome()
|
||||||
err := p.Shutdown()
|
err := p.Shutdown()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Package pm2 facilitates starting external
|
Package pm2 facilitates starting external
|
||||||
processes on which our API depends.
|
processes on which our API depends.
|
||||||
|
|
||||||
For instance, it starts Chrome headless and
|
For instance, it may start Google Chrome headless and
|
||||||
unoconv listener with PM2.
|
unoconv listener with PM2.
|
||||||
|
|
||||||
The PM2 process manager launch those processes and keep
|
The PM2 process manager launch those processes and keep
|
||||||
|
|||||||
@@ -3,69 +3,81 @@ package pm2
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Process is a type that can launch or
|
const (
|
||||||
|
stoppedState = iota
|
||||||
|
runningState
|
||||||
|
errorState
|
||||||
|
)
|
||||||
|
|
||||||
|
// Process is a type that can start or
|
||||||
// shutdown a process with PM2.
|
// shutdown a process with PM2.
|
||||||
type Process interface {
|
type Process interface {
|
||||||
Launch() error
|
Fullname() string
|
||||||
|
Start() error
|
||||||
Shutdown() error
|
Shutdown() error
|
||||||
getArgs() []string
|
args() []string
|
||||||
getName() string
|
name() string
|
||||||
getFullname() string
|
viable() bool
|
||||||
isViable() bool
|
|
||||||
warmup()
|
warmup()
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxRestartAttempts int = 5
|
type processManager struct {
|
||||||
|
heuristicState int32
|
||||||
var humanNames = map[string]string{
|
|
||||||
"start": "started",
|
|
||||||
"restart": "restarted",
|
|
||||||
"stop": "stopped",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func launch(p Process) error {
|
func (m *processManager) start(p Process) error {
|
||||||
if err := run(p, "start"); err != nil {
|
if err := m.pm2(p, "start"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.warmup()
|
p.warmup()
|
||||||
if !p.isViable() {
|
if !p.viable() {
|
||||||
attempts := 0
|
attempts := 0
|
||||||
for attempts < maxRestartAttempts && !p.isViable() {
|
for attempts < 5 && !p.viable() {
|
||||||
run(p, "restart")
|
if err := m.pm2(p, "restart"); err != nil {
|
||||||
|
m.heuristicState = errorState
|
||||||
|
return err
|
||||||
|
}
|
||||||
p.warmup()
|
p.warmup()
|
||||||
attempts++
|
attempts++
|
||||||
}
|
}
|
||||||
if !p.isViable() {
|
if !p.viable() {
|
||||||
return fmt.Errorf("failed to launch %s", p.getFullname())
|
m.heuristicState = errorState
|
||||||
|
return fmt.Errorf("failed to launch %s", p.Fullname())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m.heuristicState = runningState
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func shutdown(p Process) error {
|
func (m *processManager) shutdown(p Process) error {
|
||||||
return run(p, "stop")
|
if m.heuristicState != runningState {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := m.pm2(p, "stop"); err != nil {
|
||||||
|
m.heuristicState = errorState
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.heuristicState = stoppedState
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(p Process, cmdName string) error {
|
func (m *processManager) pm2(p Process, cmdName string) error {
|
||||||
cmdArgs := []string{
|
cmdArgs := []string{
|
||||||
cmdName,
|
cmdName,
|
||||||
p.getName(),
|
p.name(),
|
||||||
}
|
}
|
||||||
if cmdName == "start" {
|
if cmdName == "start" {
|
||||||
cmdArgs = append(cmdArgs, "--interpreter none", "--")
|
cmdArgs = append(cmdArgs, "--interpreter none", "--")
|
||||||
cmdArgs = append(cmdArgs, p.getArgs()...)
|
cmdArgs = append(cmdArgs, p.args()...)
|
||||||
}
|
}
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"pm2",
|
"pm2",
|
||||||
cmdArgs...,
|
cmdArgs...,
|
||||||
)
|
)
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("%s %s with PM2: %v", cmdName, p.getFullname(), err)
|
return fmt.Errorf("%s %s with PM2: %v", cmdName, p.Fullname(), err)
|
||||||
}
|
}
|
||||||
notify.Println(fmt.Sprintf("%s %s with PM2", p.getFullname(), humanNames[cmdName]))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,52 @@
|
|||||||
package pm2
|
package pm2
|
||||||
|
|
||||||
// Unoconv facilitates starting or shutting down
|
type unoconv struct {
|
||||||
// unoconv listener with PM2.
|
manager *processManager
|
||||||
type Unoconv struct{}
|
|
||||||
|
|
||||||
// Launch starts unoconv listener with PM2.
|
|
||||||
func (u *Unoconv) Launch() error {
|
|
||||||
return launch(u)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown stops unoconv listener and
|
// NewUnoconv retruns a unoconv listener
|
||||||
// removes it from the list of PM2
|
// process.
|
||||||
// processes.
|
func NewUnoconv() Process {
|
||||||
func (u *Unoconv) Shutdown() error {
|
return &unoconv{
|
||||||
return shutdown(u)
|
manager: &processManager{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Unoconv) getArgs() []string {
|
func (p *unoconv) Fullname() string {
|
||||||
|
return "unoconv listener"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *unoconv) Start() error {
|
||||||
|
return p.manager.start(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *unoconv) Shutdown() error {
|
||||||
|
return p.manager.shutdown(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *unoconv) args() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"--listener",
|
"--listener",
|
||||||
"--verbose",
|
"--verbose",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Unoconv) getName() string {
|
func (p *unoconv) name() string {
|
||||||
return "unoconv"
|
return "unoconv"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Unoconv) getFullname() string {
|
func (p *unoconv) viable() bool {
|
||||||
return "unoconv listener"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Unoconv) isViable() bool {
|
|
||||||
// TODO find a way to check if
|
// TODO find a way to check if
|
||||||
// unoconv is correctly started?
|
// the unoconv listener
|
||||||
|
// is correctly started?
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Unoconv) warmup() {
|
func (p *unoconv) warmup() {
|
||||||
// let's do nothing.
|
// let's do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Process(new(Unoconv))
|
_ = Process(new(unoconv))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUnoconvLaunch(t *testing.T) {
|
func TestUnoconvStart(t *testing.T) {
|
||||||
p := &Unoconv{}
|
p := NewUnoconv()
|
||||||
err := p.Launch()
|
err := p.Start()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnoconvShutdown(t *testing.T) {
|
func TestUnoconvShutdown(t *testing.T) {
|
||||||
p := &Unoconv{}
|
p := NewUnoconv()
|
||||||
err := p.Shutdown()
|
err := p.Shutdown()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
169
internal/pkg/printer/chrome.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mafredri/cdp"
|
||||||
|
"github.com/mafredri/cdp/devtool"
|
||||||
|
"github.com/mafredri/cdp/protocol/network"
|
||||||
|
"github.com/mafredri/cdp/protocol/page"
|
||||||
|
"github.com/mafredri/cdp/protocol/target"
|
||||||
|
"github.com/mafredri/cdp/rpcc"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
type chrome struct {
|
||||||
|
url string
|
||||||
|
opts *ChromeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChromeOptions helps customizing the
|
||||||
|
// Google Chrome printer behaviour.
|
||||||
|
type ChromeOptions struct {
|
||||||
|
WaitTimeout float64
|
||||||
|
WaitDelay float64
|
||||||
|
HeaderHTML string
|
||||||
|
FooterHTML string
|
||||||
|
PaperWidth float64
|
||||||
|
PaperHeight float64
|
||||||
|
MarginTop float64
|
||||||
|
MarginBottom float64
|
||||||
|
MarginLeft float64
|
||||||
|
MarginRight float64
|
||||||
|
Landscape bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chrome) Print(destination string) error {
|
||||||
|
duration := time.Duration(p.opts.WaitTimeout+p.opts.WaitDelay) * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), duration)
|
||||||
|
defer cancel()
|
||||||
|
devt, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol.
|
||||||
|
devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer devtConn.Close() // nolint: errcheck
|
||||||
|
// create a new CDP Client that uses conn.
|
||||||
|
devtClient := cdp.NewClient(devtConn)
|
||||||
|
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating new browser context: %v", err)
|
||||||
|
}
|
||||||
|
// create a new blank target with the new browser context.
|
||||||
|
createTargetArgs := target.
|
||||||
|
NewCreateTargetArgs("about:blank").
|
||||||
|
SetBrowserContextID(newContextTarget.BrowserContextID)
|
||||||
|
newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating new blank target: %v", err)
|
||||||
|
}
|
||||||
|
// connect the client to the new target.
|
||||||
|
newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID)
|
||||||
|
newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connecting client to blank target: %v", err)
|
||||||
|
}
|
||||||
|
defer newContextConn.Close() // nolint: errcheck
|
||||||
|
// create a new CDP Client that uses newContextConn.
|
||||||
|
targetClient := cdp.NewClient(newContextConn)
|
||||||
|
closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID)
|
||||||
|
// close the target when done.
|
||||||
|
defer targetClient.Target.CloseTarget(ctx, closeTargetArgs) // nolint: errcheck
|
||||||
|
if err := runBatch(
|
||||||
|
// enable all the domain events that we're interested in.
|
||||||
|
func() error { return targetClient.DOM.Enable(ctx) },
|
||||||
|
func() error { return targetClient.Network.Enable(ctx, network.NewEnableArgs()) },
|
||||||
|
func() error { return targetClient.Page.Enable(ctx) },
|
||||||
|
func() error { return targetClient.Runtime.Enable(ctx) },
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.navigate(ctx, targetClient); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
print, err := targetClient.Page.PrintToPDF(
|
||||||
|
ctx,
|
||||||
|
page.NewPrintToPDFArgs().
|
||||||
|
SetPaperWidth(p.opts.PaperWidth).
|
||||||
|
SetPaperHeight(p.opts.PaperHeight).
|
||||||
|
SetMarginTop(p.opts.MarginTop).
|
||||||
|
SetMarginBottom(p.opts.MarginBottom).
|
||||||
|
SetMarginLeft(p.opts.MarginLeft).
|
||||||
|
SetMarginRight(p.opts.MarginRight).
|
||||||
|
SetLandscape(p.opts.Landscape).
|
||||||
|
SetDisplayHeaderFooter(true).
|
||||||
|
SetHeaderTemplate(p.opts.HeaderHTML).
|
||||||
|
SetFooterTemplate(p.opts.FooterHTML).
|
||||||
|
SetPrintBackground(true),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("printing page to PDF: %v", err)
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("%s: writing file: %v", destination, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *chrome) navigate(ctx context.Context, client *cdp.Client) error {
|
||||||
|
// make sure Page events are enabled.
|
||||||
|
if err := client.Page.Enable(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// make sure Network events are enabled.
|
||||||
|
if err := client.Network.Enable(ctx, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// create all clients for events.
|
||||||
|
domContentEventFired, err := client.Page.DOMContentEventFired(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer domContentEventFired.Close() // nolint: errcheck
|
||||||
|
loadEventFired, err := client.Page.LoadEventFired(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer loadEventFired.Close() // nolint: errcheck
|
||||||
|
loadingFinished, err := client.Network.LoadingFinished(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer loadingFinished.Close() // nolint: errcheck
|
||||||
|
if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runBatch(
|
||||||
|
// wait for all events.
|
||||||
|
func() error { _, err := domContentEventFired.Recv(); return err },
|
||||||
|
func() error { _, err := loadEventFired.Recv(); return err },
|
||||||
|
func() error { _, err := loadingFinished.Recv(); return err },
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// wait for a given amount of time (useful for javascript delay).
|
||||||
|
time.Sleep(time.Duration(p.opts.WaitDelay) * time.Second)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBatch(fn ...func() error) error {
|
||||||
|
// run all functions simultaneously and wait until
|
||||||
|
// execution has completed or an error is encountered.
|
||||||
|
eg := errgroup.Group{}
|
||||||
|
for _, f := range fn {
|
||||||
|
eg.Go(f)
|
||||||
|
}
|
||||||
|
return eg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
|
var (
|
||||||
|
_ = Printer(new(chrome))
|
||||||
|
)
|
||||||
@@ -1,52 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Package printer contains structs which convert
|
Package printer contains structs which convert
|
||||||
a specific file type to PDF:
|
a specific file type to PDF.
|
||||||
|
|
||||||
// converting HTML to PDF.
|
|
||||||
p := &printer.HTML{
|
|
||||||
Context: context.Background(),
|
|
||||||
PaperWidth: 8.27,
|
|
||||||
PaperHeight: 11.7,
|
|
||||||
Landscape: false,
|
|
||||||
}
|
|
||||||
p.WithLocalURL("index.html")
|
|
||||||
if err := p.Print("result.pdf"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// converting Markdown to PDF:
|
|
||||||
// it assumes here that our template "index.html"
|
|
||||||
// will call toHTML method to convert
|
|
||||||
// markdown files to HTML.
|
|
||||||
p := &printer.Markdown{
|
|
||||||
Context: context.Background(),
|
|
||||||
TemplatePath: "index.html",
|
|
||||||
PaperWidth: 8.27,
|
|
||||||
PaperHeight: 11.7,
|
|
||||||
Landscape: false,
|
|
||||||
}
|
|
||||||
if err := p.Print("result.pdf"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// converting Office documents to PDF:
|
|
||||||
// it converts each files independently and
|
|
||||||
// then merge them.
|
|
||||||
//
|
|
||||||
// Also, as unoconv cannot perform
|
|
||||||
// concurrent conversions, a lock is applied.
|
|
||||||
p := &printer.Office{
|
|
||||||
Context: ctx,
|
|
||||||
FilePaths: []string{"document.docx", "presentation.pptx"}
|
|
||||||
}
|
|
||||||
if err := p.Print("result.pdf"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
It is also able to merge a list of PDF files:
|
|
||||||
|
|
||||||
if err := printer.Merge([]string{"foo.pdf", "bar.pdf"}, "result.pdf"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*/
|
*/
|
||||||
package printer
|
package printer
|
||||||
|
|||||||
@@ -1,188 +1,14 @@
|
|||||||
package printer
|
package printer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mafredri/cdp"
|
|
||||||
"github.com/mafredri/cdp/devtool"
|
|
||||||
"github.com/mafredri/cdp/protocol/network"
|
|
||||||
"github.com/mafredri/cdp/protocol/page"
|
|
||||||
"github.com/mafredri/cdp/protocol/runtime"
|
|
||||||
"github.com/mafredri/cdp/protocol/target"
|
|
||||||
"github.com/mafredri/cdp/rpcc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTML facilitates HTML to PDF conversion.
|
// NewHTML returns an HTML printer.
|
||||||
type HTML struct {
|
func NewHTML(fpath string, opts *ChromeOptions) Printer {
|
||||||
Context context.Context
|
URL := fmt.Sprintf("file://%s", fpath)
|
||||||
URL string
|
return &chrome{
|
||||||
HeaderHTML string
|
url: URL,
|
||||||
FooterHTML string
|
opts: opts,
|
||||||
PaperWidth float64
|
}
|
||||||
PaperHeight float64
|
|
||||||
MarginTop float64
|
|
||||||
MarginBottom float64
|
|
||||||
MarginLeft float64
|
|
||||||
MarginRight float64
|
|
||||||
Landscape bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultHeaderFooterHTML string = "<html><head></head><body></body></html>"
|
|
||||||
|
|
||||||
// Print converts HTML to PDF.
|
|
||||||
// Credits: https://medium.com/compass-true-north/go-service-to-convert-web-pages-to-pdf-using-headless-chrome-5fd9ffbae1af
|
|
||||||
func (html *HTML) Print(destination string) error {
|
|
||||||
// use the DevTools HTTP/JSON API to manage targets (e.g. pages, webworkers).
|
|
||||||
devt := devtool.New("http://127.0.0.1:9222")
|
|
||||||
pt, err := devt.Create(html.Context)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating DevTools target: %v", err)
|
|
||||||
}
|
|
||||||
// open a new RPC connection to the Chrome Debugging Protocol target.
|
|
||||||
conn, err := rpcc.DialContext(html.Context, pt.WebSocketDebuggerURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating RPC connection: %v", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
// create new browser context.
|
|
||||||
baseBrowser := cdp.NewClient(conn)
|
|
||||||
newContextTarget, err := baseBrowser.Target.CreateBrowserContext(html.Context)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating new browser context: %v", err)
|
|
||||||
}
|
|
||||||
// create a new blank target with the new browser context.
|
|
||||||
newTargetArgs := target.NewCreateTargetArgs("about:blank").
|
|
||||||
SetBrowserContextID(newContextTarget.BrowserContextID)
|
|
||||||
newTarget, err := baseBrowser.Target.CreateTarget(html.Context, newTargetArgs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating new blank target: %v", err)
|
|
||||||
}
|
|
||||||
// connect the client to the new target.
|
|
||||||
newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID)
|
|
||||||
newContextConn, err := rpcc.DialContext(html.Context, newTargetWsURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("connecting client to blank target: %v", err)
|
|
||||||
}
|
|
||||||
defer newContextConn.Close()
|
|
||||||
// close the target when done.
|
|
||||||
closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID)
|
|
||||||
defer baseBrowser.Target.CloseTarget(html.Context, closeTargetArgs)
|
|
||||||
c := cdp.NewClient(newContextConn)
|
|
||||||
// enable the runtime.
|
|
||||||
if err := c.Runtime.Enable(html.Context); err != nil {
|
|
||||||
return fmt.Errorf("enabling runtime: %v", err)
|
|
||||||
}
|
|
||||||
// enable the network.
|
|
||||||
if err := c.Network.Enable(html.Context, network.NewEnableArgs()); err != nil {
|
|
||||||
return fmt.Errorf("enabling network: %v", err)
|
|
||||||
}
|
|
||||||
// enable events on the page domain.
|
|
||||||
if err := c.Page.Enable(html.Context); err != nil {
|
|
||||||
return fmt.Errorf("enabling events on page domain: %v", err)
|
|
||||||
}
|
|
||||||
// create a client to listen for the load event to be fired.
|
|
||||||
loadEventFiredClient, err := c.Page.LoadEventFired(html.Context)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating client listening for load event: %v", err)
|
|
||||||
}
|
|
||||||
defer loadEventFiredClient.Close()
|
|
||||||
// tell the page to navigate to the URL.
|
|
||||||
navArgs := page.NewNavigateArgs(html.URL)
|
|
||||||
_, err = c.Page.Navigate(html.Context, navArgs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: navigating to URL: %v", html.URL, err)
|
|
||||||
}
|
|
||||||
// wait for the page to finish loading.
|
|
||||||
_, err = loadEventFiredClient.Recv()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("waiting for page loading: %v", err)
|
|
||||||
}
|
|
||||||
// inject a script to make sure web fonts are loaded.
|
|
||||||
script := `new Promise((resolve, reject) => {
|
|
||||||
document.fonts.ready.then(function () {
|
|
||||||
resolve('fonts loaded');
|
|
||||||
});
|
|
||||||
setTimeout(resolve.bind(resolve, 'timeout'), %.0f);
|
|
||||||
});`
|
|
||||||
scriptArg := runtime.NewEvaluateArgs(script).SetAwaitPromise(true)
|
|
||||||
returnObj, _ := c.Runtime.Evaluate(html.Context, scriptArg)
|
|
||||||
loadFontsResult := string(returnObj.Result.Value)
|
|
||||||
if strings.Contains(loadFontsResult, "timeout") {
|
|
||||||
return errors.New("timed out loading fonts")
|
|
||||||
}
|
|
||||||
// if no header or footer, use the default template
|
|
||||||
// for avoiding displaying default Chrome templates.
|
|
||||||
if html.HeaderHTML == "" {
|
|
||||||
html.HeaderHTML = defaultHeaderFooterHTML
|
|
||||||
}
|
|
||||||
if html.FooterHTML == "" {
|
|
||||||
html.FooterHTML = defaultHeaderFooterHTML
|
|
||||||
}
|
|
||||||
print, err := c.Page.PrintToPDF(
|
|
||||||
html.Context,
|
|
||||||
page.NewPrintToPDFArgs().
|
|
||||||
SetPaperWidth(html.PaperWidth).
|
|
||||||
SetPaperHeight(html.PaperHeight).
|
|
||||||
SetMarginTop(html.MarginTop).
|
|
||||||
SetMarginBottom(html.MarginBottom).
|
|
||||||
SetMarginLeft(html.MarginLeft).
|
|
||||||
SetMarginRight(html.MarginRight).
|
|
||||||
SetLandscape(html.Landscape).
|
|
||||||
SetDisplayHeaderFooter(true).
|
|
||||||
SetHeaderTemplate(html.HeaderHTML).
|
|
||||||
SetFooterTemplate(html.FooterHTML).
|
|
||||||
SetPrintBackground(true),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("printing page to PDF: %v", err)
|
|
||||||
}
|
|
||||||
return writeBytesToFile(destination, print.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLocalURL sets a local URL from a file path.
|
|
||||||
func (html *HTML) WithLocalURL(fpath string) {
|
|
||||||
html.URL = fmt.Sprintf("file://%s", fpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithHeaderFile sets header content from a file.
|
|
||||||
func (html *HTML) WithHeaderFile(fpath string) error {
|
|
||||||
if fpath == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
contentHTML, err := fileContentToString(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
html.HeaderHTML = contentHTML
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFooterFile sets footer content from a file.
|
|
||||||
func (html *HTML) WithFooterFile(fpath string) error {
|
|
||||||
if fpath == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
contentHTML, err := fileContentToString(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
html.FooterHTML = contentHTML
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileContentToString(fpath string) (string, error) {
|
|
||||||
b, err := ioutil.ReadFile(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%s: reading file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
return string(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Printer(new(HTML))
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHTML(t *testing.T) {
|
|
||||||
dirPath := test.HTMLTestDirPath(t)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
html := &HTML{
|
|
||||||
Context: ctx,
|
|
||||||
PaperWidth: 8.27,
|
|
||||||
PaperHeight: 11.7,
|
|
||||||
MarginTop: 1,
|
|
||||||
MarginBottom: 1,
|
|
||||||
MarginLeft: 1,
|
|
||||||
MarginRight: 1,
|
|
||||||
}
|
|
||||||
html.WithLocalURL(fmt.Sprintf("%s/%s", dirPath, "index.html"))
|
|
||||||
err := html.WithHeaderFile(fmt.Sprintf("%s/%s", dirPath, "header.html"))
|
|
||||||
require.Nil(t, err)
|
|
||||||
err = html.WithFooterFile(fmt.Sprintf("%s/%s", dirPath, "footer.html"))
|
|
||||||
require.Nil(t, err)
|
|
||||||
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")
|
|
||||||
err = html.Print(dst)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.FileExists(t, dst)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package printer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -13,99 +12,48 @@ import (
|
|||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Markdown facilitates Markdown to PDF conversion.
|
// NewMarkdown returns a Markdown printer.
|
||||||
type Markdown struct {
|
func NewMarkdown(fpath string, opts *ChromeOptions) (Printer, error) {
|
||||||
Context context.Context
|
tmpl, err := template.
|
||||||
TemplatePath string
|
New(filepath.Base(fpath)).
|
||||||
HeaderHTML string
|
Funcs(template.FuncMap{"toHTML": markdownToHTML}).
|
||||||
FooterHTML string
|
ParseFiles(fpath)
|
||||||
PaperWidth float64
|
if err != nil {
|
||||||
PaperHeight float64
|
return nil, fmt.Errorf("%s: parsing template: %v", fpath, err)
|
||||||
MarginTop float64
|
}
|
||||||
MarginBottom float64
|
dirPath := filepath.Dir(fpath)
|
||||||
MarginLeft float64
|
data := &templateData{DirPath: dirPath}
|
||||||
MarginRight float64
|
var buffer bytes.Buffer
|
||||||
Landscape bool
|
if err := tmpl.Execute(&buffer, data); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: executing template: %v", fpath, err)
|
||||||
html *HTML
|
}
|
||||||
|
baseFilename, err := rand.Get()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
|
||||||
|
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: writing file: %v", dst, err)
|
||||||
|
}
|
||||||
|
URL := fmt.Sprintf("file://%s", dst)
|
||||||
|
return &chrome{
|
||||||
|
url: URL,
|
||||||
|
opts: opts,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type templateData struct {
|
type templateData struct {
|
||||||
DirPath string
|
DirPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print converts markdown to PDF.
|
func markdownToHTML(dirPath, filename string) (template.HTML, error) {
|
||||||
func (md *Markdown) Print(destination string) error {
|
|
||||||
if md.html == nil {
|
|
||||||
md.html = &HTML{Context: md.Context}
|
|
||||||
}
|
|
||||||
if md.HeaderHTML != "" {
|
|
||||||
md.html.HeaderHTML = md.HeaderHTML
|
|
||||||
}
|
|
||||||
if md.FooterHTML != "" {
|
|
||||||
md.html.FooterHTML = md.FooterHTML
|
|
||||||
}
|
|
||||||
md.html.PaperWidth = md.PaperWidth
|
|
||||||
md.html.PaperHeight = md.PaperHeight
|
|
||||||
md.html.MarginTop = md.MarginTop
|
|
||||||
md.html.MarginBottom = md.MarginBottom
|
|
||||||
md.html.MarginLeft = md.MarginLeft
|
|
||||||
md.html.MarginRight = md.MarginRight
|
|
||||||
md.html.Landscape = md.Landscape
|
|
||||||
tmpl, err := template.
|
|
||||||
New(filepath.Base(md.TemplatePath)).
|
|
||||||
Funcs(template.FuncMap{"toHTML": toHTML}).
|
|
||||||
ParseFiles(md.TemplatePath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: parsing template: %v", md.TemplatePath, err)
|
|
||||||
}
|
|
||||||
dirPath := filepath.Dir(md.TemplatePath)
|
|
||||||
data := &templateData{DirPath: dirPath}
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
if err := tmpl.Execute(&buffer, data); err != nil {
|
|
||||||
return fmt.Errorf("%s: executing template: %v", md.TemplatePath, err)
|
|
||||||
}
|
|
||||||
baseFilename, err := rand.Get()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
|
|
||||||
if err := writeBytesToFile(dst, buffer.Bytes()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
md.html.WithLocalURL(dst)
|
|
||||||
return md.html.Print(destination)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toHTML(dirPath, filename string) (template.HTML, error) {
|
|
||||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
||||||
b, err := ioutil.ReadFile(fpath)
|
b, err := ioutil.ReadFile(fpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("%s: reading file: %v", fpath, err)
|
return "", fmt.Errorf("%s: reading file: %v", fpath, err)
|
||||||
}
|
}
|
||||||
unsafe := blackfriday.Run(b)
|
unsafe := blackfriday.Run(b)
|
||||||
contentHTML := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
content := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||||
return template.HTML(contentHTML), nil
|
/* #nosec */
|
||||||
|
return template.HTML(content), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithHeaderFile sets header content from a file.
|
|
||||||
func (md *Markdown) WithHeaderFile(fpath string) error {
|
|
||||||
if md.html == nil {
|
|
||||||
md.html = &HTML{Context: md.Context}
|
|
||||||
}
|
|
||||||
return md.html.WithHeaderFile(fpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFooterFile sets footer content from a file.
|
|
||||||
func (md *Markdown) WithFooterFile(fpath string) error {
|
|
||||||
if md.html == nil {
|
|
||||||
md.html = &HTML{Context: md.Context}
|
|
||||||
}
|
|
||||||
return md.html.WithFooterFile(fpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Printer(new(Markdown))
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMarkdown(t *testing.T) {
|
|
||||||
dirPath := test.MarkdownTestDirPath(t)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
markdown := &Markdown{
|
|
||||||
Context: ctx,
|
|
||||||
TemplatePath: fmt.Sprintf("%s/%s", dirPath, "index.html"),
|
|
||||||
PaperWidth: 8.27,
|
|
||||||
PaperHeight: 11.7,
|
|
||||||
MarginTop: 1,
|
|
||||||
MarginBottom: 1,
|
|
||||||
MarginLeft: 1,
|
|
||||||
MarginRight: 1,
|
|
||||||
}
|
|
||||||
err := markdown.WithHeaderFile(fmt.Sprintf("%s/%s", dirPath, "header.html"))
|
|
||||||
require.Nil(t, err)
|
|
||||||
err = markdown.WithFooterFile(fmt.Sprintf("%s/%s", dirPath, "footer.html"))
|
|
||||||
require.Nil(t, err)
|
|
||||||
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")
|
|
||||||
err = markdown.Print(dst)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.FileExists(t, dst)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
50
internal/pkg/printer/merge.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package printer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type merge struct {
|
||||||
|
ctx context.Context
|
||||||
|
fpaths []string
|
||||||
|
opts *MergeOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeOptions helps customizing the
|
||||||
|
// merge printer behaviour.
|
||||||
|
type MergeOptions struct {
|
||||||
|
WaitTimeout float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMerge returns a merge printer.
|
||||||
|
func NewMerge(fpaths []string, opts *MergeOptions) Printer {
|
||||||
|
return &merge{
|
||||||
|
fpaths: fpaths,
|
||||||
|
opts: opts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *merge) Print(destination string) error {
|
||||||
|
if p.ctx == nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(p.opts.WaitTimeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
p.ctx = ctx
|
||||||
|
}
|
||||||
|
var cmdArgs []string
|
||||||
|
cmdArgs = append(cmdArgs, p.fpaths...)
|
||||||
|
cmdArgs = append(cmdArgs, "cat", "output", destination)
|
||||||
|
cmd := exec.CommandContext(p.ctx, "pdftk", cmdArgs...)
|
||||||
|
_, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pdtk: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
|
var (
|
||||||
|
_ = Printer(new(merge))
|
||||||
|
)
|
||||||
@@ -2,61 +2,89 @@ package printer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
var mu sync.Mutex
|
type office struct {
|
||||||
|
fpaths []string
|
||||||
// Office facilitates Office documents to PDF conversion.
|
opts *OfficeOptions
|
||||||
type Office struct {
|
|
||||||
Context context.Context
|
|
||||||
FilePaths []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print converts Office documents to PDF.
|
// OfficeOptions helps customizing the
|
||||||
func (o *Office) Print(destination string) error {
|
// Office printer behaviour.
|
||||||
mu.Lock()
|
type OfficeOptions struct {
|
||||||
defer mu.Unlock()
|
WaitTimeout float64
|
||||||
fpaths := make([]string, len(o.FilePaths))
|
Landscape bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOffice returns an Office printer.
|
||||||
|
func NewOffice(fpaths []string, opts *OfficeOptions) Printer {
|
||||||
|
return &office{
|
||||||
|
fpaths: fpaths,
|
||||||
|
opts: opts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *office) Print(destination string) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(p.opts.WaitTimeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
fpaths := make([]string, len(p.fpaths))
|
||||||
dirPath := filepath.Dir(destination)
|
dirPath := filepath.Dir(destination)
|
||||||
for i, fpath := range o.FilePaths {
|
for i, fpath := range p.fpaths {
|
||||||
baseFilename, err := rand.Get()
|
baseFilename, err := rand.Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tmpDest := fmt.Sprintf("%s/%s.pdf", dirPath, baseFilename)
|
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
|
||||||
cmd := exec.CommandContext(
|
if err := unoconv(ctx, fpath, tmpDest, p.opts); err != nil {
|
||||||
o.Context,
|
return err
|
||||||
"unoconv",
|
|
||||||
"--format",
|
|
||||||
"pdf",
|
|
||||||
"--output",
|
|
||||||
tmpDest,
|
|
||||||
fpath,
|
|
||||||
)
|
|
||||||
_, err = cmd.Output()
|
|
||||||
if o.Context.Err() == context.DeadlineExceeded {
|
|
||||||
return errors.New("unoconv: command timed out")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unoconv: non-zero exit code: %v", err)
|
|
||||||
}
|
}
|
||||||
fpaths[i] = tmpDest
|
fpaths[i] = tmpDest
|
||||||
}
|
}
|
||||||
if len(fpaths) == 1 {
|
if len(fpaths) == 1 {
|
||||||
return os.Rename(fpaths[0], destination)
|
return os.Rename(fpaths[0], destination)
|
||||||
}
|
}
|
||||||
return Merge(fpaths, destination)
|
m := &merge{
|
||||||
|
ctx: ctx,
|
||||||
|
fpaths: fpaths,
|
||||||
|
}
|
||||||
|
return m.Print(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint: gochecknoglobals
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
func unoconv(ctx context.Context, fpath, destination string, opts *OfficeOptions) error {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
cmdArgs := []string{
|
||||||
|
"--format",
|
||||||
|
"pdf",
|
||||||
|
}
|
||||||
|
if opts.Landscape {
|
||||||
|
cmdArgs = append(cmdArgs, "--printer", "PaperOrientation=landscape")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "--output", destination, fpath)
|
||||||
|
cmd := exec.CommandContext(
|
||||||
|
ctx,
|
||||||
|
"unoconv",
|
||||||
|
cmdArgs...,
|
||||||
|
)
|
||||||
|
_, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unoconv: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
// Compile-time checks to ensure type implements desired interfaces.
|
||||||
var (
|
var (
|
||||||
_ = Printer(new(Office))
|
_ = Printer(new(office))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestOffice(t *testing.T) {
|
|
||||||
dirPath := test.OfficeTestDirPath(t)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
office := &Office{
|
|
||||||
Context: ctx,
|
|
||||||
FilePaths: []string{
|
|
||||||
fmt.Sprintf("%s/%s", dirPath, "document.docx"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")
|
|
||||||
err := office.Print(dst)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.FileExists(t, dst)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,7 @@
|
|||||||
package printer
|
package printer
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
pdfcpuAPI "github.com/hhrutter/pdfcpu/pkg/api"
|
|
||||||
pdfcpuConfig "github.com/hhrutter/pdfcpu/pkg/pdfcpu"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Printer is a type that can create a PDF file from a source.
|
// Printer is a type that can create a PDF file from a source.
|
||||||
// The source is defined in the underlying implementation.
|
// The source is defined in the underlying implementation.
|
||||||
type Printer interface {
|
type Printer interface {
|
||||||
Print(destination string) error
|
Print(destination string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge merges PDF files.
|
|
||||||
func Merge(fpaths []string, destination string) error {
|
|
||||||
cmd := pdfcpuAPI.MergeCommand(fpaths, destination, pdfcpuConfig.NewDefaultConfiguration())
|
|
||||||
_, err := pdfcpuAPI.Merge(cmd)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeBytesToFile(dst string, b []byte) error {
|
|
||||||
if err := ioutil.WriteFile(dst, b, 0644); err != nil {
|
|
||||||
return fmt.Errorf("%s: writing file: %v", dst, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package printer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
|
||||||
dirPath := test.PDFTestDirPath(t)
|
|
||||||
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")
|
|
||||||
err := Merge(
|
|
||||||
[]string{
|
|
||||||
fmt.Sprintf("%s/%s", dirPath, "gotenberg.pdf"),
|
|
||||||
fmt.Sprintf("%s/%s", dirPath, "gotenberg.pdf"),
|
|
||||||
},
|
|
||||||
dst,
|
|
||||||
)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.FileExists(t, dst)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
9
internal/pkg/printer/url.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package printer
|
||||||
|
|
||||||
|
// NewURL returns a URL printer.
|
||||||
|
func NewURL(url string, opts *ChromeOptions) Printer {
|
||||||
|
return &chrome{
|
||||||
|
url: url,
|
||||||
|
opts: opts,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Gotenberg Go client
|
|
||||||
|
|
||||||
A simple Go client for interacting with a Gotenberg API.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ go get -u github.com/thecodingmachine/gotenberg
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```golang
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// HTML conversion example.
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
AssetFilePaths: []string{
|
|
||||||
"style.css",
|
|
||||||
"img.png",
|
|
||||||
},
|
|
||||||
Options: &gotenberg.HTMLOptions{
|
|
||||||
HeaderFilePath: "header.html",
|
|
||||||
FooterFilePath: "footer.html",
|
|
||||||
PaperSize: gotenberg.A4,
|
|
||||||
PaperMargins: gotenberg.NormalMargins,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "foo.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For more complete usages, head to the [documentation](https://thecodingmachine.github.io/gotenberg).
|
|
||||||
155
pkg/client.go
@@ -1,155 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
webhookURL string = "webhookURL"
|
|
||||||
paperWidth string = "paperWidth"
|
|
||||||
paperHeight string = "paperHeight"
|
|
||||||
marginTop string = "marginTop"
|
|
||||||
marginBottom string = "marginBottom"
|
|
||||||
marginLeft string = "marginLeft"
|
|
||||||
marginRight string = "marginRight"
|
|
||||||
landscape string = "landscape"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// A3 paper size.
|
|
||||||
A3 = [2]float64{11.7, 16.5}
|
|
||||||
// A4 paper size.
|
|
||||||
A4 = [2]float64{8.27, 11.7}
|
|
||||||
// A5 paper size.
|
|
||||||
A5 = [2]float64{5.8, 8.3}
|
|
||||||
// A6 paper size.
|
|
||||||
A6 = [2]float64{4.1, 5.8}
|
|
||||||
// Letter paper size.
|
|
||||||
Letter = [2]float64{8.5, 11}
|
|
||||||
// Legal paper size.
|
|
||||||
Legal = [2]float64{8.5, 14}
|
|
||||||
// Tabloid paper size.
|
|
||||||
Tabloid = [2]float64{11, 17}
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// NoMargins removes margins.
|
|
||||||
NoMargins = [4]float64{0, 0, 0, 0}
|
|
||||||
// NormalMargins uses 1 inche margins.
|
|
||||||
NormalMargins = [4]float64{1, 1, 1, 1}
|
|
||||||
// LargeMargins uses 2 inche margins.
|
|
||||||
LargeMargins = [4]float64{2, 2, 2, 2}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client facilitates interacting with
|
|
||||||
// the Gotenberg API.
|
|
||||||
type Client struct {
|
|
||||||
Hostname string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request is a type for sending
|
|
||||||
// form values and form files to
|
|
||||||
// the Gotenberg API.
|
|
||||||
type Request interface {
|
|
||||||
validate() error
|
|
||||||
getPostURL() string
|
|
||||||
getFormValues() map[string]string
|
|
||||||
getFormFiles() map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post sends a request to the Gotenberg API
|
|
||||||
// and returns the response.
|
|
||||||
func (c *Client) Post(req Request) (*http.Response, error) {
|
|
||||||
if err := req.validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
body, contentType, err := multipartForm(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
URL := fmt.Sprintf("%s%s", c.Hostname, req.getPostURL())
|
|
||||||
resp, err := http.Post(URL, contentType, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store creates the resulting PDF to given destination.
|
|
||||||
func (c *Client) Store(req Request, dest string) error {
|
|
||||||
if hasWebhook(req) {
|
|
||||||
return errors.New("cannot use Store method with a webhook")
|
|
||||||
}
|
|
||||||
resp, err := c.Post(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return writeNewFile(dest, resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasWebhook(req Request) bool {
|
|
||||||
webhookURL, ok := req.getFormValues()[webhookURL]
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return webhookURL != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeNewFile(fpath string, in io.Reader) error {
|
|
||||||
if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil {
|
|
||||||
return fmt.Errorf("%s: making directory for file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
out, err := os.Create(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: creating new file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
err = out.Chmod(0644)
|
|
||||||
if err != nil && runtime.GOOS != "windows" {
|
|
||||||
return fmt.Errorf("%s: changing file mode: %v", fpath, err)
|
|
||||||
}
|
|
||||||
_, err = io.Copy(out, in)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: writing file: %v", fpath, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fileExists(name string) bool {
|
|
||||||
_, err := os.Stat(name)
|
|
||||||
return !os.IsNotExist(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func multipartForm(req Request) (*bytes.Buffer, string, error) {
|
|
||||||
body := &bytes.Buffer{}
|
|
||||||
writer := multipart.NewWriter(body)
|
|
||||||
defer writer.Close()
|
|
||||||
for filename, fpath := range req.getFormFiles() {
|
|
||||||
in, err := os.Open(fpath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("%s: opening file: %v", filename, err)
|
|
||||||
}
|
|
||||||
part, err := writer.CreateFormFile("files", filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("%s: creating form file: %v", filename, err)
|
|
||||||
}
|
|
||||||
_, err = io.Copy(part, in)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("%s: copying file: %v", filename, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for name, value := range req.getFormValues() {
|
|
||||||
if err := writer.WriteField(name, value); err != nil {
|
|
||||||
return nil, "", fmt.Errorf("%s: writing form field: %v", name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return body, writer.FormDataContentType(), nil
|
|
||||||
}
|
|
||||||
31
pkg/doc.go
@@ -1,31 +0,0 @@
|
|||||||
/*
|
|
||||||
Package gotenberg is a Go client for
|
|
||||||
interacting with a Gotenberg API.
|
|
||||||
|
|
||||||
For instance, if you want to convert HTML:
|
|
||||||
|
|
||||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// HTML conversion example.
|
|
||||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &gotenberg.HTMLRequest{
|
|
||||||
IndexFilePath: "index.html",
|
|
||||||
AssetFilePaths: []string{
|
|
||||||
"style.css",
|
|
||||||
"img.png",
|
|
||||||
},
|
|
||||||
Options: &gotenberg.HTMLOptions{
|
|
||||||
HeaderFilePath: "header.html",
|
|
||||||
FooterFilePath: "footer.html",
|
|
||||||
PaperSize: gotenberg.A4,
|
|
||||||
PaperMargins: gotenberg.NormalMargins,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dest := "foo.pdf"
|
|
||||||
c.Store(req, dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
For more complete usages, head to the https://thecodingmachine.gotenberg.github.io.
|
|
||||||
*/
|
|
||||||
package gotenberg
|
|
||||||
78
pkg/html.go
@@ -1,78 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTMLRequest facilitates HTML conversion
|
|
||||||
// with the Gotenberg API.
|
|
||||||
type HTMLRequest struct {
|
|
||||||
IndexFilePath string
|
|
||||||
AssetFilePaths []string
|
|
||||||
Options *HTMLOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTMLOptions gathers all options
|
|
||||||
// for HTML conversion with the Gotenberg API.
|
|
||||||
type HTMLOptions struct {
|
|
||||||
WebHookURL string
|
|
||||||
HeaderFilePath string
|
|
||||||
FooterFilePath string
|
|
||||||
PaperSize [2]float64
|
|
||||||
PaperMargins [4]float64
|
|
||||||
Landscape bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (html *HTMLRequest) validate() error {
|
|
||||||
if !fileExists(html.IndexFilePath) {
|
|
||||||
return fmt.Errorf("%s: index file does not exist", html.IndexFilePath)
|
|
||||||
}
|
|
||||||
if html.Options.HeaderFilePath != "" && !fileExists(html.Options.HeaderFilePath) {
|
|
||||||
return fmt.Errorf("%s: header file does not exist", html.Options.HeaderFilePath)
|
|
||||||
}
|
|
||||||
if html.Options.FooterFilePath != "" && !fileExists(html.Options.FooterFilePath) {
|
|
||||||
return fmt.Errorf("%s: footer file does not exist", html.Options.FooterFilePath)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (html *HTMLRequest) getPostURL() string {
|
|
||||||
return "/convert/html"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (html *HTMLRequest) getFormValues() map[string]string {
|
|
||||||
if html.Options == nil {
|
|
||||||
html.Options = &HTMLOptions{}
|
|
||||||
}
|
|
||||||
values := make(map[string]string)
|
|
||||||
values[webhookURL] = html.Options.WebHookURL
|
|
||||||
values[paperWidth] = fmt.Sprintf("%f", html.Options.PaperSize[0])
|
|
||||||
values[paperHeight] = fmt.Sprintf("%f", html.Options.PaperSize[1])
|
|
||||||
values[marginTop] = fmt.Sprintf("%f", html.Options.PaperMargins[0])
|
|
||||||
values[marginBottom] = fmt.Sprintf("%f", html.Options.PaperMargins[1])
|
|
||||||
values[marginLeft] = fmt.Sprintf("%f", html.Options.PaperMargins[2])
|
|
||||||
values[marginRight] = fmt.Sprintf("%f", html.Options.PaperMargins[3])
|
|
||||||
values[landscape] = strconv.FormatBool(html.Options.Landscape)
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
func (html *HTMLRequest) getFormFiles() map[string]string {
|
|
||||||
if html.Options == nil {
|
|
||||||
html.Options = &HTMLOptions{}
|
|
||||||
}
|
|
||||||
files := make(map[string]string)
|
|
||||||
files["index.html"] = html.IndexFilePath
|
|
||||||
files["header.html"] = html.Options.HeaderFilePath
|
|
||||||
files["footer.html"] = html.Options.FooterFilePath
|
|
||||||
for _, fpath := range html.AssetFilePaths {
|
|
||||||
files[filepath.Base(fpath)] = fpath
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Request(new(HTMLRequest))
|
|
||||||
)
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHTML(t *testing.T) {
|
|
||||||
c := &Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &HTMLRequest{
|
|
||||||
IndexFilePath: test.HTMLTestFilePath(t, "index.html"),
|
|
||||||
AssetFilePaths: []string{
|
|
||||||
test.HTMLTestFilePath(t, "font.woff"),
|
|
||||||
test.HTMLTestFilePath(t, "img.gif"),
|
|
||||||
test.HTMLTestFilePath(t, "style.css"),
|
|
||||||
},
|
|
||||||
Options: &HTMLOptions{
|
|
||||||
HeaderFilePath: test.HTMLTestFilePath(t, "header.html"),
|
|
||||||
FooterFilePath: test.HTMLTestFilePath(t, "footer.html"),
|
|
||||||
PaperSize: A4,
|
|
||||||
PaperMargins: NormalMargins,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dirPath, err := rand.Get()
|
|
||||||
require.Nil(t, err)
|
|
||||||
dest := fmt.Sprintf("%s/foo.pdf", dirPath)
|
|
||||||
err = c.Store(req, dest)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.FileExists(t, dest)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MarkdownRequest facilitates Markdown conversion
|
|
||||||
// with the Gotenberg API.
|
|
||||||
type MarkdownRequest struct {
|
|
||||||
IndexFilePath string
|
|
||||||
MarkdownFilePaths []string
|
|
||||||
AssetFilePaths []string
|
|
||||||
Options *MarkdownOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkdownOptions gathers all options
|
|
||||||
// for Markdown conversion with the Gotenberg API.
|
|
||||||
type MarkdownOptions struct {
|
|
||||||
WebHookURL string
|
|
||||||
HeaderFilePath string
|
|
||||||
FooterFilePath string
|
|
||||||
PaperSize [2]float64
|
|
||||||
PaperMargins [4]float64
|
|
||||||
Landscape bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (markdown *MarkdownRequest) validate() error {
|
|
||||||
if !fileExists(markdown.IndexFilePath) {
|
|
||||||
return fmt.Errorf("%s: index file does not exist", markdown.IndexFilePath)
|
|
||||||
}
|
|
||||||
if markdown.Options.HeaderFilePath != "" && !fileExists(markdown.Options.HeaderFilePath) {
|
|
||||||
return fmt.Errorf("%s: header file does not exist", markdown.Options.HeaderFilePath)
|
|
||||||
}
|
|
||||||
if markdown.Options.FooterFilePath != "" && !fileExists(markdown.Options.FooterFilePath) {
|
|
||||||
return fmt.Errorf("%s: footer file does not exist", markdown.Options.FooterFilePath)
|
|
||||||
}
|
|
||||||
for _, fpath := range markdown.MarkdownFilePaths {
|
|
||||||
if !fileExists(fpath) {
|
|
||||||
return fmt.Errorf("%s: markdown file does not exist", fpath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (markdown *MarkdownRequest) getPostURL() string {
|
|
||||||
return "/convert/markdown"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (markdown *MarkdownRequest) getFormValues() map[string]string {
|
|
||||||
if markdown.Options == nil {
|
|
||||||
markdown.Options = &MarkdownOptions{}
|
|
||||||
}
|
|
||||||
values := make(map[string]string)
|
|
||||||
values[webhookURL] = markdown.Options.WebHookURL
|
|
||||||
values[paperWidth] = fmt.Sprintf("%f", markdown.Options.PaperSize[0])
|
|
||||||
values[paperHeight] = fmt.Sprintf("%f", markdown.Options.PaperSize[1])
|
|
||||||
values[marginTop] = fmt.Sprintf("%f", markdown.Options.PaperMargins[0])
|
|
||||||
values[marginBottom] = fmt.Sprintf("%f", markdown.Options.PaperMargins[1])
|
|
||||||
values[marginLeft] = fmt.Sprintf("%f", markdown.Options.PaperMargins[2])
|
|
||||||
values[marginRight] = fmt.Sprintf("%f", markdown.Options.PaperMargins[3])
|
|
||||||
values[landscape] = strconv.FormatBool(markdown.Options.Landscape)
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
func (markdown *MarkdownRequest) getFormFiles() map[string]string {
|
|
||||||
if markdown.Options == nil {
|
|
||||||
markdown.Options = &MarkdownOptions{}
|
|
||||||
}
|
|
||||||
files := make(map[string]string)
|
|
||||||
files["index.html"] = markdown.IndexFilePath
|
|
||||||
files["header.html"] = markdown.Options.HeaderFilePath
|
|
||||||
files["footer.html"] = markdown.Options.FooterFilePath
|
|
||||||
for _, fpath := range markdown.MarkdownFilePaths {
|
|
||||||
files[filepath.Base(fpath)] = fpath
|
|
||||||
}
|
|
||||||
for _, fpath := range markdown.AssetFilePaths {
|
|
||||||
files[filepath.Base(fpath)] = fpath
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Request(new(MarkdownRequest))
|
|
||||||
)
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMarkdown(t *testing.T) {
|
|
||||||
c := &Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &MarkdownRequest{
|
|
||||||
IndexFilePath: test.MarkdownTestFilePath(t, "index.html"),
|
|
||||||
MarkdownFilePaths: []string{
|
|
||||||
test.MarkdownTestFilePath(t, "paragraph1.md"),
|
|
||||||
test.MarkdownTestFilePath(t, "paragraph2.md"),
|
|
||||||
test.MarkdownTestFilePath(t, "paragraph3.md"),
|
|
||||||
},
|
|
||||||
AssetFilePaths: []string{
|
|
||||||
test.HTMLTestFilePath(t, "font.woff"),
|
|
||||||
test.HTMLTestFilePath(t, "img.gif"),
|
|
||||||
test.HTMLTestFilePath(t, "style.css"),
|
|
||||||
},
|
|
||||||
Options: &MarkdownOptions{
|
|
||||||
HeaderFilePath: test.MarkdownTestFilePath(t, "header.html"),
|
|
||||||
FooterFilePath: test.MarkdownTestFilePath(t, "footer.html"),
|
|
||||||
PaperSize: A4,
|
|
||||||
PaperMargins: NormalMargins,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dirPath, err := rand.Get()
|
|
||||||
require.Nil(t, err)
|
|
||||||
dest := fmt.Sprintf("%s/foo.pdf", dirPath)
|
|
||||||
err = c.Store(req, dest)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.FileExists(t, dest)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
55
pkg/merge.go
@@ -1,55 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MergeRequest facilitates merging PDF
|
|
||||||
// with the Gotenberg API.
|
|
||||||
type MergeRequest struct {
|
|
||||||
FilePaths []string
|
|
||||||
Options *MergeOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeOptions gathers all options
|
|
||||||
// for merging PDF
|
|
||||||
// with the Gotenberg API.
|
|
||||||
type MergeOptions struct {
|
|
||||||
WebHookURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (merge *MergeRequest) validate() error {
|
|
||||||
for _, fpath := range merge.FilePaths {
|
|
||||||
if !fileExists(fpath) {
|
|
||||||
return fmt.Errorf("%s: PDF file does not exist", fpath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (merge *MergeRequest) getPostURL() string {
|
|
||||||
return "/merge"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (merge *MergeRequest) getFormValues() map[string]string {
|
|
||||||
if merge.Options == nil {
|
|
||||||
merge.Options = &MergeOptions{}
|
|
||||||
}
|
|
||||||
values := make(map[string]string)
|
|
||||||
values[webhookURL] = merge.Options.WebHookURL
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
func (merge *MergeRequest) getFormFiles() map[string]string {
|
|
||||||
files := make(map[string]string)
|
|
||||||
for _, fpath := range merge.FilePaths {
|
|
||||||
files[filepath.Base(fpath)] = fpath
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Request(new(MergeRequest))
|
|
||||||
)
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
|
||||||
"github.com/thecodingmachine/gotenberg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMerge(t *testing.T) {
|
|
||||||
c := &Client{Hostname: "http://localhost:3000"}
|
|
||||||
req := &MergeRequest{
|
|
||||||
FilePaths: []string{
|
|
||||||
test.PDFTestFilePath(t, "gotenberg.pdf"),
|
|
||||||
test.PDFTestFilePath(t, "gotenberg.pdf"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dirPath, err := rand.Get()
|
|
||||||
require.Nil(t, err)
|
|
||||||
dest := fmt.Sprintf("%s/foo.pdf", dirPath)
|
|
||||||
err = c.Store(req, dest)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.FileExists(t, dest)
|
|
||||||
err = os.RemoveAll(dirPath)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package gotenberg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OfficeRequest facilitates Office documents
|
|
||||||
// conversion with the Gotenberg API.
|
|
||||||
type OfficeRequest struct {
|
|
||||||
FilePaths []string
|
|
||||||
Options *OfficeOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// OfficeOptions gathers all options
|
|
||||||
// for Office documents conversion
|
|
||||||
// with the Gotenberg API.
|
|
||||||
type OfficeOptions struct {
|
|
||||||
WebHookURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (office *OfficeRequest) validate() error {
|
|
||||||
for _, fpath := range office.FilePaths {
|
|
||||||
if !fileExists(fpath) {
|
|
||||||
return fmt.Errorf("%s: office file does not exist", fpath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (office *OfficeRequest) getPostURL() string {
|
|
||||||
return "/convert/office"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (office *OfficeRequest) getFormValues() map[string]string {
|
|
||||||
if office.Options == nil {
|
|
||||||
office.Options = &OfficeOptions{}
|
|
||||||
}
|
|
||||||
values := make(map[string]string)
|
|
||||||
values[webhookURL] = office.Options.WebHookURL
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
func (office *OfficeRequest) getFormFiles() map[string]string {
|
|
||||||
files := make(map[string]string)
|
|
||||||
for _, fpath := range office.FilePaths {
|
|
||||||
files[filepath.Base(fpath)] = fpath
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time checks to ensure type implements desired interfaces.
|
|
||||||
var (
|
|
||||||
_ = Request(new(OfficeRequest))
|
|
||||||
)
|
|
||||||