Compare commits
47 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 |
7
.github/CONTRIBUTING.md
vendored
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
|
||||
## Contributing code
|
||||
@@ -8,8 +8,9 @@ find below useful information about how to contribute to the Gotenberg project.
|
||||
### Install from sources
|
||||
|
||||
1. Install and run the latest version of Docker
|
||||
2. Fork this repository
|
||||
3. Clone it to your local Go environment (requires *Go* >= 1.11)
|
||||
2. Verify your Go version (>= 1.12)
|
||||
3. Fork this repository
|
||||
4. Clone it outside of your `GOPATH` (we're using Go modules)
|
||||
|
||||
### Working with git
|
||||
|
||||
|
||||
@@ -22,4 +22,4 @@ jobs:
|
||||
script: make tests
|
||||
- stage: publish
|
||||
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
|
||||
|
||||
Copyright (c) 2018 TheCodingMachine
|
||||
Copyright (c) 2019 TheCodingMachine
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
14
Makefile
@@ -1,13 +1,17 @@
|
||||
GOLANG_VERSION=1.12
|
||||
VERSION=snapshot
|
||||
GOLANG_VERSION=1.11.2
|
||||
DOCKER_USER=
|
||||
DOCKER_PASSWORD=
|
||||
|
||||
# generate documentation.
|
||||
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.
|
||||
fmt:
|
||||
go fmt ./...
|
||||
go mod tidy
|
||||
|
||||
# run all linters.
|
||||
lint:
|
||||
@@ -27,4 +31,8 @@ image:
|
||||
|
||||
# start the API using previously built Docker image.
|
||||
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)
|
||||
13
README.md
@@ -1,8 +1,9 @@
|
||||
<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>
|
||||
<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 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
|
||||
|
||||
* 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)
|
||||
* Performance :zap:: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
|
||||
* 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
|
||||
* 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!
|
||||
* Easily interact with the API using our [Go](/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
|
||||
|
||||
## Quick start
|
||||
|
||||
Open a terminal and run the following command:
|
||||
|
||||
```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`.
|
||||
@@ -32,7 +33,7 @@ to learn how to interact with it!
|
||||
|
||||
## Badges
|
||||
|
||||
[](https://microbadger.com/images/thecodingmachine/gotenberg:3)
|
||||
[](https://microbadger.com/images/thecodingmachine/gotenberg:5)
|
||||
[](https://travis-ci.org/thecodingmachine/gotenberg)
|
||||
[](https://godoc.org/github.com/thecodingmachine/gotenberg)
|
||||
[](https://goreportcard.com/report/thecodingmachine/gotenberg)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
FROM debian:stretch-slim
|
||||
|
||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
FROM thecodingmachine/gotenberg:3.2.0 AS hack
|
||||
FROM debian:9.5-slim
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | 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 &&\
|
||||
apt-get update &&\
|
||||
apt-get install -y curl wget
|
||||
apt-get install -y curl wget python3-pip ttf-mscorefonts-installer
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | 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).
|
||||
# |
|
||||
|
||||
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
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | 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 - &&\
|
||||
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 -y install google-chrome-stable
|
||||
apt-get -y --allow-unauthenticated install google-chrome-stable
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | 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>
|
||||
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.
|
||||
|
||||
* 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)
|
||||
* Performance: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
|
||||
* 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
|
||||
* 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!
|
||||
* 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:
|
||||
|
||||
```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).
|
||||
|
||||
Or add it in your Docker Compose stack:
|
||||
## Docker Compose
|
||||
|
||||
You may also add it in your Docker Compose stack:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
@@ -22,32 +24,19 @@ services:
|
||||
# your others services
|
||||
|
||||
gotenberg:
|
||||
image: thecodingmachine/gotenberg:3
|
||||
image: thecodingmachine/gotenberg:5
|
||||
```
|
||||
|
||||
> The API will be available under `gotenberg:3000` in your Docker Compose network.
|
||||
|
||||
## 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
|
||||
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
|
||||
```
|
||||
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
|
||||
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).
|
||||
@@ -33,19 +33,17 @@ $ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := &gotenberg.HTMLRequest{
|
||||
IndexFilePath: "index.html",
|
||||
}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -109,23 +107,19 @@ $ curl --request POST \
|
||||
--form files=@index.html \
|
||||
--form files=@header.html \
|
||||
--form files=@footer.html \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := &gotenberg.HTMLRequest{
|
||||
IndexFilePath: "index.html",
|
||||
Options: &gotenberg.HTMLOptions{
|
||||
HeaderFilePath: "header.html",
|
||||
FooterFilePath: "footer.html",
|
||||
},
|
||||
}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.Header("header.html")
|
||||
req.Footer("footer.html")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -191,7 +185,7 @@ But this won't:
|
||||
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).
|
||||
> see to the [fonts section](#fonts).
|
||||
|
||||
### cURL
|
||||
|
||||
@@ -203,24 +197,18 @@ $ curl --request POST \
|
||||
--form files=@style.css \
|
||||
--form files=@img.png \
|
||||
--form files=@font.woff \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := &gotenberg.HTMLRequest{
|
||||
IndexFilePath: "index.html",
|
||||
AssetFilePaths: []string{
|
||||
"style.css",
|
||||
"img.png",
|
||||
"font.woff",
|
||||
},
|
||||
}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.Assets("font.woff", "img.gif", "style.css")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -242,8 +230,8 @@ $assets = [
|
||||
];
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setAssets($assets);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Paper size, margins, orientation
|
||||
@@ -252,8 +240,7 @@ 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`.
|
||||
> Also, you have to set both `paperWidth` and `paperHeight`. Same for margins.
|
||||
> Paper size and margins have to be provided in `inches`. Same for margins.
|
||||
|
||||
### cURL
|
||||
|
||||
@@ -269,24 +256,20 @@ $ curl --request POST \
|
||||
--form marginLeft=0 \
|
||||
--form marginRight=0 \
|
||||
--form landscape=true \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
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
|
||||
},
|
||||
}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.PaperSize(gotenberg.A4)
|
||||
req.Margins(gotenberg.NoMargins)
|
||||
req.Landscape(true)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -306,6 +289,54 @@ $request = new HTMLRequest($index);
|
||||
$request->setPaperSize(Request::A4);
|
||||
$request->setMargins(Request::NO_MARGINS);
|
||||
$request->setLandscape(true);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
$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);
|
||||
```
|
||||
@@ -36,22 +36,17 @@ $ curl --request POST \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form files=@file.md \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := &gotenberg.MarkdownRequest{
|
||||
IndexFilePath: "index.html",
|
||||
MarkdownFilePaths: []string{
|
||||
"file.md",
|
||||
},
|
||||
}
|
||||
req, _ := gotenberg.NewMarkdownRequest("index.html", "file.md")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -70,6 +65,6 @@ $markdowns = [
|
||||
DocumentFactory::makeFromPath('file.md', 'file.md'),
|
||||
];
|
||||
$request = new MarkdownRequest($index, $markdowns);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
@@ -10,6 +10,9 @@ It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
You may send one or more Office documents. Following file extensions are accepted:
|
||||
|
||||
* `.txt`
|
||||
* `.rtf`
|
||||
* `.fodt`
|
||||
* `.doc`
|
||||
* `.docx`
|
||||
* `.odt`
|
||||
@@ -34,22 +37,17 @@ $ curl --request POST \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@document.docx \
|
||||
--form files=@document2.docx \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := &gotenberg.OfficeRequest{
|
||||
FilePaths: []string{
|
||||
"document.docx",
|
||||
"document2.docx",
|
||||
},
|
||||
}
|
||||
req, _ := gotenberg.NewOfficeRequest("document.docx", "document2.docx")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -68,18 +66,54 @@ $files = [
|
||||
DocumentFactory::makeFromPath('document2.docx', 'document2.docx'),
|
||||
];
|
||||
$request = new OfficeRequest($files);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Fonts
|
||||
## Orientation
|
||||
|
||||
By default, only `ttf-mscorefonts` fonts are installed.
|
||||
You may also customize the resulting PDF format.
|
||||
|
||||
If you wish to use more fonts, you will have to create your own image:
|
||||
By default, it will be rendered with `portrait` orientation.
|
||||
|
||||
```Dockerfile
|
||||
FROM thecodingmachine/gotenberg:3
|
||||
### cURL
|
||||
|
||||
RUN apt-get -y install yourfonts
|
||||
```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);
|
||||
```
|
||||
@@ -2,39 +2,36 @@
|
||||
title: Merge
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/merge` for merging PDFs.
|
||||
Gotenberg provides the endpoint `/convert/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
|
||||
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/merge \
|
||||
--url http://localhost:3000/convert/merge \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@file.pdf \
|
||||
--form files=@file2.pdf \
|
||||
> result.pdf
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := &gotenberg.MergeRequest{
|
||||
FilePaths: []string{
|
||||
"file.pdf",
|
||||
"file2.pdf",
|
||||
},
|
||||
}
|
||||
req, _ := gotenberg.NewMergeRequest("file.pdf", "file2.pdf")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
@@ -53,6 +50,6 @@ $files = [
|
||||
DocumentFactory::makeFromPath('file2.pdf', 'file2.pdf'),
|
||||
];
|
||||
$request = new MergeRequest($files);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
$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);
|
||||
```
|
||||
@@ -24,18 +24,13 @@ $ curl --request POST \
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
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)
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.WebhookURL("http://myapp.com/webhook/")
|
||||
resp, _ := c.Post(req)
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
gotenberg:
|
||||
image: thecodingmachine/gotenberg:3
|
||||
image: thecodingmachine/gotenberg:5
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | GolangCI-Lint
|
||||
# |--------------------------------------------------------------------------
|
||||
@@ -12,7 +10,7 @@ LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
# | 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} &&\
|
||||
golangci-lint --version
|
||||
@@ -34,4 +32,4 @@ COPY go.sum .
|
||||
# Install module dependencies.
|
||||
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
|
||||
|
||||
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Common libraries
|
||||
# |--------------------------------------------------------------------------
|
||||
@@ -47,4 +45,4 @@ COPY go.sum .
|
||||
# Install module dependencies.
|
||||
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
|
||||
# spending to much time on each tests depending on
|
||||
# them.
|
||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeLaunch
|
||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvLaunch
|
||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeStart
|
||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvStart
|
||||
|
||||
# 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/pkg/printer
|
||||
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/rand
|
||||
|
||||
# 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 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
|
||||
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvShutdown
|
||||
@@ -1,21 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/app/api"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
|
||||
)
|
||||
|
||||
// version will be set on build time.
|
||||
// nolint: gochecknoglobals
|
||||
var version = "snapshot"
|
||||
|
||||
func main() {
|
||||
notify.Println(fmt.Sprintf("Gotenberg %s", version))
|
||||
if err := api.Start(); err != nil {
|
||||
notify.ErrPrintln(err)
|
||||
const (
|
||||
defaultWaitTimeoutEnvVar = "DEFAULT_WAIT_TIMEOUT"
|
||||
disableGoogleChromeEnvVar = "DISABLE_GOOGLE_CHROME"
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
924
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>
|
||||
<html>
|
||||
<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 http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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-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">
|
||||
</head>
|
||||
<body>
|
||||
@@ -14,7 +34,7 @@
|
||||
<div class="Container">
|
||||
<div class="Header">
|
||||
<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="subtext">{{.Subtitle}}</span>
|
||||
</div>
|
||||
|
||||
24
go.mod
@@ -1,21 +1,23 @@
|
||||
module github.com/thecodingmachine/gotenberg
|
||||
|
||||
go 1.12
|
||||
|
||||
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/gorilla/websocket v1.4.0 // indirect
|
||||
github.com/hhrutter/pdfcpu v0.1.18
|
||||
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73
|
||||
github.com/labstack/echo/v4 v4.0.0
|
||||
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/pkg/errors v0.8.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1
|
||||
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.2.2
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect
|
||||
github.com/stretchr/testify v1.3.0
|
||||
github.com/valyala/fasttemplate v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc // indirect
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f
|
||||
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/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/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/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
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/hhrutter/pdfcpu v0.1.18 h1:GX3d6tZxmQlmB5X9VUyOtx7+HZZpR+8ISLqBineP6dc=
|
||||
github.com/hhrutter/pdfcpu v0.1.18/go.mod h1:iaCmXGnOXPIvoGLELZZc4m/GpIopVsvLQ00thrbu8LU=
|
||||
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/echo/v4 v4.0.0 h1:q1GH+caIXPP7H2StPIdzy/ez9CO0EepqYeUg6vi9SWM=
|
||||
github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno=
|
||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
||||
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.20.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
|
||||
github.com/mafredri/cdp v0.22.0 h1:BV17j8hXLDWczo2SZIAFuOjMpQMIOq5DOcd9sgB2hv0=
|
||||
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/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/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/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/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/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/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
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/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/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-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU=
|
||||
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/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-20190215142949-d0b11bdaac8a/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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
// Start starts the API server on port 3000.
|
||||
func Start() error {
|
||||
e := setup()
|
||||
// start Chrome headless and
|
||||
// unoconv listener with PM2.
|
||||
chrome := &pm2.Chrome{}
|
||||
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)
|
||||
// Options allows to customize the behaviour
|
||||
// of the API.
|
||||
type Options struct {
|
||||
DefaultWaitTimeout float64
|
||||
EnableChromeEndpoints bool
|
||||
EnableUnoconvEndpoints bool
|
||||
}
|
||||
|
||||
func setup() *echo.Echo {
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
e.HidePort = true
|
||||
e.Use(middleware.Logger())
|
||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
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
|
||||
// DefaultOptions returns default options.
|
||||
func DefaultOptions() *Options {
|
||||
return &Options{
|
||||
DefaultWaitTimeout: 10,
|
||||
EnableChromeEndpoints: true,
|
||||
EnableUnoconvEndpoints: true,
|
||||
}
|
||||
}
|
||||
|
||||
func newContext(r *resource) (context.Context, context.CancelFunc) {
|
||||
webhookURL := r.webhookURL()
|
||||
if webhookURL == "" {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
return ctx, cancel
|
||||
// New returns an API.
|
||||
func New(opts *Options) *echo.Echo {
|
||||
api := echo.New()
|
||||
api.HideBanner = true
|
||||
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
|
||||
}
|
||||
|
||||
func print(c echo.Context, p printer.Printer, r *resource) error {
|
||||
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 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
|
||||
if opts.EnableChromeEndpoints {
|
||||
g.POST("/html", convertHTML)
|
||||
g.POST("/url", convertURL)
|
||||
g.POST("/markdown", convertMarkdown)
|
||||
}
|
||||
if opts.EnableUnoconvEndpoints {
|
||||
g.POST("/office", convertOffice)
|
||||
}
|
||||
return api
|
||||
}
|
||||
|
||||
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 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 helps managing the HTTP server behind Gotenberg.
|
||||
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,24 +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.HTMLTestMultipartForm(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)
|
||||
if assert.NoError(t, convertHTML(c)) {
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
|
||||
}
|
||||
}
|
||||
@@ -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,24 +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.MarkdownTestMultipartForm(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)
|
||||
if assert.NoError(t, convertMarkdown(c)) {
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
|
||||
}
|
||||
}
|
||||
@@ -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,24 +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.PDFTestMultipartForm(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)
|
||||
if assert.NoError(t, merge(c)) {
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
|
||||
}
|
||||
}
|
||||
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,24 +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.OfficeTestMultipartForm(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)
|
||||
if assert.NoError(t, convertOffice(c)) {
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
|
||||
}
|
||||
}
|
||||
@@ -3,93 +3,262 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||
)
|
||||
|
||||
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"
|
||||
resultFilename string = "resultFilename"
|
||||
waitTimeout string = "waitTimeout"
|
||||
webhookURL string = "webhookURL"
|
||||
remoteURL string = "remoteURL"
|
||||
waitDelay string = "waitDelay"
|
||||
paperWidth string = "paperWidth"
|
||||
paperHeight string = "paperHeight"
|
||||
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 {
|
||||
values map[string]string
|
||||
dirPath string
|
||||
formValues map[string]string
|
||||
formFilesDirPath string
|
||||
opts *Options
|
||||
}
|
||||
|
||||
func newResource(c echo.Context) (*resource, error) {
|
||||
v := make(map[string]string)
|
||||
v[webhookURL] = c.FormValue(webhookURL)
|
||||
v[paperWidth] = c.FormValue(paperWidth)
|
||||
v[paperHeight] = c.FormValue(paperHeight)
|
||||
v[marginTop] = c.FormValue(marginTop)
|
||||
v[marginBottom] = c.FormValue(marginBottom)
|
||||
v[marginLeft] = c.FormValue(marginLeft)
|
||||
v[marginRight] = c.FormValue(marginRight)
|
||||
v[landscape] = c.FormValue(landscape)
|
||||
type resourceContext struct {
|
||||
echo.Context
|
||||
opts *Options
|
||||
resource *resource
|
||||
}
|
||||
|
||||
func newResource(ctx *resourceContext) (*resource, error) {
|
||||
r := &resource{
|
||||
formValues: formValues(ctx),
|
||||
opts: ctx.opts,
|
||||
}
|
||||
dirPath, err := rand.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return r, err
|
||||
}
|
||||
r.formFilesDirPath = dirPath
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
return nil, fmt.Errorf("%s: making directory: %v", dirPath, err)
|
||||
}
|
||||
r := &resource{values: v, dirPath: dirPath}
|
||||
form, err := c.MultipartForm()
|
||||
if err := formFiles(ctx, dirPath); err != nil {
|
||||
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 {
|
||||
return nil, fmt.Errorf("getting multipart form: %v", err)
|
||||
return fmt.Errorf("getting multipart form: %v", err)
|
||||
}
|
||||
for _, files := range form.File {
|
||||
for _, fh := range files {
|
||||
in, err := fh.Open()
|
||||
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()
|
||||
if err := r.writeFile(fh.Filename, in); err != nil {
|
||||
return nil, err
|
||||
defer in.Close() // nolint: errcheck
|
||||
fpath := fmt.Sprintf("%s/%s", dirPath, fh.Filename)
|
||||
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
|
||||
}
|
||||
|
||||
func (r *resource) filePath(filename string) (string, error) {
|
||||
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
|
||||
func (r *resource) close() error {
|
||||
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)
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -98,13 +267,28 @@ func (r *resource) filePath(filename string) (string, error) {
|
||||
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
|
||||
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() {
|
||||
return nil
|
||||
}
|
||||
fpath, err := r.filePath(info.Name())
|
||||
fpath, err := r.fpath(info.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -119,66 +303,8 @@ func (r *resource) filePaths(exts []string) ([]string, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(fpaths) == 0 {
|
||||
return nil, fmt.Errorf("no form files found for extensions: %v", exts)
|
||||
}
|
||||
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
|
||||
to display nice outputs to the user.
|
||||
Package notify helps displaying nice outputs
|
||||
to the user.
|
||||
*/
|
||||
package notify
|
||||
|
||||
@@ -7,24 +7,29 @@ import (
|
||||
"github.com/labstack/gommon/color"
|
||||
)
|
||||
|
||||
var (
|
||||
stdout *color.Color
|
||||
stderr *color.Color
|
||||
)
|
||||
|
||||
func init() {
|
||||
stdout = color.New()
|
||||
// Print prints a message to stdout.
|
||||
func Print(message string) {
|
||||
stdout := color.New()
|
||||
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)
|
||||
}
|
||||
|
||||
// ErrPrintln prints an error to stderr.
|
||||
func ErrPrintln(err error) {
|
||||
// Printf prints a formatted message to stdout.
|
||||
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)))
|
||||
}
|
||||
|
||||
@@ -2,30 +2,36 @@ package pm2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/mafredri/cdp/devtool"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
|
||||
)
|
||||
|
||||
// Chrome facilitates starting or shutting down
|
||||
// Chrome headless with PM2.
|
||||
type Chrome struct{}
|
||||
|
||||
// Launch starts Chrome headless with PM2.
|
||||
func (c *Chrome) Launch() error {
|
||||
return launch(c)
|
||||
type chrome struct {
|
||||
manager *processManager
|
||||
}
|
||||
|
||||
// Shutdown stops Chrome headless and
|
||||
// removes it from the list of PM2
|
||||
// processes.
|
||||
func (c *Chrome) Shutdown() error {
|
||||
return shutdown(c)
|
||||
// NewChrome retruns a Google Chrome
|
||||
// headless process.
|
||||
func NewChrome() Process {
|
||||
return &chrome{
|
||||
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{
|
||||
"--no-sandbox",
|
||||
"--headless",
|
||||
@@ -44,28 +50,23 @@ func (c *Chrome) getArgs() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Chrome) getName() string {
|
||||
func (p *chrome) name() string {
|
||||
return "google-chrome-stable"
|
||||
}
|
||||
|
||||
func (c *Chrome) getFullname() string {
|
||||
return "Chrome headless"
|
||||
}
|
||||
|
||||
func (c *Chrome) isViable() bool {
|
||||
// check if Chrome is correctly running.
|
||||
func (p *chrome) viable() bool {
|
||||
// check if Google Chrome is correctly running.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
_, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (c *Chrome) warmup() {
|
||||
notify.Println(fmt.Sprintf("warming-up %s", c.getFullname()))
|
||||
func (p *chrome) warmup() {
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = Process(new(Chrome))
|
||||
_ = Process(new(chrome))
|
||||
)
|
||||
|
||||
@@ -6,14 +6,14 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestChromeLaunch(t *testing.T) {
|
||||
p := &Chrome{}
|
||||
err := p.Launch()
|
||||
func TestChromeStart(t *testing.T) {
|
||||
p := NewChrome()
|
||||
err := p.Start()
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestChromeShutdown(t *testing.T) {
|
||||
p := &Chrome{}
|
||||
p := NewChrome()
|
||||
err := p.Shutdown()
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Package pm2 facilitates starting external
|
||||
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.
|
||||
|
||||
The PM2 process manager launch those processes and keep
|
||||
|
||||
@@ -3,69 +3,81 @@ package pm2
|
||||
import (
|
||||
"fmt"
|
||||
"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.
|
||||
type Process interface {
|
||||
Launch() error
|
||||
Fullname() string
|
||||
Start() error
|
||||
Shutdown() error
|
||||
getArgs() []string
|
||||
getName() string
|
||||
getFullname() string
|
||||
isViable() bool
|
||||
args() []string
|
||||
name() string
|
||||
viable() bool
|
||||
warmup()
|
||||
}
|
||||
|
||||
const maxRestartAttempts int = 5
|
||||
|
||||
var humanNames = map[string]string{
|
||||
"start": "started",
|
||||
"restart": "restarted",
|
||||
"stop": "stopped",
|
||||
type processManager struct {
|
||||
heuristicState int32
|
||||
}
|
||||
|
||||
func launch(p Process) error {
|
||||
if err := run(p, "start"); err != nil {
|
||||
func (m *processManager) start(p Process) error {
|
||||
if err := m.pm2(p, "start"); err != nil {
|
||||
return err
|
||||
}
|
||||
p.warmup()
|
||||
if !p.isViable() {
|
||||
if !p.viable() {
|
||||
attempts := 0
|
||||
for attempts < maxRestartAttempts && !p.isViable() {
|
||||
run(p, "restart")
|
||||
for attempts < 5 && !p.viable() {
|
||||
if err := m.pm2(p, "restart"); err != nil {
|
||||
m.heuristicState = errorState
|
||||
return err
|
||||
}
|
||||
p.warmup()
|
||||
attempts++
|
||||
}
|
||||
if !p.isViable() {
|
||||
return fmt.Errorf("failed to launch %s", p.getFullname())
|
||||
if !p.viable() {
|
||||
m.heuristicState = errorState
|
||||
return fmt.Errorf("failed to launch %s", p.Fullname())
|
||||
}
|
||||
}
|
||||
m.heuristicState = runningState
|
||||
return nil
|
||||
}
|
||||
|
||||
func shutdown(p Process) error {
|
||||
return run(p, "stop")
|
||||
func (m *processManager) shutdown(p Process) error {
|
||||
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{
|
||||
cmdName,
|
||||
p.getName(),
|
||||
p.name(),
|
||||
}
|
||||
if cmdName == "start" {
|
||||
cmdArgs = append(cmdArgs, "--interpreter none", "--")
|
||||
cmdArgs = append(cmdArgs, p.getArgs()...)
|
||||
cmdArgs = append(cmdArgs, p.args()...)
|
||||
}
|
||||
cmd := exec.Command(
|
||||
"pm2",
|
||||
cmdArgs...,
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,47 +1,52 @@
|
||||
package pm2
|
||||
|
||||
// Unoconv facilitates starting or shutting down
|
||||
// unoconv listener with PM2.
|
||||
type Unoconv struct{}
|
||||
|
||||
// Launch starts unoconv listener with PM2.
|
||||
func (u *Unoconv) Launch() error {
|
||||
return launch(u)
|
||||
type unoconv struct {
|
||||
manager *processManager
|
||||
}
|
||||
|
||||
// Shutdown stops unoconv listener and
|
||||
// removes it from the list of PM2
|
||||
// processes.
|
||||
func (u *Unoconv) Shutdown() error {
|
||||
return shutdown(u)
|
||||
// NewUnoconv retruns a unoconv listener
|
||||
// process.
|
||||
func NewUnoconv() Process {
|
||||
return &unoconv{
|
||||
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{
|
||||
"--listener",
|
||||
"--verbose",
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Unoconv) getName() string {
|
||||
func (p *unoconv) name() string {
|
||||
return "unoconv"
|
||||
}
|
||||
|
||||
func (u *Unoconv) getFullname() string {
|
||||
return "unoconv listener"
|
||||
}
|
||||
|
||||
func (u *Unoconv) isViable() bool {
|
||||
func (p *unoconv) viable() bool {
|
||||
// TODO find a way to check if
|
||||
// unoconv is correctly started?
|
||||
// the unoconv listener
|
||||
// is correctly started?
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *Unoconv) warmup() {
|
||||
func (p *unoconv) warmup() {
|
||||
// let's do nothing.
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = Process(new(Unoconv))
|
||||
_ = Process(new(unoconv))
|
||||
)
|
||||
|
||||
@@ -6,14 +6,14 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUnoconvLaunch(t *testing.T) {
|
||||
p := &Unoconv{}
|
||||
err := p.Launch()
|
||||
func TestUnoconvStart(t *testing.T) {
|
||||
p := NewUnoconv()
|
||||
err := p.Start()
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestUnoconvShutdown(t *testing.T) {
|
||||
p := &Unoconv{}
|
||||
p := NewUnoconv()
|
||||
err := p.Shutdown()
|
||||
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
|
||||
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
|
||||
}
|
||||
a specific file type to PDF.
|
||||
*/
|
||||
package printer
|
||||
|
||||
@@ -1,189 +1,14 @@
|
||||
package printer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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.
|
||||
type HTML struct {
|
||||
Context context.Context
|
||||
URL string
|
||||
HeaderHTML string
|
||||
FooterHTML string
|
||||
PaperWidth float64
|
||||
PaperHeight float64
|
||||
MarginTop float64
|
||||
MarginBottom float64
|
||||
MarginLeft float64
|
||||
MarginRight float64
|
||||
Landscape bool
|
||||
// NewHTML returns an HTML printer.
|
||||
func NewHTML(fpath string, opts *ChromeOptions) Printer {
|
||||
URL := fmt.Sprintf("file://%s", fpath)
|
||||
return &chrome{
|
||||
url: URL,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// use the DevTools HTTP/JSON API to manage targets (e.g. pages, webworkers).
|
||||
devt, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||
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, devt.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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
@@ -13,99 +12,48 @@ import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||
)
|
||||
|
||||
// Markdown facilitates Markdown to PDF conversion.
|
||||
type Markdown struct {
|
||||
Context context.Context
|
||||
TemplatePath string
|
||||
HeaderHTML string
|
||||
FooterHTML string
|
||||
PaperWidth float64
|
||||
PaperHeight float64
|
||||
MarginTop float64
|
||||
MarginBottom float64
|
||||
MarginLeft float64
|
||||
MarginRight float64
|
||||
Landscape bool
|
||||
|
||||
html *HTML
|
||||
// NewMarkdown returns a Markdown printer.
|
||||
func NewMarkdown(fpath string, opts *ChromeOptions) (Printer, error) {
|
||||
tmpl, err := template.
|
||||
New(filepath.Base(fpath)).
|
||||
Funcs(template.FuncMap{"toHTML": markdownToHTML}).
|
||||
ParseFiles(fpath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: parsing template: %v", fpath, err)
|
||||
}
|
||||
dirPath := filepath.Dir(fpath)
|
||||
data := &templateData{DirPath: dirPath}
|
||||
var buffer bytes.Buffer
|
||||
if err := tmpl.Execute(&buffer, data); err != nil {
|
||||
return nil, fmt.Errorf("%s: executing template: %v", fpath, err)
|
||||
}
|
||||
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 {
|
||||
DirPath string
|
||||
}
|
||||
|
||||
// Print converts markdown to PDF.
|
||||
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) {
|
||||
func markdownToHTML(dirPath, filename string) (template.HTML, error) {
|
||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
||||
b, err := ioutil.ReadFile(fpath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: reading file: %v", fpath, err)
|
||||
}
|
||||
unsafe := blackfriday.Run(b)
|
||||
contentHTML := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||
return template.HTML(contentHTML), nil
|
||||
content := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||
/* #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 (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
|
||||
)
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
// Office facilitates Office documents to PDF conversion.
|
||||
type Office struct {
|
||||
Context context.Context
|
||||
FilePaths []string
|
||||
type office struct {
|
||||
fpaths []string
|
||||
opts *OfficeOptions
|
||||
}
|
||||
|
||||
// Print converts Office documents to PDF.
|
||||
func (o *Office) Print(destination string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
fpaths := make([]string, len(o.FilePaths))
|
||||
// OfficeOptions helps customizing the
|
||||
// Office printer behaviour.
|
||||
type OfficeOptions struct {
|
||||
WaitTimeout float64
|
||||
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)
|
||||
for i, fpath := range o.FilePaths {
|
||||
for i, fpath := range p.fpaths {
|
||||
baseFilename, err := rand.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpDest := fmt.Sprintf("%s/%s.pdf", dirPath, baseFilename)
|
||||
cmd := exec.CommandContext(
|
||||
o.Context,
|
||||
"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)
|
||||
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
|
||||
if err := unoconv(ctx, fpath, tmpDest, p.opts); err != nil {
|
||||
return err
|
||||
}
|
||||
fpaths[i] = tmpDest
|
||||
}
|
||||
if len(fpaths) == 1 {
|
||||
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.
|
||||
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
|
||||
|
||||
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.
|
||||
// The source is defined in the underlying implementation.
|
||||
type Printer interface {
|
||||
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))
|
||||
)
|
||||
@@ -1,29 +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 TestOffice(t *testing.T) {
|
||||
c := &Client{Hostname: "http://localhost:3000"}
|
||||
req := &OfficeRequest{
|
||||
FilePaths: []string{
|
||||
test.OfficeTestFilePath(t, "document.docx"),
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
set -e
|
||||
|
||||
GOLANG_VERSION=1.11.2
|
||||
VERSION="$1"
|
||||
DOCKER_USER="$2"
|
||||
DOCKER_PASSWORD="$3"
|
||||
GOLANG_VERSION="$1"
|
||||
VERSION="$2"
|
||||
DOCKER_USER="$3"
|
||||
DOCKER_PASSWORD="$4"
|
||||
|
||||
docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD"
|
||||
|
||||
VERSION="${VERSION//v}"
|
||||
SEMVER=( ${VERSION//./ } )
|
||||
VERSION_LENGTH=${#SEMVER[@]}
|
||||
|
||||
@@ -21,11 +22,13 @@ docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
|
||||
docker build \
|
||||
--build-arg GOLANG_VERSION=${GOLANG_VERSION} \
|
||||
--build-arg VERSION=${VERSION} \
|
||||
-t thecodingmachine/gotenberg:latest \
|
||||
-t thecodingmachine/gotenberg:${SEMVER[0]} \
|
||||
-t thecodingmachine/gotenberg:${SEMVER[0]}.${SEMVER[1]} \
|
||||
-t thecodingmachine/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]} \
|
||||
-f build/package/Dockerfile .
|
||||
|
||||
docker push "thecodingmachine/gotenberg:latest"
|
||||
docker push "thecodingmachine/gotenberg:${SEMVER[0]}"
|
||||
docker push "thecodingmachine/gotenberg:${SEMVER[0]}.${SEMVER[1]}"
|
||||
docker push "thecodingmachine/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]}"
|
||||
2196
test/testdata/office/document.rtf
vendored
Normal file
3
test/testdata/office/document.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
Gutenberg
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||