Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
194670c3bf | ||
|
|
c4222e0981 | ||
|
|
61dc4b8bb8 | ||
|
|
df357c1008 | ||
|
|
871b52a288 | ||
|
|
8a4a043e9f | ||
|
|
c0297aa061 | ||
|
|
2aaa2eeaff | ||
|
|
5f62e5bacf | ||
|
|
ab78559710 | ||
|
|
0d75bb1d3e | ||
|
|
6441062e7f | ||
|
|
4827a45f22 | ||
|
|
40eef457ab | ||
|
|
c22a4d109b | ||
|
|
571c3e04c4 | ||
|
|
f9fb97a572 | ||
|
|
e6484bb2d5 | ||
|
|
19f51fdc1f | ||
|
|
25f7ba3ee6 | ||
|
|
a3cd1ff4e1 | ||
|
|
f62c717f79 | ||
|
|
3ed99133d4 | ||
|
|
ab25cc331d | ||
|
|
2648992a9b | ||
|
|
7bcc3d081e | ||
|
|
affd1c45ca | ||
|
|
8c7458812d | ||
|
|
053908c772 | ||
|
|
d623736f9f | ||
|
|
a3e416ff68 | ||
|
|
8bf207b9f7 | ||
|
|
e1520d5e7e | ||
|
|
4f89c9f7c6 | ||
|
|
d34fb92211 | ||
|
|
3f7b006d16 | ||
|
|
a15cf33691 | ||
|
|
4c1699df6c | ||
|
|
404535c512 | ||
|
|
2cedcecc38 | ||
|
|
ba7b02d8d4 | ||
|
|
6aa3c16646 | ||
|
|
5a03e17e00 | ||
|
|
417d145b43 | ||
|
|
e5636a2afa | ||
|
|
5291d2d129 | ||
|
|
c6f6a87499 | ||
|
|
1ef21c6ed6 | ||
|
|
6ee1932cbf | ||
|
|
83cf43a314 | ||
|
|
52c14a79a1 | ||
|
|
152db0551d | ||
|
|
3a2467ccfe | ||
|
|
5a28f3597c | ||
|
|
9ce179f91b | ||
|
|
7aeb072cc3 | ||
|
|
8a652761a3 | ||
|
|
be667575d5 | ||
|
|
59ca8a44a0 | ||
|
|
c9f0d3bfd8 |
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
|
||||
|
||||
|
||||
@@ -11,13 +11,15 @@ env:
|
||||
- secure: CmLh169xwt3OC7l19lDUwdZYfw+l/+XXKjTGxnuJiXHa9hAH+nc4mEUSmqHMHwjquyYE6LnSVGZ94LH8sCaGed/TMm0Lj3DZtZ0VsjYu3sKsNrVJn5WdYEah+GvNJ8i8uzvHnGk1L2fAagLpXFUB2K9UraA1CFiHs1fF5Fua7Ra0mE7lMz0MFvyvzcmc9u8nMz71K2YVsXqn+I/rv8onJCTtc/Ds0hVrBXwG5Fh0T3xHqjOhME9ZI2hRHnnQVdBojSY3USifQ//qAKowfBVc0pebEtgFpyXx/I17A6xCm0KVtnA2tNqu1+XFkjWaBzryaLE9ujhJcTRC/YK0JUZ+QxfYbD5C7jVy9Jlx1lOEjJ42B/ImNxPP+9p3wi2MWNbcbZJPxIe635fnT17lI7CLK2Qg1xjfTQ4WTEKW5IhnYMiyfRKiripFh4PCkDm8cJlTbpntsX1n0uheydbsgf/11vS7o1WGhVQ8+Jelc4DvIbBHCqienQEOLJDBwm7FiK5JbwZJ6ZAGeDM/IhGU1h9PNjVbHUyJhUxYoPYUE9QsZFT7ENCrRQpq49KOzXBRfffyJNJRh+Ep9X5YoW0dUp0MNu5B7z2+6w9X9n/C+loa11xF42bFzniCvvbSOQ37J+EngDinVxJkNPeyivR4fye2ENhIAjAEjouTJLOSXL9nAOU=
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- tests
|
||||
- publish
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: lint
|
||||
- stage: tests
|
||||
script: make lint
|
||||
- stage: tests
|
||||
script: make tests
|
||||
- stage: publish
|
||||
if: tag IS present
|
||||
script: make image VERSION=$TRAVIS_TAG && ./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)
|
||||
15
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,9 +33,9 @@ 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?status.svg)
|
||||
[](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>
|
||||
@@ -1,257 +0,0 @@
|
||||
---
|
||||
title: HTML
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/convert/html` for HTML conversions.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
The only requirement is to send a file named `index.html`: it is the file
|
||||
which will be converted to PDF.
|
||||
|
||||
For instance:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Guzzle
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := &gotenberg.HTMLRequest{
|
||||
IndexFilePath: "index.html",
|
||||
}
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
TODO
|
||||
|
||||
## Header and footer
|
||||
|
||||
You may also add a header and/or a footer in the resulting PDF.
|
||||
Respectively, a file named `header.html` and `footer.html`.
|
||||
|
||||
Each of them **has to be a complete HTML document**:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-size: 8rem;
|
||||
margin: 4rem auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<span class="pageNumber"></span> of <span class="totalPages"></span>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
The following classes allow you to inject printing values:
|
||||
|
||||
* `date`: formatted print date
|
||||
* `title`: document title
|
||||
* `pageNumber`: current page number
|
||||
* `totalPage`: total pages in the document
|
||||
|
||||
> **Attention:** the CSS properties are independant of the ones used in the `index.html` file.
|
||||
> Also, `footer.html` CSS properties override the ones from `header.html`.
|
||||
|
||||
### Guzzle
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html
|
||||
--form files=@header.html
|
||||
--form files=@footer.html
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := &gotenberg.HTMLRequest{
|
||||
IndexFilePath: "index.html",
|
||||
Options: &gotenberg.HTMLOptions{
|
||||
HeaderFilePath: "header.html",
|
||||
FooterFilePath: "footer.html",
|
||||
},
|
||||
}
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
TODO
|
||||
|
||||
## Assets
|
||||
|
||||
You may also send additional files. For instance, images, fonts, stylesheets and so on.
|
||||
|
||||
The only requirement is to make sure that their paths
|
||||
are on the same level as the `index.html` file.
|
||||
|
||||
In others words, this will work:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
<img src="img.png">
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
But this won't:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
<img src="/foo/img.png">
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
You may also use *remote* paths for Google fonts, images and so on.
|
||||
|
||||
> If you want to install fonts directly in the Gotenberg Docker image,
|
||||
> see to the [fonts section](#office.fonts).
|
||||
|
||||
### Guzzle
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html
|
||||
--form files=@style.css
|
||||
--form files=@img.png
|
||||
--form files=@font.woff
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := &gotenberg.HTMLRequest{
|
||||
IndexFilePath: "index.html",
|
||||
AssetFilePaths: []string{
|
||||
"style.css",
|
||||
"img.png",
|
||||
"font.woff",
|
||||
},
|
||||
}
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
TODO
|
||||
|
||||
## Paper size, margins, orientation
|
||||
|
||||
You may also customize the resulting PDF paper.
|
||||
|
||||
By default, it will be rendered in `A4` size with `1 inch` margins and `portrait` orientation.
|
||||
|
||||
> Paper size and margins have to be provided in `inches`.
|
||||
> Also, you have to set both `paperWidth` and `paperHeight`. Same for margins.
|
||||
|
||||
### Guzzle
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html
|
||||
--form paperWidth=8.27
|
||||
--form paperHeight=11.27
|
||||
--form marginTop=0
|
||||
--form marginBottom=0
|
||||
--form marginLeft=0
|
||||
--form marginRight=0
|
||||
--form landscape=true
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := &gotenberg.HTMLRequest{
|
||||
IndexFilePath: "index.html",
|
||||
Options: &gotenberg.HTMLOptions{
|
||||
PaperSize: gotenberg.A4,
|
||||
PaperMargins: gotenberg.NoMargins,
|
||||
Landscape: true
|
||||
},
|
||||
}
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
TODO
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
title: Office
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/convert/office` for Office documents conversions.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
You may send one or more Office documents. Following file extensions are accepted:
|
||||
|
||||
* `.doc`
|
||||
* `.docx`
|
||||
* `.odt`
|
||||
* `.xls`
|
||||
* `.xlsx`
|
||||
* `.ods`
|
||||
* `.ppt`
|
||||
* `.pptx`
|
||||
* `.odp`
|
||||
|
||||
All files will be merged into a single resulting PDF.
|
||||
|
||||
> **Attention:** currently, `unoconv` cannot perform concurrent conversions.
|
||||
> That's why for Office conversions, the API does only one conversion at a time.
|
||||
> See the [scalability section](#scalability) to find how to mitigate this issue.
|
||||
|
||||
### Guzzle
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/office \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@document.docx
|
||||
--form files=@document2.docx
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := &gotenberg.OfficeRequest{
|
||||
FilePaths: []string{
|
||||
"document.docx",
|
||||
"document2.docx",
|
||||
},
|
||||
}
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
TODO
|
||||
|
||||
## Fonts
|
||||
|
||||
By default, only `ttf-mscorefonts` fonts are installed.
|
||||
|
||||
If you wish to use more fonts, you will have to create your own image:
|
||||
|
||||
```Dockerfile
|
||||
FROM thecodingmachine/gotenberg:3
|
||||
|
||||
RUN apt-get -y install yourfonts
|
||||
```
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
title: Merge
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/merge` for merging PDFs.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
Nothing special here: you may send one or more PDF files and the API
|
||||
will merge them and return the resulting PDF file.
|
||||
|
||||
### Guzzle
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/merge \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@file.pdf
|
||||
--form files=@file2.pdf
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := &gotenberg.MergeRequest{
|
||||
FilePaths: []string{
|
||||
"file.pdf",
|
||||
"file2.pdf",
|
||||
},
|
||||
}
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
TODO
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
title: Webhook
|
||||
---
|
||||
|
||||
All endpoints accept a form field named `webhookURL`.
|
||||
|
||||
If provided, the API will send the resulting PDF file in a `POST` request with the `application/pdf` Content-Type
|
||||
to given URL.
|
||||
|
||||
By doing so, your requests to the API will be done before the conversions are actually done!
|
||||
|
||||
## Examples
|
||||
|
||||
### Guzzle
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html
|
||||
--form webhookURL='http://myapp.com/webhook/'
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg/pkg"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := &gotenberg.HTMLRequest{
|
||||
IndexFilePath: "index.html",
|
||||
Options: &gotenberg.HTMLOptions{
|
||||
WebHookURL: "http://myapp.com/webhook/",
|
||||
},
|
||||
}
|
||||
dest := "result.pdf"
|
||||
resp, err := c.Post(req)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
TODO
|
||||
24
build/docs/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
ARG GOLANG_VERSION
|
||||
|
||||
FROM golang:${GOLANG_VERSION}-stretch
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | static
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Installs the static site anti-framework – general-purpose library,
|
||||
# | purpose-built commands for various domains.
|
||||
# |
|
||||
|
||||
RUN go get github.com/apex/static/cmd/static-docs
|
||||
|
||||
# |--------------------------------------------------------------------------
|
||||
# | Final touch
|
||||
# |--------------------------------------------------------------------------
|
||||
# |
|
||||
# | Last instructions of this build.
|
||||
# |
|
||||
|
||||
WORKDIR /docs
|
||||
|
||||
CMD [ "static-docs", "--in", "build/docs/content", "--out", "docs", "--theme", "gotenberg", "--title", "Gotenberg", "--subtitle", "A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF." ]
|
||||
@@ -5,8 +5,8 @@ title: Introduction
|
||||
[Gotenberg](https://github.com/thecodingmachine/gotenberg/) is a Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.
|
||||
|
||||
* 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).
|
||||
342
build/docs/content/04-html.md
Normal file
@@ -0,0 +1,342 @@
|
||||
---
|
||||
title: HTML
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/convert/html` for HTML conversions.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
The only requirement is to send a file named `index.html`: it is the file
|
||||
which will be converted to PDF.
|
||||
|
||||
For instance:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
```
|
||||
|
||||
## Header and footer
|
||||
|
||||
You may also add a header and/or a footer in the resulting PDF.
|
||||
Respectively, a file named `header.html` and `footer.html`.
|
||||
|
||||
Each of them **has to be a complete HTML document**:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-size: 8rem;
|
||||
margin: 4rem auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<span class="pageNumber"></span> of <span class="totalPages"></span>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
The following classes allow you to inject printing values:
|
||||
|
||||
* `date`: formatted print date
|
||||
* `title`: document title
|
||||
* `pageNumber`: current page number
|
||||
* `totalPage`: total pages in the document
|
||||
|
||||
> **Attention:** the CSS properties are independant of the ones used in the `index.html` file.
|
||||
> Also, `footer.html` CSS properties override the ones from `header.html`.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form files=@header.html \
|
||||
--form files=@footer.html \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.Header("header.html")
|
||||
req.Footer("footer.html")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$header = DocumentFactory::makeFromPath('header.html', 'header.html');
|
||||
$footer = DocumentFactory::makeFromPath('footer.html', 'footer.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setHeader($header);
|
||||
$request->setFooter($footer);
|
||||
$dirPath = "/foo";
|
||||
$filename = $client->store($request, $dirPath);
|
||||
```
|
||||
|
||||
## Assets
|
||||
|
||||
You may also send additional files. For instance: images, fonts, stylesheets and so on.
|
||||
|
||||
The only requirement is to make sure that their paths
|
||||
are on the same level as the `index.html` file.
|
||||
|
||||
In others words, this will work:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
<img src="img.png">
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
But this won't:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My PDF</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello world!</h1>
|
||||
<img src="/foo/img.png">
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
You may also use *remote* paths for Google fonts, images and so on.
|
||||
|
||||
> If you want to install fonts directly in the Gotenberg Docker image,
|
||||
> see to the [fonts section](#fonts).
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form files=@style.css \
|
||||
--form files=@img.png \
|
||||
--form files=@font.woff \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.Assets("font.woff", "img.gif", "style.css")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$assets = [
|
||||
DocumentFactory::makeFromPath('style.css', 'style.css'),
|
||||
DocumentFactory::makeFromPath('img.png', 'img.png'),
|
||||
DocumentFactory::makeFromPath('font.woff', 'font.woff'),
|
||||
];
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setAssets($assets);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Paper size, margins, orientation
|
||||
|
||||
You may also customize the resulting PDF format.
|
||||
|
||||
By default, it will be rendered with `A4` size, `1 inch` margins and `portrait` orientation.
|
||||
|
||||
> Paper size and margins have to be provided in `inches`. Same for margins.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form paperWidth=8.27 \
|
||||
--form paperHeight=11.27 \
|
||||
--form marginTop=0 \
|
||||
--form marginBottom=0 \
|
||||
--form marginLeft=0 \
|
||||
--form marginRight=0 \
|
||||
--form landscape=true \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.PaperSize(gotenberg.A4)
|
||||
req.Margins(gotenberg.NoMargins)
|
||||
req.Landscape(true)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setPaperSize(Request::A4);
|
||||
$request->setMargins(Request::NO_MARGINS);
|
||||
$request->setLandscape(true);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Wait delay
|
||||
|
||||
In some cases, you may want to wait a certain amount of time to make sure the
|
||||
page you're trying to generate is fully rendered.
|
||||
|
||||
> The wait delay is a duration in **seconds** (e.g `2.5` for 2.5 seconds).
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form waitDelay=5.5 \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.WaitDelay(5.5)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWaitDelay(5.5);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
56
build/docs/content/05-url.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: URL
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/convert/url` for remote URL conversions.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
This endpoint does not accept an `index.html` file nor assets files but a form field
|
||||
named `remoteURL` instead. Otherwise, URL conversions work the same as HTML conversions.
|
||||
|
||||
> **Attention:** when converting a website to PDF, you should remove all margins.
|
||||
> If not, some of the content of the page might be hidden.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/url \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form remoteURL=https://google.com \
|
||||
--form marginTop=0 \
|
||||
--form marginBottom=0 \
|
||||
--form marginLeft=0 \
|
||||
--form marginRight=0 \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req := gotenberg.NewURLRequest("https://google.com")
|
||||
req.Margins(gotenberg.NoMargins)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\URLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$request = new URLRequest('https://google.com');
|
||||
$request->setMargins(Request::NO_MARGINS);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
@@ -11,7 +11,7 @@ It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
Markdown conversions work the same as HTML conversions.
|
||||
|
||||
Only difference is that you have access to the Go template function `toHTML`
|
||||
in the file `index.html` which will convert a given markdown file to HTML.
|
||||
in the file `index.html`. This function will convert a given markdown file to HTML.
|
||||
|
||||
For instance:
|
||||
|
||||
@@ -28,30 +28,25 @@ For instance:
|
||||
</html>
|
||||
```
|
||||
|
||||
### Guzzle
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/markdown \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html
|
||||
--form files=@file.md
|
||||
> result.pdf
|
||||
--form files=@index.html \
|
||||
--form files=@file.md \
|
||||
-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)
|
||||
}
|
||||
@@ -59,4 +54,17 @@ func main() {
|
||||
|
||||
### PHP
|
||||
|
||||
TODO
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\MarkdownRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$markdowns = [
|
||||
DocumentFactory::makeFromPath('file.md', 'file.md'),
|
||||
];
|
||||
$request = new MarkdownRequest($index, $markdowns);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
119
build/docs/content/07-office.md
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Office
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/convert/office` for Office document conversions.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
You may send one or more Office documents. Following file extensions are accepted:
|
||||
|
||||
* `.txt`
|
||||
* `.rtf`
|
||||
* `.fodt`
|
||||
* `.doc`
|
||||
* `.docx`
|
||||
* `.odt`
|
||||
* `.xls`
|
||||
* `.xlsx`
|
||||
* `.ods`
|
||||
* `.ppt`
|
||||
* `.pptx`
|
||||
* `.odp`
|
||||
|
||||
All files will be merged into a single resulting PDF.
|
||||
|
||||
> **Attention:** currently, `unoconv` cannot perform concurrent conversions.
|
||||
> That's why for Office conversions, the API does only one conversion at a time.
|
||||
> See the [scalability section](#scalability) to find how to mitigate this issue.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/office \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@document.docx \
|
||||
--form files=@document2.docx \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewOfficeRequest("document.docx", "document2.docx")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\OfficeRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$files = [
|
||||
DocumentFactory::makeFromPath('document.docx', 'document.docx'),
|
||||
DocumentFactory::makeFromPath('document2.docx', 'document2.docx'),
|
||||
];
|
||||
$request = new OfficeRequest($files);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
|
||||
## Orientation
|
||||
|
||||
You may also customize the resulting PDF format.
|
||||
|
||||
By default, it will be rendered with `portrait` orientation.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/office \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@document.docx \
|
||||
--form landscape=true \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewOfficeRequest("document.docx")
|
||||
req.Landscape(true)
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\OfficeRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$files = [
|
||||
DocumentFactory::makeFromPath('document.docx', 'document.docx'),
|
||||
];
|
||||
$request = new OfficeRequest($files);
|
||||
$request->setLandscape(true);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
55
build/docs/content/08-merge.md
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: Merge
|
||||
---
|
||||
|
||||
Gotenberg provides the endpoint `/convert/merge` for merging PDFs.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type.
|
||||
|
||||
## Basic
|
||||
|
||||
Nothing fancy here: you may send one or more PDF files and the API
|
||||
will merge them and return the resulting PDF file.
|
||||
|
||||
> **Attention:** Gotenberg merges the PDF files alphabetically.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/merge \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@file.pdf \
|
||||
--form files=@file2.pdf \
|
||||
-o result.pdf
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewMergeRequest("file.pdf", "file2.pdf")
|
||||
dest := "result.pdf"
|
||||
c.Store(req, dest)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\MergeRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$files = [
|
||||
DocumentFactory::makeFromPath('file.pdf', 'file.pdf'),
|
||||
DocumentFactory::makeFromPath('file2.pdf', 'file2.pdf'),
|
||||
];
|
||||
$request = new MergeRequest($files);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
52
build/docs/content/09-timeout.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Timeout
|
||||
---
|
||||
|
||||
All endpoints accept a form field named `waitTimeout`.
|
||||
|
||||
The API will wait the given **seconds** before it considers the conversion to be unsucessful.
|
||||
|
||||
It takes a float as value (e.g `2.5` for 2.5 seconds).
|
||||
|
||||
> You may also define this value globally: see the [environment variables](#environment_variables.default_wait_timeout) section.
|
||||
|
||||
## Examples
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form waitTimeout=2.5
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.WaitTimeout(2.5)
|
||||
resp, _ := c.Post(req)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWaitTimeout(2.5);
|
||||
$dest = "result.pdf";
|
||||
$client->store($request, $dest);
|
||||
```
|
||||
49
build/docs/content/10-webhook.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Webhook
|
||||
---
|
||||
|
||||
All endpoints accept a form field named `webhookURL`.
|
||||
|
||||
If provided, the API will send the resulting PDF file in a `POST` request with the `application/pdf` Content-Type
|
||||
to given URL.
|
||||
|
||||
By doing so, your requests to the API will be over before the conversions are actually done!
|
||||
|
||||
## Examples
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form webhookURL='http://myapp.com/webhook/'
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.WebhookURL("http://myapp.com/webhook/")
|
||||
resp, _ := c.Post(req)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setWebhookURL('http://myapp.com/webhook/');
|
||||
$resp = $client->post($request);
|
||||
```
|
||||
50
build/docs/content/11-result-filename.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Result filename
|
||||
---
|
||||
|
||||
All endpoints accept a form field named `resultFilename`.
|
||||
|
||||
If provided, the API will return the resulting PDF file with the given filename.
|
||||
Otherwise a random filename is used.
|
||||
|
||||
> **Attention:** this feature does not work if the form field `webhookURL` is given.
|
||||
|
||||
## Examples
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
$ curl --request POST \
|
||||
--url http://localhost:3000/convert/html \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@index.html \
|
||||
--form resultFilename='foo.pdf'
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```golang
|
||||
import "github.com/thecodingmachine/gotenberg-go-client/v5"
|
||||
|
||||
func main() {
|
||||
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
|
||||
req, _ := gotenberg.NewHTMLRequest("index.html")
|
||||
req.ResultFilename("foo.pdf")
|
||||
resp, _ := c.Post(req)
|
||||
}
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
```php
|
||||
use TheCodingMachine\Gotenberg\Client;
|
||||
use TheCodingMachine\Gotenberg\DocumentFactory;
|
||||
use TheCodingMachine\Gotenberg\HTMLRequest;
|
||||
use TheCodingMachine\Gotenberg\Request;
|
||||
|
||||
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
|
||||
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
|
||||
$request = new HTMLRequest($index);
|
||||
$request->setResultFilename('foo.pdf');
|
||||
$resp = $client->post($request);
|
||||
```
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
# your others services
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
922
docs/index.html
BIN
docs/theme/gotenberg/img/apple-touch-icon-114x114.png
vendored
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-120x120.png
vendored
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-144x144.png
vendored
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-152x152.png
vendored
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-57x57.png
vendored
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-60x60.png
vendored
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-72x72.png
vendored
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
docs/theme/gotenberg/img/apple-touch-icon-76x76.png
vendored
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/theme/gotenberg/img/favicon-128.png
vendored
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/theme/gotenberg/img/favicon-16x16.png
vendored
|
Before Width: | Height: | Size: 923 B After Width: | Height: | Size: 917 B |
BIN
docs/theme/gotenberg/img/favicon-196x196.png
vendored
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/theme/gotenberg/img/favicon-32x32.png
vendored
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
BIN
docs/theme/gotenberg/img/favicon-96x96.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/theme/gotenberg/img/favicon.ico
vendored
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 34 KiB |
BIN
docs/theme/gotenberg/img/mstile-144x144.png
vendored
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/theme/gotenberg/img/mstile-150x150.png
vendored
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
docs/theme/gotenberg/img/mstile-310x150.png
vendored
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
docs/theme/gotenberg/img/mstile-310x310.png
vendored
Normal file
|
After Width: | Height: | Size: 372 KiB |
BIN
docs/theme/gotenberg/img/mstile-70x70.png
vendored
Normal file
|
After Width: | Height: | Size: 24 KiB |
24
docs/theme/gotenberg/views/index.html
vendored
@@ -1,12 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<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,21 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestHTML(t *testing.T) {
|
||||
body, contentType := test.HTMLMultipartForm(t)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
e := echo.New()
|
||||
c := e.NewContext(req, rec)
|
||||
assert.NoError(t, convertHTML(c))
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
)
|
||||
|
||||
func convertMarkdown(c echo.Context) error {
|
||||
r, err := newResource(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.removeAll()
|
||||
ctx, cancel := newContext(r)
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
indexPath, err := r.filePath("index.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p := &printer.Markdown{Context: ctx, TemplatePath: indexPath}
|
||||
headerPath, _ := r.filePath("header.html")
|
||||
if err := p.WithHeaderFile(headerPath); err != nil {
|
||||
return err
|
||||
}
|
||||
footerPath, _ := r.filePath("footer.html")
|
||||
if err := p.WithFooterFile(footerPath); err != nil {
|
||||
return err
|
||||
}
|
||||
paperSize, err := r.paperSize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.PaperWidth = paperSize[0]
|
||||
p.PaperHeight = paperSize[1]
|
||||
paperMargins, err := r.paperMargins()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.MarginTop = paperMargins[0]
|
||||
p.MarginBottom = paperMargins[1]
|
||||
p.MarginLeft = paperMargins[2]
|
||||
p.MarginRight = paperMargins[3]
|
||||
landscape, err := r.landscape()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Landscape = landscape
|
||||
return print(c, p, r)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestMarkdown(t *testing.T) {
|
||||
body, contentType := test.MarkdownMultipartForm(t)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
e := echo.New()
|
||||
c := e.NewContext(req, rec)
|
||||
assert.NoError(t, convertMarkdown(c))
|
||||
}
|
||||
@@ -1,57 +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)
|
||||
dest := 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.
|
||||
return printer.Merge(fpaths, dest)
|
||||
}
|
||||
// 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, dest); err != nil {
|
||||
c.Logger().Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
f, err := os.Open(dest)
|
||||
if err != nil {
|
||||
c.Logger().Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
|
||||
if err != nil {
|
||||
c.Logger().Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
body, contentType := test.PDFMultipartForm(t)
|
||||
req := httptest.NewRequest(http.MethodPost, "/merge", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
e := echo.New()
|
||||
c := e.NewContext(req, rec)
|
||||
assert.NoError(t, merge(c))
|
||||
}
|
||||
61
internal/app/api/middleware.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func handleContext(opts *Options) echo.MiddlewareFunc {
|
||||
// middleware for extending default context with our
|
||||
// custom constext.
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := &resourceContext{c, opts, nil}
|
||||
r, err := newResource(ctx)
|
||||
if err != nil {
|
||||
if resourceErr := r.close(); resourceErr != nil {
|
||||
c.Logger().Error(resourceErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
ctx.resource = r
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleError() echo.MiddlewareFunc {
|
||||
// middleware for handling errors and removing resources
|
||||
// once the request has been handled.
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
err := next(c)
|
||||
ctx := c.(*resourceContext)
|
||||
// if a webhookURL has been given,
|
||||
// do not remove the resources here because
|
||||
// we don't know if the result file has been
|
||||
// generated or sent.
|
||||
if !ctx.resource.has(webhookURL) {
|
||||
if resourceErr := ctx.resource.close(); resourceErr != nil {
|
||||
c.Logger().Error(resourceErr)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if _, ok := err.(*echo.HTTPError); ok {
|
||||
return err
|
||||
}
|
||||
if _, ok := err.(*errBadRequest); ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) {
|
||||
return echo.NewHTTPError(http.StatusRequestTimeout)
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
|
||||
)
|
||||
|
||||
var officeExts = []string{
|
||||
".doc",
|
||||
".docx",
|
||||
".odt",
|
||||
".xls",
|
||||
".xlsx",
|
||||
".ods",
|
||||
".ppt",
|
||||
".pptx",
|
||||
".odp",
|
||||
}
|
||||
|
||||
func convertOffice(c echo.Context) error {
|
||||
r, err := newResource(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.removeAll()
|
||||
ctx, cancel := newContext(r)
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
fpaths, err := r.filePaths(officeExts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(fpaths) == 0 {
|
||||
return errors.New("no suitable office documents to convert")
|
||||
}
|
||||
p := &printer.Office{Context: ctx, FilePaths: fpaths}
|
||||
return print(c, p, r)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestOffice(t *testing.T) {
|
||||
body, contentType := test.OfficeMultipartForm(t)
|
||||
req := httptest.NewRequest(http.MethodPost, "/convert/office", body)
|
||||
req.Header.Set(echo.HeaderContentType, contentType)
|
||||
rec := httptest.NewRecorder()
|
||||
e := echo.New()
|
||||
c := e.NewContext(req, rec)
|
||||
assert.NoError(t, convertOffice(c))
|
||||
}
|
||||
@@ -3,93 +3,262 @@ package api
|
||||
import (
|
||||
"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,27 +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.
|
||||
devt := devtool.New("http://127.0.0.1:9222")
|
||||
_, err := devt.Create(context.TODO())
|
||||
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,188 +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 {
|
||||
// use the DevTools HTTP/JSON API to manage targets (e.g. pages, webworkers).
|
||||
devt := devtool.New("http://127.0.0.1:9222")
|
||||
pt, err := devt.Create(html.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating DevTools target: %v", err)
|
||||
}
|
||||
// open a new RPC connection to the Chrome Debugging Protocol target.
|
||||
conn, err := rpcc.DialContext(html.Context, pt.WebSocketDebuggerURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating RPC connection: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
// create new browser context.
|
||||
baseBrowser := cdp.NewClient(conn)
|
||||
newContextTarget, err := baseBrowser.Target.CreateBrowserContext(html.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating new browser context: %v", err)
|
||||
}
|
||||
// create a new blank target with the new browser context.
|
||||
newTargetArgs := target.NewCreateTargetArgs("about:blank").
|
||||
SetBrowserContextID(newContextTarget.BrowserContextID)
|
||||
newTarget, err := baseBrowser.Target.CreateTarget(html.Context, newTargetArgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating new blank target: %v", err)
|
||||
}
|
||||
// connect the client to the new target.
|
||||
newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID)
|
||||
newContextConn, err := rpcc.DialContext(html.Context, newTargetWsURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connecting client to blank target: %v", err)
|
||||
}
|
||||
defer newContextConn.Close()
|
||||
// close the target when done.
|
||||
closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID)
|
||||
defer baseBrowser.Target.CloseTarget(html.Context, closeTargetArgs)
|
||||
c := cdp.NewClient(newContextConn)
|
||||
// enable the runtime.
|
||||
if err := c.Runtime.Enable(html.Context); err != nil {
|
||||
return fmt.Errorf("enabling runtime: %v", err)
|
||||
}
|
||||
// enable the network.
|
||||
if err := c.Network.Enable(html.Context, network.NewEnableArgs()); err != nil {
|
||||
return fmt.Errorf("enabling network: %v", err)
|
||||
}
|
||||
// enable events on the page domain.
|
||||
if err := c.Page.Enable(html.Context); err != nil {
|
||||
return fmt.Errorf("enabling events on page domain: %v", err)
|
||||
}
|
||||
// create a client to listen for the load event to be fired.
|
||||
loadEventFiredClient, err := c.Page.LoadEventFired(html.Context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating client listening for load event: %v", err)
|
||||
}
|
||||
defer loadEventFiredClient.Close()
|
||||
// tell the page to navigate to the URL.
|
||||
navArgs := page.NewNavigateArgs(html.URL)
|
||||
_, err = c.Page.Navigate(html.Context, navArgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: navigating to URL: %v", html.URL, err)
|
||||
}
|
||||
// wait for the page to finish loading.
|
||||
_, err = loadEventFiredClient.Recv()
|
||||
if err != nil {
|
||||
return fmt.Errorf("waiting for page loading: %v", err)
|
||||
}
|
||||
// inject a script to make sure web fonts are loaded.
|
||||
script := `new Promise((resolve, reject) => {
|
||||
document.fonts.ready.then(function () {
|
||||
resolve('fonts loaded');
|
||||
});
|
||||
setTimeout(resolve.bind(resolve, 'timeout'), %.0f);
|
||||
});`
|
||||
scriptArg := runtime.NewEvaluateArgs(script).SetAwaitPromise(true)
|
||||
returnObj, _ := c.Runtime.Evaluate(html.Context, scriptArg)
|
||||
loadFontsResult := string(returnObj.Result.Value)
|
||||
if strings.Contains(loadFontsResult, "timeout") {
|
||||
return errors.New("timed out loading fonts")
|
||||
}
|
||||
// if no header or footer, use the default template
|
||||
// for avoiding displaying default Chrome templates.
|
||||
if html.HeaderHTML == "" {
|
||||
html.HeaderHTML = defaultHeaderFooterHTML
|
||||
}
|
||||
if html.FooterHTML == "" {
|
||||
html.FooterHTML = defaultHeaderFooterHTML
|
||||
}
|
||||
print, err := c.Page.PrintToPDF(
|
||||
html.Context,
|
||||
page.NewPrintToPDFArgs().
|
||||
SetPaperWidth(html.PaperWidth).
|
||||
SetPaperHeight(html.PaperHeight).
|
||||
SetMarginTop(html.MarginTop).
|
||||
SetMarginBottom(html.MarginBottom).
|
||||
SetMarginLeft(html.MarginLeft).
|
||||
SetMarginRight(html.MarginRight).
|
||||
SetLandscape(html.Landscape).
|
||||
SetDisplayHeaderFooter(true).
|
||||
SetHeaderTemplate(html.HeaderHTML).
|
||||
SetFooterTemplate(html.FooterHTML).
|
||||
SetPrintBackground(true),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("printing page to PDF: %v", err)
|
||||
}
|
||||
return writeBytesToFile(destination, print.Data)
|
||||
}
|
||||
|
||||
// WithLocalURL sets a local URL from a file path.
|
||||
func (html *HTML) WithLocalURL(fpath string) {
|
||||
html.URL = fmt.Sprintf("file://%s", fpath)
|
||||
}
|
||||
|
||||
// WithHeaderFile sets header content from a file.
|
||||
func (html *HTML) WithHeaderFile(fpath string) error {
|
||||
if fpath == "" {
|
||||
return nil
|
||||
}
|
||||
contentHTML, err := fileContentToString(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
html.HeaderHTML = contentHTML
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithFooterFile sets footer content from a file.
|
||||
func (html *HTML) WithFooterFile(fpath string) error {
|
||||
if fpath == "" {
|
||||
return nil
|
||||
}
|
||||
contentHTML, err := fileContentToString(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
html.FooterHTML = contentHTML
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileContentToString(fpath string) (string, error) {
|
||||
b, err := ioutil.ReadFile(fpath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: reading file: %v", fpath, err)
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = Printer(new(HTML))
|
||||
)
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package printer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestHTML(t *testing.T) {
|
||||
dirPath := test.HTMLTestDirPath(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
html := &HTML{
|
||||
Context: ctx,
|
||||
PaperWidth: 8.27,
|
||||
PaperHeight: 11.7,
|
||||
MarginTop: 1,
|
||||
MarginBottom: 1,
|
||||
MarginLeft: 1,
|
||||
MarginRight: 1,
|
||||
}
|
||||
html.WithLocalURL(fmt.Sprintf("%s/%s", dirPath, "index.html"))
|
||||
err := html.WithHeaderFile(fmt.Sprintf("%s/%s", dirPath, "header.html"))
|
||||
require.Nil(t, err)
|
||||
err = html.WithFooterFile(fmt.Sprintf("%s/%s", dirPath, "footer.html"))
|
||||
require.Nil(t, err)
|
||||
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")
|
||||
err = html.Print(dst)
|
||||
require.Nil(t, err)
|
||||
require.FileExists(t, dst)
|
||||
err = os.RemoveAll(dirPath)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package printer
|
||||
|
||||
import (
|
||||
"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: writting 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: writting 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))
|
||||
)
|
||||