mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1f6100382 | ||
|
|
223c0de35a | ||
|
|
e3366d0b62 | ||
|
|
5100693789 | ||
|
|
faec55dd01 | ||
|
|
2871fb1ffe | ||
|
|
810d35ffbe | ||
|
|
4997b73bac | ||
|
|
ef9801b1fd | ||
|
|
1f2317dc74 |
@@ -1,136 +0,0 @@
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/8983173/38133342-11df3bd8-340f-11e8-9fe4-50baecdceeca.png" alt="Gotenberg's logo" width="250" height="250" />
|
||||
</p>
|
||||
<h3 align="center">Gotenberg</h3>
|
||||
<p align="center">A stateless API for converting Markdown files, HTML files and Office documents to PDF</p>
|
||||
<p align="center">
|
||||
<a href="https://microbadger.com/images/thecodingmachine/gotenberg:{{ .Orbit.Latest }}">
|
||||
<img src="https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:{{ .Orbit.Latest }}.svg" alt="MicroBadger layers">
|
||||
</a>
|
||||
<a href="https://travis-ci.org/thecodingmachine/gotenberg">
|
||||
<img src="https://travis-ci.org/thecodingmachine/gotenberg.svg?branch={{ .Orbit.Branch }}" alt="Travis CI">
|
||||
</a>
|
||||
<a href="https://godoc.org/github.com/thecodingmachine/gotenberg">
|
||||
<img src="https://godoc.org/github.com/thecodingmachine/gotenberg?status.svg" alt="GoDoc">
|
||||
</a>
|
||||
<a href="https://goreportcard.com/report/thecodingmachine/gotenberg">
|
||||
<img src="https://goreportcard.com/badge/github.com/thecodingmachine/gotenberg" alt="Go Report Card">
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/thecodingmachine/gotenberg/branch/{{ .Orbit.Branch }}">
|
||||
<img src="https://codecov.io/gh/thecodingmachine/gotenberg/branch/{{ .Orbit.Branch }}/graph/badge.svg" alt="Codecov">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
At TheCodingMachine, we build a lot of web applications (intranets, extranets and so on) which require to generate PDF from
|
||||
various sources. Each time, we ended up using some well known libraries like **wkhtmltopdf** or **unoconv** and kind of lost time by
|
||||
reimplementing a solution from a project to another project. Meh.
|
||||
|
||||
# Menu
|
||||
|
||||
* [Usage](#usage)
|
||||
* [Security](#security)
|
||||
* [Scalability](#scalability)
|
||||
* [Custom implementation](#custom-implementation)
|
||||
* [Clients](#clients)
|
||||
|
||||
## Usage
|
||||
|
||||
Let's say you're starting the API using this simple command:
|
||||
|
||||
```sh
|
||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:{{ .Orbit.Latest }}
|
||||
```
|
||||
|
||||
The API is now available on your host under `http://127.0.0.1:3000`.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type. Your form data should provide one or more files to convert.
|
||||
It currently accepts the following:
|
||||
|
||||
* Markdown files
|
||||
* HTML files
|
||||
* Office documents (.docx, .doc, .odt, .pptx, .ppt, .odp and so on)
|
||||
* PDF files (if more than one file to convert)
|
||||
|
||||
**Heads up:** the API relies on the file extension to determine which library to use for conversion.
|
||||
|
||||
There are two use cases:
|
||||
|
||||
* If you send one file, it will convert it and return the resulting PDF
|
||||
* If many files, it will convert them to PDF, merge the resulting PDFs into a single PDF and return it
|
||||
|
||||
### Examples:
|
||||
|
||||
* One file
|
||||
|
||||
```sh
|
||||
$ curl --request POST \
|
||||
--url http://127.0.0.1:3000 \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@file.docx \
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
* Many files
|
||||
|
||||
```sh
|
||||
$ curl --request POST \
|
||||
--url http://127.0.0.1:3000 \
|
||||
--header 'Content-Type: multipart/form-data' \
|
||||
--form files=@file.md \
|
||||
--form files=@file.html \
|
||||
--form files=@file.pdf \
|
||||
--form files=@file.docx \
|
||||
> result.pdf
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
The API does not provide any authentication mechanisms. Make sure to not put it on a public facing port and your client(s) should always
|
||||
controls what is sent to the API.
|
||||
|
||||
## Scalability
|
||||
|
||||
Some libraries like **unoconv** cannot perform concurrent conversions. That's why the API does only one conversion at a time.
|
||||
If your API is under heavy load, a request will take time to be processed.
|
||||
|
||||
Fortunately, you may pass through this limitation by scaling the API.
|
||||
|
||||
In the following example, I'll demonstrate how to do some vertical scaling (= on the same machine) with Docker Compose, but of course horizontal scaling works too!
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
|
||||
# your others services
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg:1.0.0
|
||||
```
|
||||
|
||||
You may now launch your services using:
|
||||
|
||||
```bash
|
||||
docker-compose up --scale gotenberg=your_number_of_instances
|
||||
```
|
||||
|
||||
When requesting the Gotenberg service with your client(s), Docker will automatically redirect a request to a Gotenberg container
|
||||
according to the round-robin strategy.
|
||||
|
||||
## Custom implementation
|
||||
|
||||
The API relies on a simple YAML configuration file called `gotenberg.yml`. It allows you to tweak some values and even provides you
|
||||
a way to change the commands called for each kind of conversion. The configuration file should be located under `/gotenberg` in your container.
|
||||
|
||||
The default configuration is located here: [.ci/gotenberg.yml](.ci/gotenberg.yml)
|
||||
|
||||
## Clients
|
||||
|
||||
* https://github.com/thecodingmachine/gotenberg-php-client (PHP client)
|
||||
* Add your own client by submitting a [pull request](../../pulls)!
|
||||
|
||||
---
|
||||
|
||||
Would you like to update this documentation ? Feel free to open an [issue](../../issues).
|
||||
@@ -8,26 +8,53 @@ logs:
|
||||
|
||||
# Accepted values: text, json.
|
||||
# When a TTY is not attached, the output will be in the defined format.
|
||||
format: "text"
|
||||
formatter: "text"
|
||||
|
||||
# You don't like a library which is used for a conversion? You may provide here your own implementation.
|
||||
# You don't like a library which is used for a conversion? You want to handle a new file type?
|
||||
# You may provide here your own implementation!
|
||||
commands:
|
||||
|
||||
markdown:
|
||||
# Duration in seconds after which the command will be killed if it has not finished.
|
||||
timeout: 30
|
||||
# The command template: you have access to FilePath and ResultFilePath variables.
|
||||
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
|
||||
html:
|
||||
timeout: 30
|
||||
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
|
||||
office:
|
||||
timeout: 30
|
||||
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
# Some libraries like unoconv cannot perform concurrent conversions. That's why the API does only one conversion at a time.
|
||||
# If your current implementation uses libraries which are able to perform concurrent conversions, you may
|
||||
# change this value to false.
|
||||
lock: true
|
||||
|
||||
# Unlike others commands' templates, you have access to FilesPaths instead of FilePath: it gathers all PDF files which should be merged.
|
||||
merge:
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
# Unlike others commands' templates, you have access to FilesPaths instead of FilePath: it gathers all PDF files which should be merged.
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
|
||||
# You may add more commands (or less, or even none).
|
||||
conversions:
|
||||
|
||||
# The command template: you have access to FilePath and ResultFilePath variables.
|
||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
# The binary which will call the command.
|
||||
interpreter: "/bin/sh -c"
|
||||
# Duration in seconds after which the command will be killed if it has not finished.
|
||||
timeout: 30
|
||||
# Files with the following extensions will be converted by the current command.
|
||||
extensions:
|
||||
- ".md"
|
||||
|
||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".html"
|
||||
- ".htm"
|
||||
|
||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".doc"
|
||||
- ".docx"
|
||||
- ".odt"
|
||||
- ".xls"
|
||||
- ".xlsx"
|
||||
- ".ods"
|
||||
- ".ppt"
|
||||
- ".pptx"
|
||||
- ".odp"
|
||||
28
.goreleaser.yml
Normal file
28
.goreleaser.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
project_name: Gotenberg
|
||||
builds:
|
||||
- main: ./main.go
|
||||
binary: gotenberg
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
archive:
|
||||
name_template: '{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
|
||||
replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
8
Gopkg.lock
generated
8
Gopkg.lock
generated
@@ -1,6 +1,12 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
packages = ["."]
|
||||
revision = "02af3965c54e8cacf948b97fef38925c4120652c"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/context"
|
||||
packages = ["."]
|
||||
@@ -55,6 +61,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "9d7a607d9b015e46c535126cbc62648ce78459507cd523d6ff0e048051cf3228"
|
||||
inputs-digest = "97e0dbb20631792bc25ad19e6a85a90a9d24bb03f773cd36dee11eebae152091"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
@@ -48,3 +48,7 @@
|
||||
[[constraint]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
version = "1.0.5"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
|
||||
88
README.md
88
README.md
@@ -4,11 +4,11 @@
|
||||
<h3 align="center">Gotenberg</h3>
|
||||
<p align="center">A stateless API for converting Markdown files, HTML files and Office documents to PDF</p>
|
||||
<p align="center">
|
||||
<a href="https://microbadger.com/images/thecodingmachine/gotenberg:1.0.0">
|
||||
<img src="https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:1.0.0.svg" alt="MicroBadger layers">
|
||||
<a href="https://microbadger.com/images/thecodingmachine/gotenberg:2.0.0">
|
||||
<img src="https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:2.0.0.svg" alt="MicroBadger layers">
|
||||
</a>
|
||||
<a href="https://travis-ci.org/thecodingmachine/gotenberg">
|
||||
<img src="https://travis-ci.org/thecodingmachine/gotenberg.svg?branch=1.0.0" alt="Travis CI">
|
||||
<img src="https://travis-ci.org/thecodingmachine/gotenberg.svg?branch=master" alt="Travis CI">
|
||||
</a>
|
||||
<a href="https://godoc.org/github.com/thecodingmachine/gotenberg">
|
||||
<img src="https://godoc.org/github.com/thecodingmachine/gotenberg?status.svg" alt="GoDoc">
|
||||
@@ -16,8 +16,8 @@
|
||||
<a href="https://goreportcard.com/report/thecodingmachine/gotenberg">
|
||||
<img src="https://goreportcard.com/badge/github.com/thecodingmachine/gotenberg" alt="Go Report Card">
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/thecodingmachine/gotenberg/branch/1.0.0">
|
||||
<img src="https://codecov.io/gh/thecodingmachine/gotenberg/branch/1.0.0/graph/badge.svg" alt="Codecov">
|
||||
<a href="https://codecov.io/gh/thecodingmachine/gotenberg/branch/master">
|
||||
<img src="https://codecov.io/gh/thecodingmachine/gotenberg/branch/master/graph/badge.svg" alt="Codecov">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -40,18 +40,18 @@ reimplementing a solution from a project to another project. Meh.
|
||||
Let's say you're starting the API using this simple command:
|
||||
|
||||
```sh
|
||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:1.0.0
|
||||
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:2.0.0
|
||||
```
|
||||
|
||||
The API is now available on your host under `http://127.0.0.1:3000`.
|
||||
|
||||
It accepts `POST` requests with a `multipart/form-data` Content-Type. Your form data should provide one or more files to convert.
|
||||
It currently accepts the following:
|
||||
The default image accepts the following:
|
||||
|
||||
* Markdown files
|
||||
* HTML files
|
||||
* Office documents (.docx, .doc, .odt, .pptx, .ppt, .odp and so on)
|
||||
* PDF files (if more than one file to convert)
|
||||
* PDF files
|
||||
|
||||
**Heads up:** the API relies on the file extension to determine which library to use for conversion.
|
||||
|
||||
@@ -107,7 +107,7 @@ services:
|
||||
# your others services
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg:1.0.0
|
||||
image: thecodingmachine/gotenberg:2.0.0
|
||||
```
|
||||
|
||||
You may now launch your services using:
|
||||
@@ -122,9 +122,75 @@ according to the round-robin strategy.
|
||||
## Custom implementation
|
||||
|
||||
The API relies on a simple YAML configuration file called `gotenberg.yml`. It allows you to tweak some values and even provides you
|
||||
a way to change the commands called for each kind of conversion. The configuration file should be located under `/gotenberg` in your container.
|
||||
a way to change the commands called for each kind of conversion.
|
||||
|
||||
The default configuration is located here: [.ci/gotenberg.yml](.ci/gotenberg.yml)
|
||||
Below the default configuration file:
|
||||
|
||||
```yaml
|
||||
# The port the application will listen to.
|
||||
port: 3000
|
||||
|
||||
logs:
|
||||
# Accepted values, in order of severity: DEBUG, INFO, WARN, ERROR, FATAL, PANIC.
|
||||
# Messages at and above the selected level will be logged.
|
||||
level: "INFO"
|
||||
|
||||
# Accepted values: text, json.
|
||||
# When a TTY is not attached, the output will be in the defined format.
|
||||
formatter: "text"
|
||||
|
||||
# You don't like a library which is used for a conversion? You want to handle a new file type?
|
||||
# You may provide here your own implementation!
|
||||
commands:
|
||||
|
||||
# Some libraries like unoconv cannot perform concurrent conversions. That's why the API does only one conversion at a time.
|
||||
# If your current implementation uses libraries which are able to perform concurrent conversions, you may
|
||||
# change this value to false.
|
||||
lock: true
|
||||
|
||||
# Unlike others commands' templates, you have access to FilesPaths instead of FilePath: it gathers all PDF files which should be merged.
|
||||
merge:
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
|
||||
# You may add more commands (or less, or even none).
|
||||
conversions:
|
||||
|
||||
# The command template: you have access to FilePath and ResultFilePath variables.
|
||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
# The binary which will call the command.
|
||||
interpreter: "/bin/sh -c"
|
||||
# Duration in seconds after which the command will be killed if it has not finished.
|
||||
timeout: 30
|
||||
# Files with the following extensions will be converted by the current command.
|
||||
extensions:
|
||||
- ".md"
|
||||
|
||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".html"
|
||||
- ".htm"
|
||||
|
||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".doc"
|
||||
- ".docx"
|
||||
- ".odt"
|
||||
- ".xls"
|
||||
- ".xlsx"
|
||||
- ".ods"
|
||||
- ".ppt"
|
||||
- ".pptx"
|
||||
- ".odp"
|
||||
```
|
||||
|
||||
We provide binaries for a wide range of OS and architecture in the [releases page](../../releases),
|
||||
so feel free to create your own Docker image for your implementation of the Gotenberg API :metal:
|
||||
|
||||
## Clients
|
||||
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "DEBUG"
|
||||
format: {
|
||||
formatter: {
|
||||
value: [
|
||||
...
|
||||
}
|
||||
commands:
|
||||
markdown:
|
||||
timeout: 30
|
||||
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
html:
|
||||
timeout: 30
|
||||
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
office:
|
||||
timeout: 30
|
||||
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
lock: true
|
||||
merge:
|
||||
timeout: 30
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
conversions:
|
||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".md"
|
||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".html"
|
||||
- ".htm"
|
||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".doc"
|
||||
- ".docx"
|
||||
- ".odt"
|
||||
- ".xls"
|
||||
- ".xlsx"
|
||||
- ".ods"
|
||||
- ".ppt"
|
||||
- ".pptx"
|
||||
- ".odp"
|
||||
|
||||
|
||||
40
_tests/configurations/duplicate-command-gotenberg.yml
Normal file
40
_tests/configurations/duplicate-command-gotenberg.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "DEBUG"
|
||||
formatter: "text"
|
||||
commands:
|
||||
lock: true
|
||||
merge:
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
conversions:
|
||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".md"
|
||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".md"
|
||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".html"
|
||||
- ".htm"
|
||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".doc"
|
||||
- ".docx"
|
||||
- ".odt"
|
||||
- ".xls"
|
||||
- ".xlsx"
|
||||
- ".ods"
|
||||
- ".ppt"
|
||||
- ".pptx"
|
||||
- ".odp"
|
||||
@@ -1,17 +1,35 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "DEBUG"
|
||||
format: "text"
|
||||
formatter: "text"
|
||||
commands:
|
||||
markdown:
|
||||
timeout: 30
|
||||
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
html:
|
||||
timeout: 30
|
||||
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
office:
|
||||
timeout: 30
|
||||
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
lock: true
|
||||
merge:
|
||||
timeout: 30
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
conversions:
|
||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".md"
|
||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".html"
|
||||
- ".htm"
|
||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".doc"
|
||||
- ".docx"
|
||||
- ".odt"
|
||||
- ".xls"
|
||||
- ".xlsx"
|
||||
- ".ods"
|
||||
- ".ppt"
|
||||
- ".pptx"
|
||||
- ".odp"
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "DEBUG"
|
||||
format: "text"
|
||||
formatter: "text"
|
||||
commands:
|
||||
markdown:
|
||||
timeout: 30
|
||||
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
html:
|
||||
timeout: 30
|
||||
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
office:
|
||||
timeout: 30
|
||||
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
lock: true
|
||||
merge:
|
||||
timeout: 0
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 0
|
||||
conversions:
|
||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".md"
|
||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".html"
|
||||
- ".htm"
|
||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".doc"
|
||||
- ".docx"
|
||||
- ".odt"
|
||||
- ".xls"
|
||||
- ".xlsx"
|
||||
- ".ods"
|
||||
- ".ppt"
|
||||
- ".pptx"
|
||||
- ".odp"
|
||||
|
||||
35
_tests/configurations/no-lock-gotenberg.yml
Normal file
35
_tests/configurations/no-lock-gotenberg.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "DEBUG"
|
||||
formatter: "text"
|
||||
commands:
|
||||
lock: false
|
||||
merge:
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
conversions:
|
||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".md"
|
||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".html"
|
||||
- ".htm"
|
||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".doc"
|
||||
- ".docx"
|
||||
- ".odt"
|
||||
- ".xls"
|
||||
- ".xlsx"
|
||||
- ".ods"
|
||||
- ".ppt"
|
||||
- ".pptx"
|
||||
- ".odp"
|
||||
@@ -1,17 +1,35 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "DEBUG"
|
||||
format: "text"
|
||||
formatter: "text"
|
||||
commands:
|
||||
markdown:
|
||||
timeout: 0
|
||||
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
html:
|
||||
timeout: 0
|
||||
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
office:
|
||||
timeout: 0
|
||||
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
lock: true
|
||||
merge:
|
||||
timeout: 0
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 0
|
||||
conversions:
|
||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 0
|
||||
extensions:
|
||||
- ".md"
|
||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 0
|
||||
extensions:
|
||||
- ".html"
|
||||
- ".htm"
|
||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 0
|
||||
extensions:
|
||||
- ".doc"
|
||||
- ".docx"
|
||||
- ".odt"
|
||||
- ".xls"
|
||||
- ".xlsx"
|
||||
- ".ods"
|
||||
- ".ppt"
|
||||
- ".pptx"
|
||||
- ".odp"
|
||||
|
||||
35
_tests/configurations/wrong-command-template-gotenberg.yml
Normal file
35
_tests/configurations/wrong-command-template-gotenberg.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "DEBUG"
|
||||
formatter: "text"
|
||||
commands:
|
||||
lock: true
|
||||
merge:
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
conversions:
|
||||
- template: "markdown-pdf {{ FilePath }} -o {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".md"
|
||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".html"
|
||||
- ".htm"
|
||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".doc"
|
||||
- ".docx"
|
||||
- ".odt"
|
||||
- ".xls"
|
||||
- ".xlsx"
|
||||
- ".ods"
|
||||
- ".ppt"
|
||||
- ".pptx"
|
||||
- ".odp"
|
||||
@@ -1,17 +0,0 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "DEBUG"
|
||||
format: "text"
|
||||
commands:
|
||||
markdown:
|
||||
timeout: 30
|
||||
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
html:
|
||||
timeout: 30
|
||||
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ FilePath }} {{ .ResultFilePath }}"
|
||||
office:
|
||||
timeout: 30
|
||||
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
merge:
|
||||
timeout: 30
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
@@ -1,17 +0,0 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "DEBUG"
|
||||
format: "DEBUG"
|
||||
commands:
|
||||
markdown:
|
||||
timeout: 30
|
||||
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
html:
|
||||
timeout: 30
|
||||
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
office:
|
||||
timeout: 30
|
||||
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
merge:
|
||||
timeout: 30
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
35
_tests/configurations/wrong-logging-formatter-gotenberg.yml
Normal file
35
_tests/configurations/wrong-logging-formatter-gotenberg.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "DEBUG"
|
||||
formatter: "DEBUG"
|
||||
commands:
|
||||
lock: true
|
||||
merge:
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
conversions:
|
||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".md"
|
||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".html"
|
||||
- ".htm"
|
||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".doc"
|
||||
- ".docx"
|
||||
- ".odt"
|
||||
- ".xls"
|
||||
- ".xlsx"
|
||||
- ".ods"
|
||||
- ".ppt"
|
||||
- ".pptx"
|
||||
- ".odp"
|
||||
@@ -1,17 +1,35 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "text"
|
||||
format: "text"
|
||||
formatter: "text"
|
||||
commands:
|
||||
markdown:
|
||||
timeout: 30
|
||||
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
html:
|
||||
timeout: 30
|
||||
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
office:
|
||||
timeout: 30
|
||||
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
lock: true
|
||||
merge:
|
||||
timeout: 30
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
conversions:
|
||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".md"
|
||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".html"
|
||||
- ".htm"
|
||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".doc"
|
||||
- ".docx"
|
||||
- ".odt"
|
||||
- ".xls"
|
||||
- ".xlsx"
|
||||
- ".ods"
|
||||
- ".ppt"
|
||||
- ".pptx"
|
||||
- ".odp"
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "DEBUG"
|
||||
format: "text"
|
||||
commands:
|
||||
markdown:
|
||||
timeout: 30
|
||||
template: "markdown-pdf {{ FilePath }} -o {{ .ResultFilePath }}"
|
||||
html:
|
||||
timeout: 30
|
||||
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
office:
|
||||
timeout: 30
|
||||
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
merge:
|
||||
timeout: 30
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
@@ -1,17 +1,35 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "DEBUG"
|
||||
format: "text"
|
||||
formatter: "text"
|
||||
commands:
|
||||
markdown:
|
||||
timeout: 30
|
||||
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
html:
|
||||
timeout: 30
|
||||
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
office:
|
||||
timeout: 30
|
||||
template: "unoconv --format pdf --output {{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
lock: true
|
||||
merge:
|
||||
template: "pdftk {{ range $filePath := FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} cat output {{ .ResultFilePath }}"
|
||||
conversions:
|
||||
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".md"
|
||||
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".html"
|
||||
- ".htm"
|
||||
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
interpreter: "/bin/sh -c"
|
||||
timeout: 30
|
||||
extensions:
|
||||
- ".doc"
|
||||
- ".docx"
|
||||
- ".odt"
|
||||
- ".xls"
|
||||
- ".xlsx"
|
||||
- ".ods"
|
||||
- ".ppt"
|
||||
- ".pptx"
|
||||
- ".odp"
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
port: 3000
|
||||
logs:
|
||||
level: "DEBUG"
|
||||
format: "text"
|
||||
commands:
|
||||
markdown:
|
||||
timeout: 30
|
||||
template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
|
||||
html:
|
||||
timeout: 30
|
||||
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||
office:
|
||||
timeout: 30
|
||||
template: "unoconv --format pdf --output \"{{ ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||
merge:
|
||||
timeout: 30
|
||||
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
@@ -7,139 +7,54 @@ It should be located where the user starts the application from the CLI.
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type (
|
||||
// AppConfig gathers all data required to instantiate the application.
|
||||
AppConfig struct {
|
||||
// Port is the port which the application will listen to.
|
||||
Port string
|
||||
// Logs contains the logging configuration.
|
||||
Logs struct {
|
||||
// Level is the level of messages which will be logged.
|
||||
Level logrus.Level
|
||||
// Formatter defines the logging format when a TTY is not attached.
|
||||
Formatter logrus.Formatter
|
||||
}
|
||||
// CommandsConfig is... an instance of CommandsConfig.
|
||||
CommandsConfig *CommandsConfig
|
||||
// appConfig gathers all configuration data.
|
||||
appConfig struct {
|
||||
port string
|
||||
logsLevel logrus.Level
|
||||
logsFormatter logrus.Formatter
|
||||
lock bool
|
||||
// commands associates a file extension with a Command instance.
|
||||
// Particular case: ".pdf" extension is used for the merge command.
|
||||
commands map[string]*Command
|
||||
}
|
||||
|
||||
// CommandsConfig gathers all commands' configurations as defined
|
||||
// by the user in the gotenberg.yml file.
|
||||
CommandsConfig struct {
|
||||
// Markdown is the command's configuration for converting
|
||||
// an Markdown file to PDF.
|
||||
Markdown *CommandConfig
|
||||
// HTML is the command's configuration for converting
|
||||
// an HTML file to PDF.
|
||||
HTML *CommandConfig
|
||||
// Office is the command's configuration for converting
|
||||
// an Office document to PDF.
|
||||
Office *CommandConfig
|
||||
// Merge is the command's configuration for merging
|
||||
// multiple PDF files into one PDF file.
|
||||
Merge *CommandConfig
|
||||
}
|
||||
|
||||
// CommandConfig is a command's configuration.
|
||||
CommandConfig struct {
|
||||
// Command gathers information on how to launch an external binary used for converting
|
||||
// a file to PDF.
|
||||
Command struct {
|
||||
// Template is the data-driven template of the command.
|
||||
Template *template.Template
|
||||
// The binary which will call the command.
|
||||
Interpreter []string
|
||||
// Timeout is the duration in seconds after which the command's process will be killed
|
||||
// if it does not finish before.
|
||||
Timeout int
|
||||
// Template is the data-driven template of the command.
|
||||
Template *template.Template
|
||||
}
|
||||
)
|
||||
|
||||
// NewAppConfig instantiates the application's configuration.
|
||||
// If something bad happens here, the application should not start.
|
||||
func NewAppConfig(configurationFilePath string) (*AppConfig, error) {
|
||||
fileConfig, err := loadFileConfig(configurationFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// our default instance of appConfig.
|
||||
var config = &appConfig{}
|
||||
|
||||
c := &AppConfig{}
|
||||
c.Port = fileConfig.Port
|
||||
|
||||
if err := makeLogs(c, fileConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := makeCommandsConfig(c, fileConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
// Reset reinitializes our configuration.
|
||||
func Reset() {
|
||||
config = &appConfig{}
|
||||
}
|
||||
|
||||
// fileConfig gathers all data coming from the configuration file gotenberg.yml.
|
||||
type fileConfig struct {
|
||||
Port string `yaml:"port"`
|
||||
Logs struct {
|
||||
Level string `yaml:"level"`
|
||||
Format string `yaml:"format"`
|
||||
} `yaml:"logs"`
|
||||
Commands struct {
|
||||
Markdown struct {
|
||||
Timeout int `yaml:"timeout"`
|
||||
Template string `yaml:"template"`
|
||||
} `yaml:"markdown"`
|
||||
HTML struct {
|
||||
Timeout int
|
||||
Template string
|
||||
} `yaml:"html"`
|
||||
Office struct {
|
||||
Timeout int `yaml:"timeout"`
|
||||
Template string `yaml:"template"`
|
||||
} `yaml:"office"`
|
||||
Merge struct {
|
||||
Timeout int `yaml:"timeout"`
|
||||
Template string `yaml:"template"`
|
||||
} `yaml:"merge"`
|
||||
} `yaml:"commands"`
|
||||
// WithPort sets the port which will be used by the application.
|
||||
func WithPort(port string) {
|
||||
config.port = port
|
||||
}
|
||||
|
||||
// loadFileConfig instantiates a fileConfig instance by loading
|
||||
// the configuration file gotenberg.yml.
|
||||
func loadFileConfig(configurationFilePath string) (*fileConfig, error) {
|
||||
c := &fileConfig{}
|
||||
|
||||
data, err := ioutil.ReadFile(configurationFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// makeLogs is a simple wrapper which populates all data related
|
||||
// to application's logging.
|
||||
func makeLogs(appConfig *AppConfig, fileConfig *fileConfig) error {
|
||||
lvl, err := getLoggingLevelFromFileConfig(fileConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
formatter, err := getLoggingFormatterFromFileConfig(fileConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appConfig.Logs.Level = lvl
|
||||
appConfig.Logs.Formatter = formatter
|
||||
|
||||
return nil
|
||||
// GetPort returns the current port.
|
||||
func GetPort() string {
|
||||
return config.port
|
||||
}
|
||||
|
||||
// levels associates logging levels as defined in the configuration file gotenberg.yml
|
||||
@@ -153,102 +68,146 @@ var levels = map[string]logrus.Level{
|
||||
"PANIC": logrus.PanicLevel,
|
||||
}
|
||||
|
||||
type wrongLoggingLevelError struct{}
|
||||
type wrongLogsLevelError struct{}
|
||||
|
||||
const wrongLoggingLevelErrorMessage = "Accepted values for logging level: DEBUG, INFO, WARN, ERROR, FATAL, PANIC"
|
||||
const wrongLogsLevelErrorMessage = "accepted values for logs level: DEBUG, INFO, WARN, ERROR, FATAL, PANIC"
|
||||
|
||||
func (e *wrongLoggingLevelError) Error() string {
|
||||
return wrongLoggingLevelErrorMessage
|
||||
func (e *wrongLogsLevelError) Error() string {
|
||||
return wrongLogsLevelErrorMessage
|
||||
}
|
||||
|
||||
// getLoggingLevelFromFileConfig returns a logrus level if a matching was found
|
||||
// with the one defined by the user.
|
||||
// If no match, throws an error.
|
||||
func getLoggingLevelFromFileConfig(c *fileConfig) (logrus.Level, error) {
|
||||
l, ok := levels[c.Logs.Level]
|
||||
// WithLogsLevel sets the logs level.
|
||||
// If the given string does not match with a logrus level,
|
||||
// throws an error.
|
||||
func WithLogsLevel(level string) error {
|
||||
l, ok := levels[level]
|
||||
if !ok {
|
||||
return 999, &wrongLoggingLevelError{}
|
||||
return &wrongLogsLevelError{}
|
||||
}
|
||||
|
||||
return l, nil
|
||||
config.logsLevel = l
|
||||
return nil
|
||||
}
|
||||
|
||||
// levels associates logging formats as defined in the configuration file gotenberg.yml
|
||||
// GetLogsLevel returns the current logs level.
|
||||
func GetLogsLevel() logrus.Level {
|
||||
return config.logsLevel
|
||||
}
|
||||
|
||||
// formatters associates logging formatter as defined in the configuration file gotenberg.yml
|
||||
// with its counterpart from the logrus library.
|
||||
var formatters = map[string]logrus.Formatter{
|
||||
"text": &logrus.TextFormatter{},
|
||||
"json": &logrus.JSONFormatter{},
|
||||
}
|
||||
|
||||
type wrongLoggingFormatError struct{}
|
||||
type wrongLogsFormatterError struct{}
|
||||
|
||||
const wrongLoggingFormatErrorMessage = "Accepted value for logging format: text, json"
|
||||
const wrongLogsFormatterErrorMessage = "accepted value for logs formatter: text, json"
|
||||
|
||||
func (e *wrongLoggingFormatError) Error() string {
|
||||
return wrongLoggingFormatErrorMessage
|
||||
func (e *wrongLogsFormatterError) Error() string {
|
||||
return wrongLogsFormatterErrorMessage
|
||||
}
|
||||
|
||||
// getLoggingLevelFromFileConfig returns a logrus Formatter if a matching was found
|
||||
// with the format defined by the user.
|
||||
// If no match, throws an error.
|
||||
func getLoggingFormatterFromFileConfig(c *fileConfig) (logrus.Formatter, error) {
|
||||
f, ok := formatters[c.Logs.Format]
|
||||
// WithLogsFormatter sets the logs formatter.
|
||||
// If the given string does not match with a logrus formatter,
|
||||
// throws an error.
|
||||
func WithLogsFormatter(formatter string) error {
|
||||
f, ok := formatters[formatter]
|
||||
if !ok {
|
||||
return nil, &wrongLoggingFormatError{}
|
||||
return &wrongLogsFormatterError{}
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// makeCommandsConfigs is a simple wrapper which populates all data related
|
||||
// to commands' configurations.
|
||||
func makeCommandsConfig(appConfig *AppConfig, fileConfig *fileConfig) error {
|
||||
appConfig.CommandsConfig = &CommandsConfig{}
|
||||
appConfig.CommandsConfig.Markdown = &CommandConfig{}
|
||||
appConfig.CommandsConfig.HTML = &CommandConfig{}
|
||||
appConfig.CommandsConfig.Office = &CommandConfig{}
|
||||
appConfig.CommandsConfig.Merge = &CommandConfig{}
|
||||
|
||||
appConfig.CommandsConfig.Markdown.Timeout = fileConfig.Commands.Markdown.Timeout
|
||||
appConfig.CommandsConfig.HTML.Timeout = fileConfig.Commands.HTML.Timeout
|
||||
appConfig.CommandsConfig.Office.Timeout = fileConfig.Commands.Office.Timeout
|
||||
appConfig.CommandsConfig.Merge.Timeout = fileConfig.Commands.Merge.Timeout
|
||||
|
||||
tmplMarkdown, err := getCommandTemplate(fileConfig.Commands.Markdown.Template, "Markdown")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmplHTML, err := getCommandTemplate(fileConfig.Commands.HTML.Template, "HTML")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmplOffice, err := getCommandTemplate(fileConfig.Commands.Office.Template, "Office")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmplMerge, err := getCommandTemplate(fileConfig.Commands.Merge.Template, "Merge")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appConfig.CommandsConfig.Markdown.Template = tmplMarkdown
|
||||
appConfig.CommandsConfig.HTML.Template = tmplHTML
|
||||
appConfig.CommandsConfig.Office.Template = tmplOffice
|
||||
appConfig.CommandsConfig.Merge.Template = tmplMerge
|
||||
|
||||
config.logsFormatter = f
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCommandTemplate is a simple helper for parsing a command template as defined by the user.
|
||||
// If the user gives us a wrong template, throws an error.
|
||||
func getCommandTemplate(command string, commandName string) (*template.Template, error) {
|
||||
t, err := template.New(commandName).Parse(command)
|
||||
// GetLogsFormatter returns the current logs formatter.
|
||||
func GetLogsFormatter() logrus.Formatter {
|
||||
return config.logsFormatter
|
||||
}
|
||||
|
||||
// WithLock sets the lock strategy.
|
||||
func WithLock(lock bool) {
|
||||
config.lock = lock
|
||||
}
|
||||
|
||||
// HasLock returns the current lock strategy.
|
||||
func HasLock() bool {
|
||||
return config.lock
|
||||
}
|
||||
|
||||
type interpreterEmptyError struct {
|
||||
command string
|
||||
}
|
||||
|
||||
const interpreterEmptyErrorMessage = "the interepreter for command %s should not be empty"
|
||||
|
||||
func (e *interpreterEmptyError) Error() string {
|
||||
return fmt.Sprintf(interpreterEmptyErrorMessage, e.command)
|
||||
}
|
||||
|
||||
// NewCommand instantiates a Command. If the given command string
|
||||
// is not a valid template, throws an error.
|
||||
func NewCommand(command string, interpreter string, timeout int) (*Command, error) {
|
||||
t, err := template.New(command).Parse(command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
if interpreter == "" {
|
||||
return nil, &interpreterEmptyError{command}
|
||||
}
|
||||
|
||||
return &Command{t, strings.Fields(interpreter), timeout}, nil
|
||||
}
|
||||
|
||||
type fileExtensionAlreadyUsedError struct {
|
||||
extension string
|
||||
command *Command
|
||||
existingCommand *Command
|
||||
}
|
||||
|
||||
const fileExtensionAlreadyUsedErrorMessage = "file extension %s from command %s is already used by command %s"
|
||||
|
||||
func (e *fileExtensionAlreadyUsedError) Error() string {
|
||||
return fmt.Sprintf(fileExtensionAlreadyUsedErrorMessage, e.extension, e.command.Template.Name(), e.existingCommand.Template.Name())
|
||||
}
|
||||
|
||||
// WithCommand adds a Command instance and associates it with the given
|
||||
// file extension. If the file extension is already used by another Command
|
||||
// instance, throws an error.
|
||||
func WithCommand(extension string, command *Command) error {
|
||||
if config.commands == nil {
|
||||
config.commands = make(map[string]*Command)
|
||||
}
|
||||
|
||||
existingCommand, ok := config.commands[extension]
|
||||
if ok {
|
||||
return &fileExtensionAlreadyUsedError{extension, command, existingCommand}
|
||||
}
|
||||
|
||||
config.commands[extension] = command
|
||||
return nil
|
||||
}
|
||||
|
||||
type noCommandFoundForFileExtensionError struct {
|
||||
extension string
|
||||
}
|
||||
|
||||
const noCommandFoundForFileExtensionErrorMessage = "no command found for file extension %s"
|
||||
|
||||
func (e *noCommandFoundForFileExtensionError) Error() string {
|
||||
return fmt.Sprintf(noCommandFoundForFileExtensionErrorMessage, e.extension)
|
||||
}
|
||||
|
||||
// GetCommand returns the Command instance associated with the given
|
||||
// file extension. If no Command instance found, throws an error.
|
||||
func GetCommand(extension string) (*Command, error) {
|
||||
c, ok := config.commands[extension]
|
||||
if !ok {
|
||||
return nil, &noCommandFoundForFileExtensionError{extension}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -1,77 +1,201 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func TestNewAppConfig(t *testing.T) {
|
||||
var path string
|
||||
func TestReset(t *testing.T) {
|
||||
c := &appConfig{}
|
||||
config.port = "3000"
|
||||
Reset()
|
||||
|
||||
// case 1: uses an empty configuration file path.
|
||||
if _, err := NewAppConfig(""); err == nil {
|
||||
t.Error("AppConfig should not have been instantiated by using an empty configuration file path")
|
||||
}
|
||||
|
||||
// case 2: uses a broken configuration file.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/broken-gotenberg.yml")
|
||||
if _, err := NewAppConfig(path); err == nil {
|
||||
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
|
||||
}
|
||||
|
||||
// case 3: uses a configuration file with a wrong logging level.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-logging-level-gotenberg.yml")
|
||||
if _, err := NewAppConfig(path); err == nil {
|
||||
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
|
||||
}
|
||||
|
||||
// case 4: uses a configuration file with a wrong logging format.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-logging-format-gotenberg.yml")
|
||||
if _, err := NewAppConfig(path); err == nil {
|
||||
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
|
||||
}
|
||||
|
||||
// case 5: uses a configuration file with a wrong markdown command template.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-markdown-command-template-gotenberg.yml")
|
||||
if _, err := NewAppConfig(path); err == nil {
|
||||
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
|
||||
}
|
||||
|
||||
// case 6: uses a configuration file with a wrong HTML command template.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-html-command-template-gotenberg.yml")
|
||||
if _, err := NewAppConfig(path); err == nil {
|
||||
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
|
||||
}
|
||||
|
||||
// case 7: uses a configuration file with a wrong Office command template.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-office-command-template-gotenberg.yml")
|
||||
if _, err := NewAppConfig(path); err == nil {
|
||||
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
|
||||
}
|
||||
|
||||
// case 8: uses a configuration file with a wrong merge command template.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-merge-command-template-gotenberg.yml")
|
||||
if _, err := NewAppConfig(path); err == nil {
|
||||
t.Errorf("AppConfig should not have been instantiated with '%s'", path)
|
||||
}
|
||||
|
||||
// case 9: uses a correct configuration file.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/gotenberg.yml")
|
||||
if _, err := NewAppConfig(path); err != nil {
|
||||
t.Errorf("AppConfig should have been instantiated with '%s'", path)
|
||||
if c.port != config.port {
|
||||
t.Error("Configuration should have been reset")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrongLoggingLevelError(t *testing.T) {
|
||||
err := &wrongLoggingLevelError{}
|
||||
if err.Error() != wrongLoggingLevelErrorMessage {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), wrongLoggingLevelErrorMessage)
|
||||
func TestWithPort(t *testing.T) {
|
||||
port := "3000"
|
||||
WithPort(port)
|
||||
|
||||
if config.port != port {
|
||||
t.Errorf("Configuration populated with a wrong port: got '%s' want '%s'", config.port, port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrongLoggingFormatError(t *testing.T) {
|
||||
err := &wrongLoggingFormatError{}
|
||||
if err.Error() != wrongLoggingFormatErrorMessage {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), wrongLoggingFormatErrorMessage)
|
||||
func TestGetPort(t *testing.T) {
|
||||
port := "3000"
|
||||
config.port = port
|
||||
|
||||
if GetPort() != port {
|
||||
t.Errorf("Configuration returned a wrong port: got '%s' want '%s'", GetPort(), port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrongLogsLevelError(t *testing.T) {
|
||||
err := &wrongLogsLevelError{}
|
||||
if err.Error() != wrongLogsLevelErrorMessage {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), wrongLogsLevelErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithLogsLevel(t *testing.T) {
|
||||
var lvl string
|
||||
|
||||
// case 1: uses a wrong logs level.
|
||||
lvl = "text"
|
||||
if err := WithLogsLevel(lvl); err == nil {
|
||||
t.Errorf("Configuration should not have been populated by using '%s' as logs level", lvl)
|
||||
}
|
||||
|
||||
// case 2: uses a correct logs level.
|
||||
lvl = "DEBUG"
|
||||
if err := WithLogsLevel(lvl); err != nil {
|
||||
t.Errorf("Configuration should have been populated by using '%s' as logs level", lvl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLogsLevel(t *testing.T) {
|
||||
lvl := logrus.DebugLevel
|
||||
config.logsLevel = lvl
|
||||
|
||||
if GetLogsLevel() != lvl {
|
||||
t.Errorf("Configuration returned a wrong logs level: got '%s' want '%s'", GetLogsLevel(), lvl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrongLogsFormatterError(t *testing.T) {
|
||||
err := &wrongLogsFormatterError{}
|
||||
if err.Error() != wrongLogsFormatterErrorMessage {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), wrongLogsFormatterErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithLogsFormatter(t *testing.T) {
|
||||
var formatter string
|
||||
|
||||
// case 1: uses a wrong logs formatter.
|
||||
formatter = "DEBUG"
|
||||
if err := WithLogsFormatter(formatter); err == nil {
|
||||
t.Errorf("Configuration should not have been populated by using '%s' as logs formatter", formatter)
|
||||
}
|
||||
|
||||
// case 2: uses a correct logs formatter.
|
||||
formatter = "text"
|
||||
if err := WithLogsFormatter(formatter); err != nil {
|
||||
t.Errorf("Configuration should have been populated by using '%s' as logs formatter", formatter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLogsFormatter(t *testing.T) {
|
||||
formatter := &logrus.TextFormatter{}
|
||||
config.logsFormatter = formatter
|
||||
|
||||
if GetLogsFormatter() != formatter {
|
||||
t.Errorf("Configuration returned a wrong logs formatter: got '%v' want '%v'", GetLogsFormatter(), formatter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithLock(t *testing.T) {
|
||||
lock := true
|
||||
WithLock(lock)
|
||||
|
||||
if config.lock != lock {
|
||||
t.Errorf("Configuration populated with a wrong lock strategy: got '%t' want '%t'", config.lock, lock)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasLock(t *testing.T) {
|
||||
lock := true
|
||||
config.lock = true
|
||||
|
||||
if HasLock() != lock {
|
||||
t.Errorf("Configuration returned a wrong port: got '%t' want '%t'", HasLock(), lock)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpreterEmptyError(t *testing.T) {
|
||||
err := &interpreterEmptyError{"echo hello world"}
|
||||
expected := fmt.Sprintf(interpreterEmptyErrorMessage, err.command)
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCommand(t *testing.T) {
|
||||
var cmd string
|
||||
|
||||
// case 1: uses a wrong command template.
|
||||
cmd = "pdftk {{ range $filePath := FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
if _, err := NewCommand(cmd, "/bin/sh -c", 0); err == nil {
|
||||
t.Errorf("Command should not have been instantiated by using '%s' as command template", cmd)
|
||||
}
|
||||
|
||||
// case 2: uses a correct command template.
|
||||
cmd = "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||
if _, err := NewCommand(cmd, "/bin/sh -c", 0); err != nil {
|
||||
t.Errorf("Command should have been instantiated by using '%s' as command template", cmd)
|
||||
}
|
||||
|
||||
// case 3: uses an empty interpreter.
|
||||
if _, err := NewCommand(cmd, "", 0); err == nil {
|
||||
t.Error("Command should not have been instantiated by using an empty interpreter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileExtensionAlreadyUsedError(t *testing.T) {
|
||||
ext := ".pdf"
|
||||
cmd1, _ := NewCommand("echo", "/bin/sh -c", 0)
|
||||
cmd2, _ := NewCommand("echo", "/bin/sh -c", 0)
|
||||
err := &fileExtensionAlreadyUsedError{ext, cmd1, cmd2}
|
||||
expected := fmt.Sprintf(fileExtensionAlreadyUsedErrorMessage, err.extension, err.command.Template.Name(), err.existingCommand.Template.Name())
|
||||
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithCommand(t *testing.T) {
|
||||
ext := ".pdf"
|
||||
cmd, _ := NewCommand("echo", "/bin/sh -c", 0)
|
||||
|
||||
// case 1: uses a command with a file extension not already referenced.
|
||||
if err := WithCommand(ext, cmd); err != nil {
|
||||
t.Errorf("Configuration should have been populated by using a command with the file extension '%s'", ext)
|
||||
}
|
||||
|
||||
// case 2: uses a command with a file extension already referenced.
|
||||
if err := WithCommand(ext, cmd); err == nil {
|
||||
t.Errorf("Configuration should not have been populated by using a command with the file extension '%s'", ext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoCommandFoundForFileExtensionError(t *testing.T) {
|
||||
err := &noCommandFoundForFileExtensionError{".pdf"}
|
||||
expected := fmt.Sprintf(noCommandFoundForFileExtensionErrorMessage, err.extension)
|
||||
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCommand(t *testing.T) {
|
||||
Reset()
|
||||
ext := ".pdf"
|
||||
cmd, _ := NewCommand("echo", "/bin/sh -c", 0)
|
||||
WithCommand(ext, cmd)
|
||||
|
||||
// case 1: uses a file extension which has a command associated.
|
||||
if _, err := GetCommand(ext); err != nil {
|
||||
t.Errorf("Configuration should have been able to return a command by using the file extension '%s'", ext)
|
||||
}
|
||||
|
||||
// case 2: uses a file extension which has no command associated.
|
||||
ext = ".docx"
|
||||
if _, err := GetCommand(ext); err == nil {
|
||||
t.Errorf("Configuration should not have been able to return a command by using the file extension '%s'", ext)
|
||||
}
|
||||
}
|
||||
|
||||
99
app/config/parser.go
Normal file
99
app/config/parser.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// ParseFile instantiates the application's configuration using the given YAML file.
|
||||
func ParseFile(configurationFilePath string) error {
|
||||
fileConfig, err := readFile(configurationFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
WithPort(fileConfig.Port)
|
||||
|
||||
if err := WithLogsLevel(fileConfig.Logs.Level); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := WithLogsFormatter(fileConfig.Logs.Formatter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
WithLock(fileConfig.Commands.Lock)
|
||||
|
||||
// handles merge command first...
|
||||
cmd, err := NewCommand(fileConfig.Commands.Merge.Template, fileConfig.Commands.Merge.Interpreter, fileConfig.Commands.Merge.Timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
WithCommand(".pdf", cmd)
|
||||
|
||||
// ...then conversion commands!
|
||||
for _, command := range fileConfig.Commands.Conversions {
|
||||
cmd, err := NewCommand(command.Template, command.Interpreter, command.Timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ext := range command.Extensions {
|
||||
if err := WithCommand(ext, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
// fileConfig gathers all data coming from the configuration file gotenberg.yml.
|
||||
fileConfig struct {
|
||||
Port string `yaml:"port"`
|
||||
Logs struct {
|
||||
Level string `yaml:"level"`
|
||||
Formatter string `yaml:"formatter"`
|
||||
} `yaml:"logs"`
|
||||
Commands struct {
|
||||
Lock bool `yaml:"lock"`
|
||||
Merge *mergeCommand `yaml:"merge"`
|
||||
Conversions []*conversionCommand `yaml:"conversions,omitempty"`
|
||||
} `yaml:"commands"`
|
||||
}
|
||||
|
||||
// mergeCommand gathers all data regarding the... merge command.
|
||||
mergeCommand struct {
|
||||
Template string `yaml:"template"`
|
||||
Interpreter string `yaml:"interpreter"`
|
||||
Timeout int `yaml:"timeout"`
|
||||
}
|
||||
|
||||
// conversionCommand gathers all data regarding a conversion command.
|
||||
conversionCommand struct {
|
||||
Template string `yaml:"template"`
|
||||
Interpreter string `yaml:"interpreter"`
|
||||
Timeout int `yaml:"timeout"`
|
||||
Extensions []string `yaml:"extensions"`
|
||||
}
|
||||
)
|
||||
|
||||
// readFile instantiates a fileConfig instance by reading
|
||||
// the given YAML file.
|
||||
func readFile(configurationFilePath string) (*fileConfig, error) {
|
||||
c := &fileConfig{}
|
||||
|
||||
data, err := ioutil.ReadFile(configurationFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
62
app/config/parser_test.go
Normal file
62
app/config/parser_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func load(configurationFilePath string) error {
|
||||
Reset()
|
||||
return ParseFile(configurationFilePath)
|
||||
}
|
||||
|
||||
func TestParseFile(t *testing.T) {
|
||||
var path string
|
||||
|
||||
// case 1: uses an empty configuration file path.
|
||||
if err := load(""); err == nil {
|
||||
t.Error("Configuration should not have been populated by using an empty configuration file path")
|
||||
}
|
||||
|
||||
// case 2: uses a broken configuration file.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/broken-gotenberg.yml")
|
||||
if err := load(path); err == nil {
|
||||
t.Errorf("Configuration should not have been populated with '%s'", path)
|
||||
}
|
||||
|
||||
// case 3: uses a configuration file with a wrong logging level.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-logging-level-gotenberg.yml")
|
||||
if err := load(path); err == nil {
|
||||
t.Errorf("Configuration should not have been populated with '%s'", path)
|
||||
}
|
||||
|
||||
// case 4: uses a configuration file with a wrong logging formatter.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-logging-formatter-gotenberg.yml")
|
||||
if err := load(path); err == nil {
|
||||
t.Errorf("Configuration should not have been populated with '%s'", path)
|
||||
}
|
||||
|
||||
// case 5: uses a configuration file with a wrong merge command template.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-merge-command-template-gotenberg.yml")
|
||||
if err := load(path); err == nil {
|
||||
t.Errorf("Configuration should not have been populated with '%s'", path)
|
||||
}
|
||||
|
||||
// case 6: uses a configuration file with a wrong command template.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/wrong-command-template-gotenberg.yml")
|
||||
if err := load(path); err == nil {
|
||||
t.Errorf("Configuration should not have been populated with '%s'", path)
|
||||
}
|
||||
|
||||
// case 7: uses a configuration file with a duplicate command.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/duplicate-command-gotenberg.yml")
|
||||
if err := load(path); err == nil {
|
||||
t.Errorf("Configuration should not have been populated with '%s'", path)
|
||||
}
|
||||
|
||||
// case 8: uses a correct configuration file.
|
||||
path, _ = filepath.Abs("../../_tests/configurations/gotenberg.yml")
|
||||
if err := load(path); err != nil {
|
||||
t.Errorf("Configuration should have been populated with '%s'", path)
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,40 @@ import (
|
||||
type key uint32
|
||||
|
||||
const (
|
||||
converterKey key = iota
|
||||
requestIDKey key = iota
|
||||
converterKey
|
||||
resultFilePathKey
|
||||
)
|
||||
|
||||
// WithRequestID populates a request's context with the given request ID
|
||||
// and returns the updated request.
|
||||
func WithRequestID(r *http.Request, requestID string) *http.Request {
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, requestIDKey, requestID)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type requestIDNotFoundError struct{}
|
||||
|
||||
const requestIDNotFoundErrorMessage = "the request ID was not found in request context"
|
||||
|
||||
func (e *requestIDNotFoundError) Error() string {
|
||||
return requestIDNotFoundErrorMessage
|
||||
}
|
||||
|
||||
// GetRequestID returns the request ID if found in
|
||||
// the request's context. Otherwise throws an error.
|
||||
func GetRequestID(r *http.Request) (string, error) {
|
||||
ID, ok := r.Context().Value(requestIDKey).(string)
|
||||
if !ok {
|
||||
return "", &requestIDNotFoundError{}
|
||||
}
|
||||
|
||||
return ID, nil
|
||||
}
|
||||
|
||||
// WithConverter populates a request's context with the given converter
|
||||
// and returns the updated request.
|
||||
func WithConverter(r *http.Request, converter *converter.Converter) *http.Request {
|
||||
@@ -28,7 +58,7 @@ func WithConverter(r *http.Request, converter *converter.Converter) *http.Reques
|
||||
|
||||
type converterNotFoundError struct{}
|
||||
|
||||
const converterNotFoundErrorMessage = "The converter was not found in request context"
|
||||
const converterNotFoundErrorMessage = "the converter was not found in request context"
|
||||
|
||||
func (e *converterNotFoundError) Error() string {
|
||||
return converterNotFoundErrorMessage
|
||||
@@ -57,7 +87,7 @@ func WithResultFilePath(r *http.Request, resultFilePath string) *http.Request {
|
||||
|
||||
type resultFilePathNotFoundError struct{}
|
||||
|
||||
const resultFilePathNotFoundErrorMessage = "The result file path was not found in request context"
|
||||
const resultFilePathNotFoundErrorMessage = "the result file path was not found in request context"
|
||||
|
||||
func (e *resultFilePathNotFoundError) Error() string {
|
||||
return resultFilePathNotFoundErrorMessage
|
||||
|
||||
@@ -6,8 +6,40 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/app/converter"
|
||||
|
||||
"github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
func TestWithRequestID(t *testing.T) {
|
||||
requestID := uuid.NewV4().String()
|
||||
req := WithRequestID(httptest.NewRequest(http.MethodPost, "/", nil), requestID)
|
||||
if ID, _ := req.Context().Value(requestIDKey).(string); ID != requestID {
|
||||
t.Errorf("Context returned a wrong converter: got '%s' want '%s'", ID, requestID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestIDNotFoundError(t *testing.T) {
|
||||
err := &requestIDNotFoundError{}
|
||||
if err.Error() != requestIDNotFoundErrorMessage {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), requestIDNotFoundErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRequestID(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
|
||||
// case 1: uses a request without a request ID entry in its context.
|
||||
if _, err := GetRequestID(req); err == nil {
|
||||
t.Error("Context should not have a request ID entry")
|
||||
}
|
||||
|
||||
// case 2: uses a request with a request ID entry in its context.
|
||||
req = WithRequestID(req, uuid.NewV4().String())
|
||||
if _, err := GetRequestID(req); err != nil {
|
||||
t.Error("Context should have a request ID entry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithConverter(t *testing.T) {
|
||||
req := WithConverter(httptest.NewRequest(http.MethodPost, "/", nil), &converter.Converter{})
|
||||
if c, _ := req.Context().Value(converterKey).(*converter.Converter); c == nil {
|
||||
@@ -15,6 +47,13 @@ func TestWithConverter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConverterNotFoundError(t *testing.T) {
|
||||
err := &converterNotFoundError{}
|
||||
if err.Error() != converterNotFoundErrorMessage {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), converterNotFoundErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConverter(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
|
||||
@@ -38,6 +77,13 @@ func TestWithResultFilePath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultFilePathNotFoundError(t *testing.T) {
|
||||
err := &resultFilePathNotFoundError{}
|
||||
if err.Error() != resultFilePathNotFoundErrorMessage {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), resultFilePathNotFoundErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetResultFilePath(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
|
||||
@@ -52,17 +98,3 @@ func TestGetResultFilePath(t *testing.T) {
|
||||
t.Error("Context should have a result file path entry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConverterNotFoundError(t *testing.T) {
|
||||
err := &converterNotFoundError{}
|
||||
if err.Error() != converterNotFoundErrorMessage {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), converterNotFoundErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultFilePathNotFoundError(t *testing.T) {
|
||||
err := &resultFilePathNotFoundError{}
|
||||
if err.Error() != resultFilePathNotFoundErrorMessage {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), resultFilePathNotFoundErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
gfile "github.com/thecodingmachine/gotenberg/app/converter/file"
|
||||
"github.com/thecodingmachine/gotenberg/app/converter/process"
|
||||
"github.com/thecodingmachine/gotenberg/app/logger"
|
||||
|
||||
"github.com/satori/go.uuid"
|
||||
)
|
||||
@@ -22,7 +23,7 @@ type Converter struct {
|
||||
// NoFileToConvertError is raided when a request has not file attached to it.
|
||||
type NoFileToConvertError struct{}
|
||||
|
||||
const noFileToConvertErrorMessage = "No file to convert"
|
||||
const noFileToConvertErrorMessage = "no file to convert"
|
||||
|
||||
func (e *NoFileToConvertError) Error() string {
|
||||
return noFileToConvertErrorMessage
|
||||
@@ -38,6 +39,8 @@ func NewConverter(r *http.Request) (*Converter, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Debugf("created working directory %s", c.workingDir)
|
||||
|
||||
reader, err := r.MultipartReader()
|
||||
if err != nil {
|
||||
return c, err
|
||||
@@ -75,7 +78,7 @@ func NewConverter(r *http.Request) (*Converter, error) {
|
||||
func (c *Converter) Convert() (string, error) {
|
||||
var filesPaths []string
|
||||
for _, f := range c.files {
|
||||
if f.Type != gfile.PDFType {
|
||||
if f.Extension != ".pdf" {
|
||||
path, err := process.Unconv(c.workingDir, f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -101,5 +104,11 @@ func (c *Converter) Convert() (string, error) {
|
||||
|
||||
// Clear removes all file inside its working directory.
|
||||
func (c *Converter) Clear() error {
|
||||
return os.RemoveAll(c.workingDir)
|
||||
if err := os.RemoveAll(c.workingDir); err != nil {
|
||||
logger.Error(fmt.Errorf("failed to remove working directory %s", c.workingDir))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debugf("removed working directory %s", c.workingDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/app/config"
|
||||
"github.com/thecodingmachine/gotenberg/app/converter/process"
|
||||
)
|
||||
|
||||
func makeRequest(filesPaths ...string) *http.Request {
|
||||
@@ -43,10 +42,17 @@ func makeRequest(filesPaths ...string) *http.Request {
|
||||
return req
|
||||
}
|
||||
|
||||
func loadCommandConfigs(configurationFilePath string) {
|
||||
func load(configurationFilePath string) {
|
||||
config.Reset()
|
||||
path, _ := filepath.Abs(configurationFilePath)
|
||||
c, _ := config.NewAppConfig(path)
|
||||
process.Load(c.CommandsConfig)
|
||||
config.ParseFile(path)
|
||||
}
|
||||
|
||||
func TestNoFileToConvertError(t *testing.T) {
|
||||
err := &NoFileToConvertError{}
|
||||
if err.Error() != noFileToConvertErrorMessage {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), noFileToConvertErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConverter(t *testing.T) {
|
||||
@@ -55,6 +61,8 @@ func TestNewConverter(t *testing.T) {
|
||||
oPath string
|
||||
)
|
||||
|
||||
load("../../_tests/configurations/gotenberg.yml")
|
||||
|
||||
// case 1: uses a request with a single file.
|
||||
path, _ = filepath.Abs("../../_tests/file.docx")
|
||||
if _, err := NewConverter(makeRequest(path)); err != nil {
|
||||
@@ -94,7 +102,7 @@ func TestConvert(t *testing.T) {
|
||||
c *Converter
|
||||
)
|
||||
|
||||
loadCommandConfigs("../../_tests/configurations/gotenberg.yml")
|
||||
load("../../_tests/configurations/gotenberg.yml")
|
||||
|
||||
// case 1: uses a request with a single file.
|
||||
path, _ = filepath.Abs("../../_tests/file.docx")
|
||||
@@ -111,7 +119,7 @@ func TestConvert(t *testing.T) {
|
||||
t.Errorf("Converter should have been able to convert '%s' and '%s' to PDF", path, oPath)
|
||||
}
|
||||
|
||||
loadCommandConfigs("../../_tests/configurations/timeout-gotenberg.yml")
|
||||
load("../../_tests/configurations/timeout-gotenberg.yml")
|
||||
|
||||
// case 3: uses a request with a single file and a configuration with an unsuitable timeout for the conversion commands.
|
||||
path, _ = filepath.Abs("../../_tests/file.docx")
|
||||
@@ -120,7 +128,7 @@ func TestConvert(t *testing.T) {
|
||||
t.Errorf("Converter should not have been able to convert '%s' to PDF", path)
|
||||
}
|
||||
|
||||
loadCommandConfigs("../../_tests/configurations/merge-timeout-gotenberg.yml")
|
||||
load("../../_tests/configurations/merge-timeout-gotenberg.yml")
|
||||
|
||||
// case 4: uses a request with two files and a configuration with an unsuitable timeout for the merge command.
|
||||
path, _ = filepath.Abs("../../_tests/file.pdf")
|
||||
@@ -132,16 +140,11 @@ func TestConvert(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClear(t *testing.T) {
|
||||
load("../../_tests/configurations/gotenberg.yml")
|
||||
|
||||
path, _ := filepath.Abs("../../_tests/file.docx")
|
||||
c, _ := NewConverter(makeRequest(path))
|
||||
if err := c.Clear(); err != nil {
|
||||
t.Error("Converter should have been able to clear itself")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoFileToConvertError(t *testing.T) {
|
||||
err := &NoFileToConvertError{}
|
||||
if err.Error() != noFileToConvertErrorMessage {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), noFileToConvertErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,71 +7,32 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/app/config"
|
||||
"github.com/thecodingmachine/gotenberg/app/logger"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
// File represents a file which has been created
|
||||
// from a request.
|
||||
type File struct {
|
||||
// Type is the kind of file.
|
||||
Type Type
|
||||
// Extension is the extension of the file.
|
||||
Extension string
|
||||
// Path is the file path.
|
||||
Path string
|
||||
}
|
||||
|
||||
// Type represents what kind of file we're dealing with.
|
||||
type Type uint32
|
||||
|
||||
const (
|
||||
// PDFType represents a... PDF file.
|
||||
PDFType Type = iota
|
||||
// MarkdownType represents a... Markdown file.
|
||||
MarkdownType
|
||||
// HTMLType represents an... HTML file.
|
||||
HTMLType
|
||||
// OfficeType represents an... Office document.
|
||||
OfficeType
|
||||
)
|
||||
|
||||
// filesTypes associates a file extension with its file kind counterpart.
|
||||
var filesTypes = map[string]Type{
|
||||
".pdf": PDFType,
|
||||
".md": MarkdownType,
|
||||
".htm": HTMLType,
|
||||
".html": HTMLType,
|
||||
".doc": OfficeType,
|
||||
".docx": OfficeType,
|
||||
".odt": OfficeType,
|
||||
".xls": OfficeType,
|
||||
".xlsx": OfficeType,
|
||||
".ods": OfficeType,
|
||||
".ppt": OfficeType,
|
||||
".pptx": OfficeType,
|
||||
".odp": OfficeType,
|
||||
}
|
||||
|
||||
type fileTypeNotFoundError struct {
|
||||
fileName string
|
||||
}
|
||||
|
||||
func (e *fileTypeNotFoundError) Error() string {
|
||||
return fmt.Sprintf("File type was not found for '%s'", e.fileName)
|
||||
}
|
||||
|
||||
// NewFile creates a file in the considered directory.
|
||||
// Returns a *File instance or an error if something bad happened.
|
||||
func NewFile(workingDir string, r io.Reader, fileName string) (*File, error) {
|
||||
ext := filepath.Ext(fileName)
|
||||
|
||||
t, ok := filesTypes[ext]
|
||||
if !ok {
|
||||
return nil, &fileTypeNotFoundError{fileName: fileName}
|
||||
if _, err := config.GetCommand(ext); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f := &File{
|
||||
Path: MakeFilePath(workingDir, ext),
|
||||
Type: t,
|
||||
}
|
||||
f := &File{ext, MakeFilePath(workingDir, ext)}
|
||||
|
||||
file, err := os.Create(f.Path)
|
||||
if err != nil {
|
||||
@@ -80,7 +41,7 @@ func NewFile(workingDir string, r io.Reader, fileName string) (*File, error) {
|
||||
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, r)
|
||||
n, err := io.Copy(file, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -88,6 +49,7 @@ func NewFile(workingDir string, r io.Reader, fileName string) (*File, error) {
|
||||
// resets the read pointer.
|
||||
file.Seek(0, 0)
|
||||
|
||||
logger.Debugf("working file %s has been created from %s (%s copied)", f.Path, fileName, humanize.Bytes(uint64(n)))
|
||||
return f, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,22 @@ package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/app/config"
|
||||
)
|
||||
|
||||
func load(configurationFilePath string) {
|
||||
config.Reset()
|
||||
path, _ := filepath.Abs(configurationFilePath)
|
||||
config.ParseFile(path)
|
||||
}
|
||||
|
||||
func TestNewFile(t *testing.T) {
|
||||
load("../../../_tests/configurations/gotenberg.yml")
|
||||
|
||||
workingDir := "test"
|
||||
os.Mkdir(workingDir, 0666)
|
||||
|
||||
@@ -17,21 +26,13 @@ func TestNewFile(t *testing.T) {
|
||||
t.Error("File should not have been instantiated with an empty buffer")
|
||||
}
|
||||
|
||||
// case 2: uses a reader from a correct file type.
|
||||
// case 2: uses a file name.
|
||||
filePath, _ := filepath.Abs("../../../_tests/file.pdf")
|
||||
r, _ := os.Open(filePath)
|
||||
defer r.Close()
|
||||
if _, err := NewFile(workingDir, r, "file.pdf"); err != nil {
|
||||
t.Errorf("File should have been instantiated using a reader from '%s'", filePath)
|
||||
t.Errorf("File should have been instantiated using a reader of '%s'", filePath)
|
||||
}
|
||||
|
||||
os.RemoveAll(workingDir)
|
||||
}
|
||||
func TestFileTypeNotFoundError(t *testing.T) {
|
||||
fileName := "file.wp"
|
||||
err := &fileTypeNotFoundError{fileName: fileName}
|
||||
expected := fmt.Sprintf("File type was not found for '%s'", fileName)
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,15 @@ import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/app/config"
|
||||
gfile "github.com/thecodingmachine/gotenberg/app/converter/file"
|
||||
"github.com/thecodingmachine/gotenberg/app/logger"
|
||||
)
|
||||
|
||||
type runner struct {
|
||||
mu sync.Mutex
|
||||
commandsConfig *config.CommandsConfig
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var forest = &runner{}
|
||||
@@ -25,17 +24,27 @@ type commandTimeoutError struct {
|
||||
timeout int
|
||||
}
|
||||
|
||||
const commandTimeoutErrorMessage = "the command %s has reached the %d second(s) timeout"
|
||||
|
||||
func (e *commandTimeoutError) Error() string {
|
||||
return fmt.Sprintf("The command '%s' has reached the %d second(s) timeout", e.command, e.timeout)
|
||||
return fmt.Sprintf(commandTimeoutErrorMessage, e.command, e.timeout)
|
||||
}
|
||||
|
||||
// run runs the given command. If timeout is reached or
|
||||
// something bad happened, returns an error.
|
||||
func (r *runner) run(command string, timeout int) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
func (r *runner) run(command string, interpreter []string, timeout int) error {
|
||||
if config.HasLock() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
} else {
|
||||
logger.Warn("lock disabled")
|
||||
}
|
||||
|
||||
binary := interpreter[0]
|
||||
parameters := append(interpreter[1:], command)
|
||||
cmd := exec.Command(binary, parameters...)
|
||||
logger.Debugf("executing command %s", cmd.Args)
|
||||
|
||||
cmd := exec.Command("/bin/sh", "-c", command)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -51,10 +60,8 @@ func (r *runner) run(command string, timeout int) error {
|
||||
if err := cmd.Process.Kill(); err != nil {
|
||||
return err
|
||||
}
|
||||
return &commandTimeoutError{
|
||||
command: command,
|
||||
timeout: timeout,
|
||||
}
|
||||
|
||||
return &commandTimeoutError{command, timeout}
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -64,64 +71,32 @@ func (r *runner) run(command string, timeout int) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Load loads the commands configuration coming from the application configuration.
|
||||
func Load(config *config.CommandsConfig) {
|
||||
forest.commandsConfig = config
|
||||
}
|
||||
|
||||
// conversionData will be applied to the data-driven templates of conversions commands.
|
||||
type conversionData struct {
|
||||
FilePath string
|
||||
ResultFilePath string
|
||||
}
|
||||
|
||||
type impossibleConversionError struct{}
|
||||
|
||||
const impossibleConversionErrorMessage = "Impossible conversion"
|
||||
|
||||
func (e *impossibleConversionError) Error() string {
|
||||
return impossibleConversionErrorMessage
|
||||
}
|
||||
|
||||
// Unconv converts a file to PDF and returns the new file path.
|
||||
func Unconv(workingDir string, file *gfile.File) (string, error) {
|
||||
cmdData := &conversionData{
|
||||
FilePath: file.Path,
|
||||
ResultFilePath: gfile.MakeFilePath(workingDir, ".pdf"),
|
||||
}
|
||||
cmdData := &conversionData{file.Path, gfile.MakeFilePath(workingDir, ".pdf")}
|
||||
|
||||
var (
|
||||
cmdTimeout int
|
||||
cmdTemplate *template.Template
|
||||
)
|
||||
|
||||
switch file.Type {
|
||||
case gfile.MarkdownType:
|
||||
cmdTimeout = forest.commandsConfig.Markdown.Timeout
|
||||
cmdTemplate = forest.commandsConfig.Markdown.Template
|
||||
break
|
||||
case gfile.HTMLType:
|
||||
cmdTimeout = forest.commandsConfig.HTML.Timeout
|
||||
cmdTemplate = forest.commandsConfig.HTML.Template
|
||||
break
|
||||
case gfile.OfficeType:
|
||||
cmdTimeout = forest.commandsConfig.Office.Timeout
|
||||
cmdTemplate = forest.commandsConfig.Office.Template
|
||||
break
|
||||
default:
|
||||
return "", &impossibleConversionError{}
|
||||
}
|
||||
|
||||
var data bytes.Buffer
|
||||
if err := cmdTemplate.Execute(&data, cmdData); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err := forest.run(data.String(), cmdTimeout)
|
||||
cmd, err := config.GetCommand(file.Extension)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var data bytes.Buffer
|
||||
if err := cmd.Template.Execute(&data, cmdData); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = forest.run(data.String(), cmd.Interpreter, cmd.Timeout)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
logger.Debugf("created %s from %s", cmdData.ResultFilePath, cmdData.FilePath)
|
||||
return cmdData.ResultFilePath, nil
|
||||
}
|
||||
|
||||
@@ -133,23 +108,23 @@ type mergeData struct {
|
||||
|
||||
// Merge merges many PDF files to one unique PDF file and returns the new file path.
|
||||
func Merge(workingDir string, filesPaths []string) (string, error) {
|
||||
cmdData := &mergeData{
|
||||
FilesPaths: filesPaths,
|
||||
ResultFilePath: gfile.MakeFilePath(workingDir, ".pdf"),
|
||||
}
|
||||
cmdData := &mergeData{filesPaths, gfile.MakeFilePath(workingDir, ".pdf")}
|
||||
|
||||
cmdTimeout := forest.commandsConfig.Merge.Timeout
|
||||
cmdTemplate := forest.commandsConfig.Merge.Template
|
||||
|
||||
var data bytes.Buffer
|
||||
if err := cmdTemplate.Execute(&data, cmdData); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err := forest.run(data.String(), cmdTimeout)
|
||||
cmd, err := config.GetCommand(".pdf")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var data bytes.Buffer
|
||||
if err := cmd.Template.Execute(&data, cmdData); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = forest.run(data.String(), cmd.Interpreter, cmd.Timeout)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
logger.Debugf("created %s from %+v", cmdData.ResultFilePath, cmdData.FilesPaths)
|
||||
return cmdData.ResultFilePath, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/app/config"
|
||||
@@ -22,42 +23,51 @@ func makeFile(workingDir string, fileName string) *gfile.File {
|
||||
return f
|
||||
}
|
||||
|
||||
func loadCommandConfigs(configurationFilePath string) {
|
||||
func load(configurationFilePath string) {
|
||||
config.Reset()
|
||||
path, _ := filepath.Abs(configurationFilePath)
|
||||
c, _ := config.NewAppConfig(path)
|
||||
Load(c.CommandsConfig)
|
||||
config.ParseFile(path)
|
||||
}
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
path, _ := filepath.Abs("../../../_tests/configurations/gotenberg.yml")
|
||||
c, _ := config.NewAppConfig(path)
|
||||
Load(c.CommandsConfig)
|
||||
func TestCommandTimeoutError(t *testing.T) {
|
||||
err := &commandTimeoutError{"echo hello", 30}
|
||||
expected := fmt.Sprintf(commandTimeoutErrorMessage, err.command, err.timeout)
|
||||
|
||||
if c.CommandsConfig != forest.commandsConfig {
|
||||
t.Error("Commands configuration should have been loaded correctly")
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
var cmd string
|
||||
|
||||
load("../../../_tests/configurations/gotenberg.yml")
|
||||
|
||||
// case 1: uses a simple command.
|
||||
cmd = "echo Hello world"
|
||||
if err := forest.run(cmd, 30); err != nil {
|
||||
if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 30); err != nil {
|
||||
t.Errorf("Command '%s' should have worked", cmd)
|
||||
}
|
||||
|
||||
// case 2: uses a simple command but with an unsuitable timeout.
|
||||
cmd = "sleep 5"
|
||||
if err := forest.run(cmd, 0); err == nil {
|
||||
if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 0); err == nil {
|
||||
t.Errorf("Command '%s' should not have worked", cmd)
|
||||
}
|
||||
|
||||
// case 3: uses a broken command.
|
||||
cmd = "helloworld"
|
||||
if err := forest.run(cmd, 30); err == nil {
|
||||
if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 30); err == nil {
|
||||
t.Errorf("Command '%s' should not have worked", cmd)
|
||||
}
|
||||
|
||||
load("../../../_tests/configurations/no-lock-gotenberg.yml")
|
||||
|
||||
// case 4: uses a configuration with a no lock strategy.
|
||||
cmd = "echo Hello world"
|
||||
if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 30); err != nil {
|
||||
t.Errorf("Command '%s' should have worked", cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnconv(t *testing.T) {
|
||||
@@ -66,7 +76,7 @@ func TestUnconv(t *testing.T) {
|
||||
workingDir := "test"
|
||||
os.Mkdir(workingDir, 0666)
|
||||
|
||||
loadCommandConfigs("../../../_tests/configurations/gotenberg.yml")
|
||||
load("../../../_tests/configurations/gotenberg.yml")
|
||||
|
||||
// case 1: uses an Markdown file type.
|
||||
file = makeFile(workingDir, "file.md")
|
||||
@@ -92,7 +102,7 @@ func TestUnconv(t *testing.T) {
|
||||
t.Errorf("Converting '%s' to PDF should not have worked", file.Path)
|
||||
}
|
||||
|
||||
loadCommandConfigs("../../../_tests/configurations/timeout-gotenberg.yml")
|
||||
load("../../../_tests/configurations/timeout-gotenberg.yml")
|
||||
|
||||
// case 5: uses a command with an unsuitable timeout.
|
||||
file = makeFile(workingDir, "file.docx")
|
||||
@@ -107,7 +117,7 @@ func TestMerge(t *testing.T) {
|
||||
workingDir := "test"
|
||||
os.Mkdir(workingDir, 0666)
|
||||
|
||||
loadCommandConfigs("../../../_tests/configurations/gotenberg.yml")
|
||||
load("../../../_tests/configurations/gotenberg.yml")
|
||||
|
||||
var filesPaths []string
|
||||
path, _ := filepath.Abs("../../../_tests/file.pdf")
|
||||
@@ -119,7 +129,7 @@ func TestMerge(t *testing.T) {
|
||||
t.Error("Merge should have worked")
|
||||
}
|
||||
|
||||
loadCommandConfigs("../../../_tests/configurations/timeout-gotenberg.yml")
|
||||
load("../../../_tests/configurations/timeout-gotenberg.yml")
|
||||
|
||||
// case 2: uses a command with an unsuitable timeout.
|
||||
if _, err := Merge(workingDir, filesPaths); err == nil {
|
||||
@@ -128,21 +138,3 @@ func TestMerge(t *testing.T) {
|
||||
|
||||
os.RemoveAll(workingDir)
|
||||
}
|
||||
|
||||
func TestImpossibleConversionError(t *testing.T) {
|
||||
err := &impossibleConversionError{}
|
||||
if err.Error() != impossibleConversionErrorMessage {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), impossibleConversionErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandTimeoutError(t *testing.T) {
|
||||
err := &commandTimeoutError{
|
||||
command: "echo hello",
|
||||
timeout: 30,
|
||||
}
|
||||
expected := fmt.Sprintf("The command '%s' has reached the %d second(s) timeout", err.command, err.timeout)
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ import (
|
||||
ghttp "github.com/thecodingmachine/gotenberg/app/http"
|
||||
"github.com/thecodingmachine/gotenberg/app/logger"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/justinas/alice"
|
||||
"github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
// GetHandlersChain returns the handlers chaining
|
||||
@@ -23,8 +25,10 @@ func GetHandlersChain() http.Handler {
|
||||
|
||||
type requestHasNoContentError struct{}
|
||||
|
||||
const requestHasNoContentErrorMessage = "request has not content"
|
||||
|
||||
func (e *requestHasNoContentError) Error() string {
|
||||
return "Request has not content"
|
||||
return requestHasNoContentErrorMessage
|
||||
}
|
||||
|
||||
// enforeContentLengthHandler checks if the request has content.
|
||||
@@ -37,6 +41,10 @@ func enforceContentLengthHandler(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
requestID := uuid.NewV4().String()
|
||||
r = context.WithRequestID(r, requestID)
|
||||
logger.Infof("identified new request (%s) with %s", humanize.Bytes(uint64(r.ContentLength)), requestID)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -120,6 +128,13 @@ func serveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
requestID, err := context.GetRequestID(r)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
logger.Debugf("serving result file %s for request %s...", path, requestID)
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", resultFileInfo.Name()))
|
||||
@@ -134,6 +149,8 @@ func serveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
logger.Error(err)
|
||||
} else {
|
||||
logger.Infof("result file %s (%s) sent for request %s", path, humanize.Bytes(uint64(resultFileInfo.Size())), requestID)
|
||||
}
|
||||
|
||||
cleanup(r)
|
||||
@@ -143,11 +160,11 @@ func serveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func cleanup(r *http.Request) {
|
||||
c, err := context.GetConverter(r)
|
||||
if err != nil {
|
||||
logger.Warn(err.Error())
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.Clear(); err != nil {
|
||||
logger.Warn(err.Error())
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/app/config"
|
||||
"github.com/thecodingmachine/gotenberg/app/context"
|
||||
"github.com/thecodingmachine/gotenberg/app/converter/process"
|
||||
|
||||
"github.com/justinas/alice"
|
||||
)
|
||||
@@ -46,10 +45,10 @@ func makeRequest(filesPaths ...string) *http.Request {
|
||||
return req
|
||||
}
|
||||
|
||||
func loadCommandConfigs(configurationFilePath string) {
|
||||
func load(configurationFilePath string) {
|
||||
config.Reset()
|
||||
path, _ := filepath.Abs(configurationFilePath)
|
||||
c, _ := config.NewAppConfig(path)
|
||||
process.Load(c.CommandsConfig)
|
||||
config.ParseFile(path)
|
||||
}
|
||||
|
||||
func fakeSuccessHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -63,6 +62,13 @@ func TestGetHandlersChain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestHasNoContentError(t *testing.T) {
|
||||
err := &requestHasNoContentError{}
|
||||
if err.Error() != requestHasNoContentErrorMessage {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), requestHasNoContentErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnforceContentLengthHandler(t *testing.T) {
|
||||
var (
|
||||
req *http.Request
|
||||
@@ -140,7 +146,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
loadCommandConfigs("../_tests/configurations/merge-timeout-gotenberg.yml")
|
||||
load("../_tests/configurations/merge-timeout-gotenberg.yml")
|
||||
|
||||
// case 3: sends a request with two files and using an unsuitable timeout for merge commande.
|
||||
path, _ = filepath.Abs("../_tests/file.pdf")
|
||||
@@ -152,7 +158,7 @@ func TestConvertHandler(t *testing.T) {
|
||||
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
loadCommandConfigs("../_tests/configurations/gotenberg.yml")
|
||||
load("../_tests/configurations/gotenberg.yml")
|
||||
|
||||
// case 4: sends a request with two files.
|
||||
path, _ = filepath.Abs("../_tests/file.pdf")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package http provides functions for detecting a request or a file content type.
|
||||
// Package http provides functions for detecting a request content type.
|
||||
package http
|
||||
|
||||
import (
|
||||
@@ -15,8 +15,10 @@ const MultipartFormDataContentType ContentType = "multipart/form-data"
|
||||
|
||||
type notAuthorizedContentTypeError struct{}
|
||||
|
||||
const notAuthorizedContentTypeErrorMessage = "accepted value for 'Content-Type': %s"
|
||||
|
||||
func (e *notAuthorizedContentTypeError) Error() string {
|
||||
return fmt.Sprintf("Accepted value for 'Content-Type': %s", MultipartFormDataContentType)
|
||||
return fmt.Sprintf(notAuthorizedContentTypeErrorMessage, MultipartFormDataContentType)
|
||||
}
|
||||
|
||||
// CheckAuthorizedContentType checks if the request header header has an authorized content type.
|
||||
|
||||
@@ -31,7 +31,8 @@ func TestCheckAuthorizedContentType(t *testing.T) {
|
||||
|
||||
func TestNotAuthorizedContentTypeError(t *testing.T) {
|
||||
err := ¬AuthorizedContentTypeError{}
|
||||
expected := fmt.Sprintf("Accepted value for 'Content-Type': %s", MultipartFormDataContentType)
|
||||
expected := fmt.Sprintf(notAuthorizedContentTypeErrorMessage, MultipartFormDataContentType)
|
||||
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
|
||||
}
|
||||
|
||||
@@ -17,10 +17,7 @@ var log = newLogger()
|
||||
|
||||
// newLogger instantiates a logger instance with default values.
|
||||
func newLogger() *logger {
|
||||
l := &logger{
|
||||
logger: logrus.New(),
|
||||
}
|
||||
|
||||
l := &logger{logrus.New()}
|
||||
l.logger.Out = os.Stdout
|
||||
l.logger.Level = logrus.InfoLevel
|
||||
|
||||
@@ -45,7 +42,7 @@ func Debug(message string) {
|
||||
|
||||
// Debugf is a wrapper of the logrus Debugf function.
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
log.logger.Debugf(format, args)
|
||||
log.logger.Debugf(format, args...)
|
||||
}
|
||||
|
||||
// Info is a wrapper of the logrus Info function.
|
||||
@@ -55,7 +52,7 @@ func Info(message string) {
|
||||
|
||||
// Infof is a wrapper of the logrus Infof function.
|
||||
func Infof(format string, args ...interface{}) {
|
||||
log.logger.Infof(format, args)
|
||||
log.logger.Infof(format, args...)
|
||||
}
|
||||
|
||||
// Warn is a wrapper of the logrus Warn function.
|
||||
@@ -63,6 +60,11 @@ func Warn(message string) {
|
||||
log.logger.Warn(message)
|
||||
}
|
||||
|
||||
// Warnf is wrapper of the logrus Warnf function.
|
||||
func Warnf(format string, args ...interface{}) {
|
||||
log.logger.Warnf(format, args...)
|
||||
}
|
||||
|
||||
// Error is a wrapper of the logrus Error function.
|
||||
func Error(err error) {
|
||||
log.logger.Error(err.Error())
|
||||
|
||||
17
main.go
17
main.go
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/app"
|
||||
"github.com/thecodingmachine/gotenberg/app/config"
|
||||
"github.com/thecodingmachine/gotenberg/app/converter/process"
|
||||
"github.com/thecodingmachine/gotenberg/app/logger"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -33,7 +32,7 @@ const defaultConfigurationFilePath = "gotenberg.yml"
|
||||
// main initializes the application, starts it, and handles
|
||||
// graceful shutdown.
|
||||
func main() {
|
||||
c, err := config.NewAppConfig(defaultConfigurationFilePath)
|
||||
err := config.ParseFile(defaultConfigurationFilePath)
|
||||
if err != nil {
|
||||
logger.SetLevel(logrus.InfoLevel)
|
||||
logger.Fatal(err)
|
||||
@@ -41,8 +40,8 @@ func main() {
|
||||
}
|
||||
|
||||
// defines our application logging.
|
||||
logger.SetLevel(c.Logs.Level)
|
||||
logger.SetFormatter(c.Logs.Formatter)
|
||||
logger.SetLevel(config.GetLogsLevel())
|
||||
logger.SetFormatter(config.GetLogsFormatter())
|
||||
|
||||
// defines our application router.
|
||||
r := mux.NewRouter()
|
||||
@@ -50,13 +49,13 @@ func main() {
|
||||
|
||||
// defines our server.
|
||||
s := &http.Server{
|
||||
Addr: fmt.Sprintf(":%s", c.Port),
|
||||
Addr: fmt.Sprintf(":%s", config.GetPort()),
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
process.Load(c.CommandsConfig)
|
||||
logger.Infof("Starting Gotenberg version %s", version)
|
||||
logger.Infof("Listening on port %s", c.Port)
|
||||
logger.Debugf("configuration loaded from file %s", defaultConfigurationFilePath)
|
||||
logger.Infof("starting Gotenberg version %s", version)
|
||||
logger.Infof("listening on port %s", config.GetPort())
|
||||
|
||||
// runs our server in a goroutine so that it doesn't block.
|
||||
go func() {
|
||||
@@ -85,6 +84,6 @@ func main() {
|
||||
s.Shutdown(ctx)
|
||||
|
||||
logger.SetLevel(logrus.InfoLevel)
|
||||
logger.Info("Bye!")
|
||||
logger.Info("bye!")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
payload:
|
||||
|
||||
- key: Version
|
||||
value: snapshot
|
||||
|
||||
- key: Latest
|
||||
value: 1.0.0
|
||||
value: snapshot
|
||||
@@ -8,7 +8,6 @@ tasks:
|
||||
- use: generate
|
||||
short: Generates all files from blueprints
|
||||
run:
|
||||
- BRANCH="$(git symbolic-ref --short HEAD)"; orbit generate -f .blueprints/README.blueprint.md -o README.md -p "Branch,${BRANCH}"
|
||||
- orbit generate -f .blueprints/Dockerfile.blueprint -o Dockerfile.ci -p "Image,CI"
|
||||
- orbit generate -f .blueprints/Dockerfile.blueprint -o Dockerfile -p "Image,MAIN"
|
||||
|
||||
|
||||
21
vendor/github.com/dustin/go-humanize/.travis.yml
generated
vendored
Normal file
21
vendor/github.com/dustin/go-humanize/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.3.x
|
||||
- 1.5.x
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- 1.9.x
|
||||
- master
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: master
|
||||
fast_finish: true
|
||||
install:
|
||||
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d -s .)
|
||||
- go tool vet .
|
||||
- go test -v -race ./...
|
||||
21
vendor/github.com/dustin/go-humanize/LICENSE
generated
vendored
Normal file
21
vendor/github.com/dustin/go-humanize/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
<http://www.opensource.org/licenses/mit-license.php>
|
||||
124
vendor/github.com/dustin/go-humanize/README.markdown
generated
vendored
Normal file
124
vendor/github.com/dustin/go-humanize/README.markdown
generated
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
# Humane Units [](https://travis-ci.org/dustin/go-humanize) [](https://godoc.org/github.com/dustin/go-humanize)
|
||||
|
||||
Just a few functions for helping humanize times and sizes.
|
||||
|
||||
`go get` it as `github.com/dustin/go-humanize`, import it as
|
||||
`"github.com/dustin/go-humanize"`, use it as `humanize`.
|
||||
|
||||
See [godoc](https://godoc.org/github.com/dustin/go-humanize) for
|
||||
complete documentation.
|
||||
|
||||
## Sizes
|
||||
|
||||
This lets you take numbers like `82854982` and convert them to useful
|
||||
strings like, `83 MB` or `79 MiB` (whichever you prefer).
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.
|
||||
```
|
||||
|
||||
## Times
|
||||
|
||||
This lets you take a `time.Time` and spit it out in relative terms.
|
||||
For example, `12 seconds ago` or `3 days from now`.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago.
|
||||
```
|
||||
|
||||
Thanks to Kyle Lemons for the time implementation from an IRC
|
||||
conversation one day. It's pretty neat.
|
||||
|
||||
## Ordinals
|
||||
|
||||
From a [mailing list discussion][odisc] where a user wanted to be able
|
||||
to label ordinals.
|
||||
|
||||
0 -> 0th
|
||||
1 -> 1st
|
||||
2 -> 2nd
|
||||
3 -> 3rd
|
||||
4 -> 4th
|
||||
[...]
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend.
|
||||
```
|
||||
|
||||
## Commas
|
||||
|
||||
Want to shove commas into numbers? Be my guest.
|
||||
|
||||
0 -> 0
|
||||
100 -> 100
|
||||
1000 -> 1,000
|
||||
1000000000 -> 1,000,000,000
|
||||
-100000 -> -100,000
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.
|
||||
```
|
||||
|
||||
## Ftoa
|
||||
|
||||
Nicer float64 formatter that removes trailing zeros.
|
||||
|
||||
```go
|
||||
fmt.Printf("%f", 2.24) // 2.240000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
|
||||
fmt.Printf("%f", 2.0) // 2.000000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
|
||||
```
|
||||
|
||||
## SI notation
|
||||
|
||||
Format numbers with [SI notation][sinotation].
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
humanize.SI(0.00000000223, "M") // 2.23 nM
|
||||
```
|
||||
|
||||
## English-specific functions
|
||||
|
||||
The following functions are in the `humanize/english` subpackage.
|
||||
|
||||
### Plurals
|
||||
|
||||
Simple English pluralization
|
||||
|
||||
```go
|
||||
english.PluralWord(1, "object", "") // object
|
||||
english.PluralWord(42, "object", "") // objects
|
||||
english.PluralWord(2, "bus", "") // buses
|
||||
english.PluralWord(99, "locus", "loci") // loci
|
||||
|
||||
english.Plural(1, "object", "") // 1 object
|
||||
english.Plural(42, "object", "") // 42 objects
|
||||
english.Plural(2, "bus", "") // 2 buses
|
||||
english.Plural(99, "locus", "loci") // 99 loci
|
||||
```
|
||||
|
||||
### Word series
|
||||
|
||||
Format comma-separated words lists with conjuctions:
|
||||
|
||||
```go
|
||||
english.WordSeries([]string{"foo"}, "and") // foo
|
||||
english.WordSeries([]string{"foo", "bar"}, "and") // foo and bar
|
||||
english.WordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar and baz
|
||||
|
||||
english.OxfordWordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar, and baz
|
||||
```
|
||||
|
||||
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
|
||||
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix
|
||||
31
vendor/github.com/dustin/go-humanize/big.go
generated
vendored
Normal file
31
vendor/github.com/dustin/go-humanize/big.go
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// order of magnitude (to a max order)
|
||||
func oomm(n, b *big.Int, maxmag int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
if mag == maxmag && maxmag >= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
||||
|
||||
// total order of magnitude
|
||||
// (same as above, but with no upper limit)
|
||||
func oom(n, b *big.Int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
||||
173
vendor/github.com/dustin/go-humanize/bigbytes.go
generated
vendored
Normal file
173
vendor/github.com/dustin/go-humanize/bigbytes.go
generated
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
bigIECExp = big.NewInt(1024)
|
||||
|
||||
// BigByte is one byte in bit.Ints
|
||||
BigByte = big.NewInt(1)
|
||||
// BigKiByte is 1,024 bytes in bit.Ints
|
||||
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp)
|
||||
// BigMiByte is 1,024 k bytes in bit.Ints
|
||||
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp)
|
||||
// BigGiByte is 1,024 m bytes in bit.Ints
|
||||
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp)
|
||||
// BigTiByte is 1,024 g bytes in bit.Ints
|
||||
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp)
|
||||
// BigPiByte is 1,024 t bytes in bit.Ints
|
||||
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp)
|
||||
// BigEiByte is 1,024 p bytes in bit.Ints
|
||||
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp)
|
||||
// BigZiByte is 1,024 e bytes in bit.Ints
|
||||
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp)
|
||||
// BigYiByte is 1,024 z bytes in bit.Ints
|
||||
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp)
|
||||
)
|
||||
|
||||
var (
|
||||
bigSIExp = big.NewInt(1000)
|
||||
|
||||
// BigSIByte is one SI byte in big.Ints
|
||||
BigSIByte = big.NewInt(1)
|
||||
// BigKByte is 1,000 SI bytes in big.Ints
|
||||
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp)
|
||||
// BigMByte is 1,000 SI k bytes in big.Ints
|
||||
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp)
|
||||
// BigGByte is 1,000 SI m bytes in big.Ints
|
||||
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp)
|
||||
// BigTByte is 1,000 SI g bytes in big.Ints
|
||||
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp)
|
||||
// BigPByte is 1,000 SI t bytes in big.Ints
|
||||
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp)
|
||||
// BigEByte is 1,000 SI p bytes in big.Ints
|
||||
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp)
|
||||
// BigZByte is 1,000 SI e bytes in big.Ints
|
||||
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp)
|
||||
// BigYByte is 1,000 SI z bytes in big.Ints
|
||||
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp)
|
||||
)
|
||||
|
||||
var bigBytesSizeTable = map[string]*big.Int{
|
||||
"b": BigByte,
|
||||
"kib": BigKiByte,
|
||||
"kb": BigKByte,
|
||||
"mib": BigMiByte,
|
||||
"mb": BigMByte,
|
||||
"gib": BigGiByte,
|
||||
"gb": BigGByte,
|
||||
"tib": BigTiByte,
|
||||
"tb": BigTByte,
|
||||
"pib": BigPiByte,
|
||||
"pb": BigPByte,
|
||||
"eib": BigEiByte,
|
||||
"eb": BigEByte,
|
||||
"zib": BigZiByte,
|
||||
"zb": BigZByte,
|
||||
"yib": BigYiByte,
|
||||
"yb": BigYByte,
|
||||
// Without suffix
|
||||
"": BigByte,
|
||||
"ki": BigKiByte,
|
||||
"k": BigKByte,
|
||||
"mi": BigMiByte,
|
||||
"m": BigMByte,
|
||||
"gi": BigGiByte,
|
||||
"g": BigGByte,
|
||||
"ti": BigTiByte,
|
||||
"t": BigTByte,
|
||||
"pi": BigPiByte,
|
||||
"p": BigPByte,
|
||||
"ei": BigEiByte,
|
||||
"e": BigEByte,
|
||||
"z": BigZByte,
|
||||
"zi": BigZiByte,
|
||||
"y": BigYByte,
|
||||
"yi": BigYiByte,
|
||||
}
|
||||
|
||||
var ten = big.NewInt(10)
|
||||
|
||||
func humanateBigBytes(s, base *big.Int, sizes []string) string {
|
||||
if s.Cmp(ten) < 0 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
c := (&big.Int{}).Set(s)
|
||||
val, mag := oomm(c, base, len(sizes)-1)
|
||||
suffix := sizes[mag]
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
|
||||
}
|
||||
|
||||
// BigBytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigBytes(82854982) -> 83 MB
|
||||
func BigBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
||||
return humanateBigBytes(s, bigSIExp, sizes)
|
||||
}
|
||||
|
||||
// BigIBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigIBytes(82854982) -> 79 MiB
|
||||
func BigIBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"}
|
||||
return humanateBigBytes(s, bigIECExp, sizes)
|
||||
}
|
||||
|
||||
// ParseBigBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See also: BigBytes, BigIBytes.
|
||||
//
|
||||
// ParseBigBytes("42 MB") -> 42000000, nil
|
||||
// ParseBigBytes("42 mib") -> 44040192, nil
|
||||
func ParseBigBytes(s string) (*big.Int, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
val := &big.Rat{}
|
||||
_, err := fmt.Sscanf(num, "%f", val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bigBytesSizeTable[extra]; ok {
|
||||
mv := (&big.Rat{}).SetInt(m)
|
||||
val.Mul(val, mv)
|
||||
rv := &big.Int{}
|
||||
rv.Div(val.Num(), val.Denom())
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
||||
143
vendor/github.com/dustin/go-humanize/bytes.go
generated
vendored
Normal file
143
vendor/github.com/dustin/go-humanize/bytes.go
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// IEC Sizes.
|
||||
// kibis of bits
|
||||
const (
|
||||
Byte = 1 << (iota * 10)
|
||||
KiByte
|
||||
MiByte
|
||||
GiByte
|
||||
TiByte
|
||||
PiByte
|
||||
EiByte
|
||||
)
|
||||
|
||||
// SI Sizes.
|
||||
const (
|
||||
IByte = 1
|
||||
KByte = IByte * 1000
|
||||
MByte = KByte * 1000
|
||||
GByte = MByte * 1000
|
||||
TByte = GByte * 1000
|
||||
PByte = TByte * 1000
|
||||
EByte = PByte * 1000
|
||||
)
|
||||
|
||||
var bytesSizeTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"kib": KiByte,
|
||||
"kb": KByte,
|
||||
"mib": MiByte,
|
||||
"mb": MByte,
|
||||
"gib": GiByte,
|
||||
"gb": GByte,
|
||||
"tib": TiByte,
|
||||
"tb": TByte,
|
||||
"pib": PiByte,
|
||||
"pb": PByte,
|
||||
"eib": EiByte,
|
||||
"eb": EByte,
|
||||
// Without suffix
|
||||
"": Byte,
|
||||
"ki": KiByte,
|
||||
"k": KByte,
|
||||
"mi": MiByte,
|
||||
"m": MByte,
|
||||
"gi": GiByte,
|
||||
"g": GByte,
|
||||
"ti": TiByte,
|
||||
"t": TByte,
|
||||
"pi": PiByte,
|
||||
"p": PByte,
|
||||
"ei": EiByte,
|
||||
"e": EByte,
|
||||
}
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
|
||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
}
|
||||
|
||||
// Bytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// Bytes(82854982) -> 83 MB
|
||||
func Bytes(s uint64) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||
return humanateBytes(s, 1000, sizes)
|
||||
}
|
||||
|
||||
// IBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// IBytes(82854982) -> 79 MiB
|
||||
func IBytes(s uint64) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
return humanateBytes(s, 1024, sizes)
|
||||
}
|
||||
|
||||
// ParseBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See Also: Bytes, IBytes.
|
||||
//
|
||||
// ParseBytes("42 MB") -> 42000000, nil
|
||||
// ParseBytes("42 mib") -> 44040192, nil
|
||||
func ParseBytes(s string) (uint64, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(num, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bytesSizeTable[extra]; ok {
|
||||
f *= float64(m)
|
||||
if f >= math.MaxUint64 {
|
||||
return 0, fmt.Errorf("too large: %v", s)
|
||||
}
|
||||
return uint64(f), nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
||||
116
vendor/github.com/dustin/go-humanize/comma.go
generated
vendored
Normal file
116
vendor/github.com/dustin/go-humanize/comma.go
generated
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Comma produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Comma(834142) -> 834,142
|
||||
func Comma(v int64) string {
|
||||
sign := ""
|
||||
|
||||
// Min int64 can't be negated to a usable value, so it has to be special cased.
|
||||
if v == math.MinInt64 {
|
||||
return "-9,223,372,036,854,775,808"
|
||||
}
|
||||
|
||||
if v < 0 {
|
||||
sign = "-"
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
parts := []string{"", "", "", "", "", "", ""}
|
||||
j := len(parts) - 1
|
||||
|
||||
for v > 999 {
|
||||
parts[j] = strconv.FormatInt(v%1000, 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
v = v / 1000
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(v))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
||||
|
||||
// Commaf produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Commaf(834142.32) -> 834,142.32
|
||||
func Commaf(v float64) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// CommafWithDigits works like the Commaf but limits the resulting
|
||||
// string to the given number of decimal places.
|
||||
//
|
||||
// e.g. CommafWithDigits(834142.32, 1) -> 834,142.3
|
||||
func CommafWithDigits(f float64, decimals int) string {
|
||||
return stripTrailingDigits(Commaf(f), decimals)
|
||||
}
|
||||
|
||||
// BigComma produces a string form of the given big.Int in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigComma(b *big.Int) string {
|
||||
sign := ""
|
||||
if b.Sign() < 0 {
|
||||
sign = "-"
|
||||
b.Abs(b)
|
||||
}
|
||||
|
||||
athousand := big.NewInt(1000)
|
||||
c := (&big.Int{}).Set(b)
|
||||
_, m := oom(c, athousand)
|
||||
parts := make([]string, m+1)
|
||||
j := len(parts) - 1
|
||||
|
||||
mod := &big.Int{}
|
||||
for b.Cmp(athousand) >= 0 {
|
||||
b.DivMod(b, athousand, mod)
|
||||
parts[j] = strconv.FormatInt(mod.Int64(), 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(b.Int64()))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
||||
40
vendor/github.com/dustin/go-humanize/commaf.go
generated
vendored
Normal file
40
vendor/github.com/dustin/go-humanize/commaf.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
// +build go1.6
|
||||
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BigCommaf produces a string form of the given big.Float in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigCommaf(v *big.Float) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v.Sign() < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v.Abs(v)
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(v.Text('f', -1), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
46
vendor/github.com/dustin/go-humanize/ftoa.go
generated
vendored
Normal file
46
vendor/github.com/dustin/go-humanize/ftoa.go
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func stripTrailingZeros(s string) string {
|
||||
offset := len(s) - 1
|
||||
for offset > 0 {
|
||||
if s[offset] == '.' {
|
||||
offset--
|
||||
break
|
||||
}
|
||||
if s[offset] != '0' {
|
||||
break
|
||||
}
|
||||
offset--
|
||||
}
|
||||
return s[:offset+1]
|
||||
}
|
||||
|
||||
func stripTrailingDigits(s string, digits int) string {
|
||||
if i := strings.Index(s, "."); i >= 0 {
|
||||
if digits <= 0 {
|
||||
return s[:i]
|
||||
}
|
||||
i++
|
||||
if i+digits >= len(s) {
|
||||
return s
|
||||
}
|
||||
return s[:i+digits]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Ftoa converts a float to a string with no trailing zeros.
|
||||
func Ftoa(num float64) string {
|
||||
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64))
|
||||
}
|
||||
|
||||
// FtoaWithDigits converts a float to a string but limits the resulting string
|
||||
// to the given number of decimal places, and no trailing zeros.
|
||||
func FtoaWithDigits(num float64, digits int) string {
|
||||
return stripTrailingZeros(stripTrailingDigits(strconv.FormatFloat(num, 'f', 6, 64), digits))
|
||||
}
|
||||
8
vendor/github.com/dustin/go-humanize/humanize.go
generated
vendored
Normal file
8
vendor/github.com/dustin/go-humanize/humanize.go
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Package humanize converts boring ugly numbers to human-friendly strings and back.
|
||||
|
||||
Durations can be turned into strings such as "3 days ago", numbers
|
||||
representing sizes like 82854982 into useful strings like, "83 MB" or
|
||||
"79 MiB" (whichever you prefer).
|
||||
*/
|
||||
package humanize
|
||||
192
vendor/github.com/dustin/go-humanize/number.go
generated
vendored
Normal file
192
vendor/github.com/dustin/go-humanize/number.go
generated
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
package humanize
|
||||
|
||||
/*
|
||||
Slightly adapted from the source to fit go-humanize.
|
||||
|
||||
Author: https://github.com/gorhill
|
||||
Source: https://gist.github.com/gorhill/5285193
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
renderFloatPrecisionMultipliers = [...]float64{
|
||||
1,
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
10000,
|
||||
100000,
|
||||
1000000,
|
||||
10000000,
|
||||
100000000,
|
||||
1000000000,
|
||||
}
|
||||
|
||||
renderFloatPrecisionRounders = [...]float64{
|
||||
0.5,
|
||||
0.05,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.00005,
|
||||
0.000005,
|
||||
0.0000005,
|
||||
0.00000005,
|
||||
0.000000005,
|
||||
0.0000000005,
|
||||
}
|
||||
)
|
||||
|
||||
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
|
||||
// * thousands separator
|
||||
// * decimal separator
|
||||
// * decimal precision
|
||||
//
|
||||
// Usage: s := RenderFloat(format, n)
|
||||
// The format parameter tells how to render the number n.
|
||||
//
|
||||
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
|
||||
//
|
||||
// Examples of format strings, given n = 12345.6789:
|
||||
// "#,###.##" => "12,345.67"
|
||||
// "#,###." => "12,345"
|
||||
// "#,###" => "12345,678"
|
||||
// "#\u202F###,##" => "12 345,68"
|
||||
// "#.###,###### => 12.345,678900
|
||||
// "" (aka default format) => 12,345.67
|
||||
//
|
||||
// The highest precision allowed is 9 digits after the decimal symbol.
|
||||
// There is also a version for integer number, FormatInteger(),
|
||||
// which is convenient for calls within template.
|
||||
func FormatFloat(format string, n float64) string {
|
||||
// Special cases:
|
||||
// NaN = "NaN"
|
||||
// +Inf = "+Infinity"
|
||||
// -Inf = "-Infinity"
|
||||
if math.IsNaN(n) {
|
||||
return "NaN"
|
||||
}
|
||||
if n > math.MaxFloat64 {
|
||||
return "Infinity"
|
||||
}
|
||||
if n < -math.MaxFloat64 {
|
||||
return "-Infinity"
|
||||
}
|
||||
|
||||
// default format
|
||||
precision := 2
|
||||
decimalStr := "."
|
||||
thousandStr := ","
|
||||
positiveStr := ""
|
||||
negativeStr := "-"
|
||||
|
||||
if len(format) > 0 {
|
||||
format := []rune(format)
|
||||
|
||||
// If there is an explicit format directive,
|
||||
// then default values are these:
|
||||
precision = 9
|
||||
thousandStr = ""
|
||||
|
||||
// collect indices of meaningful formatting directives
|
||||
formatIndx := []int{}
|
||||
for i, char := range format {
|
||||
if char != '#' && char != '0' {
|
||||
formatIndx = append(formatIndx, i)
|
||||
}
|
||||
}
|
||||
|
||||
if len(formatIndx) > 0 {
|
||||
// Directive at index 0:
|
||||
// Must be a '+'
|
||||
// Raise an error if not the case
|
||||
// index: 0123456789
|
||||
// +0.000,000
|
||||
// +000,000.0
|
||||
// +0000.00
|
||||
// +0000
|
||||
if formatIndx[0] == 0 {
|
||||
if format[formatIndx[0]] != '+' {
|
||||
panic("RenderFloat(): invalid positive sign directive")
|
||||
}
|
||||
positiveStr = "+"
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// Two directives:
|
||||
// First is thousands separator
|
||||
// Raise an error if not followed by 3-digit
|
||||
// 0123456789
|
||||
// 0.000,000
|
||||
// 000,000.00
|
||||
if len(formatIndx) == 2 {
|
||||
if (formatIndx[1] - formatIndx[0]) != 4 {
|
||||
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
|
||||
}
|
||||
thousandStr = string(format[formatIndx[0]])
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// One directive:
|
||||
// Directive is decimal separator
|
||||
// The number of digit-specifier following the separator indicates wanted precision
|
||||
// 0123456789
|
||||
// 0.00
|
||||
// 000,0000
|
||||
if len(formatIndx) == 1 {
|
||||
decimalStr = string(format[formatIndx[0]])
|
||||
precision = len(format) - formatIndx[0] - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate sign part
|
||||
var signStr string
|
||||
if n >= 0.000000001 {
|
||||
signStr = positiveStr
|
||||
} else if n <= -0.000000001 {
|
||||
signStr = negativeStr
|
||||
n = -n
|
||||
} else {
|
||||
signStr = ""
|
||||
n = 0.0
|
||||
}
|
||||
|
||||
// split number into integer and fractional parts
|
||||
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
|
||||
|
||||
// generate integer part string
|
||||
intStr := strconv.FormatInt(int64(intf), 10)
|
||||
|
||||
// add thousand separator if required
|
||||
if len(thousandStr) > 0 {
|
||||
for i := len(intStr); i > 3; {
|
||||
i -= 3
|
||||
intStr = intStr[:i] + thousandStr + intStr[i:]
|
||||
}
|
||||
}
|
||||
|
||||
// no fractional part, we can leave now
|
||||
if precision == 0 {
|
||||
return signStr + intStr
|
||||
}
|
||||
|
||||
// generate fractional part
|
||||
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
|
||||
// may need padding
|
||||
if len(fracStr) < precision {
|
||||
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
|
||||
}
|
||||
|
||||
return signStr + intStr + decimalStr + fracStr
|
||||
}
|
||||
|
||||
// FormatInteger produces a formatted number as string.
|
||||
// See FormatFloat.
|
||||
func FormatInteger(format string, n int) string {
|
||||
return FormatFloat(format, float64(n))
|
||||
}
|
||||
25
vendor/github.com/dustin/go-humanize/ordinals.go
generated
vendored
Normal file
25
vendor/github.com/dustin/go-humanize/ordinals.go
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
package humanize
|
||||
|
||||
import "strconv"
|
||||
|
||||
// Ordinal gives you the input number in a rank/ordinal format.
|
||||
//
|
||||
// Ordinal(3) -> 3rd
|
||||
func Ordinal(x int) string {
|
||||
suffix := "th"
|
||||
switch x % 10 {
|
||||
case 1:
|
||||
if x%100 != 11 {
|
||||
suffix = "st"
|
||||
}
|
||||
case 2:
|
||||
if x%100 != 12 {
|
||||
suffix = "nd"
|
||||
}
|
||||
case 3:
|
||||
if x%100 != 13 {
|
||||
suffix = "rd"
|
||||
}
|
||||
}
|
||||
return strconv.Itoa(x) + suffix
|
||||
}
|
||||
123
vendor/github.com/dustin/go-humanize/si.go
generated
vendored
Normal file
123
vendor/github.com/dustin/go-humanize/si.go
generated
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var siPrefixTable = map[float64]string{
|
||||
-24: "y", // yocto
|
||||
-21: "z", // zepto
|
||||
-18: "a", // atto
|
||||
-15: "f", // femto
|
||||
-12: "p", // pico
|
||||
-9: "n", // nano
|
||||
-6: "µ", // micro
|
||||
-3: "m", // milli
|
||||
0: "",
|
||||
3: "k", // kilo
|
||||
6: "M", // mega
|
||||
9: "G", // giga
|
||||
12: "T", // tera
|
||||
15: "P", // peta
|
||||
18: "E", // exa
|
||||
21: "Z", // zetta
|
||||
24: "Y", // yotta
|
||||
}
|
||||
|
||||
var revSIPrefixTable = revfmap(siPrefixTable)
|
||||
|
||||
// revfmap reverses the map and precomputes the power multiplier
|
||||
func revfmap(in map[float64]string) map[string]float64 {
|
||||
rv := map[string]float64{}
|
||||
for k, v := range in {
|
||||
rv[v] = math.Pow(10, k)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
var riParseRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
ri := `^([\-0-9.]+)\s?([`
|
||||
for _, v := range siPrefixTable {
|
||||
ri += v
|
||||
}
|
||||
ri += `]?)(.*)`
|
||||
|
||||
riParseRegex = regexp.MustCompile(ri)
|
||||
}
|
||||
|
||||
// ComputeSI finds the most appropriate SI prefix for the given number
|
||||
// and returns the prefix along with the value adjusted to be within
|
||||
// that prefix.
|
||||
//
|
||||
// See also: SI, ParseSI.
|
||||
//
|
||||
// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p")
|
||||
func ComputeSI(input float64) (float64, string) {
|
||||
if input == 0 {
|
||||
return 0, ""
|
||||
}
|
||||
mag := math.Abs(input)
|
||||
exponent := math.Floor(logn(mag, 10))
|
||||
exponent = math.Floor(exponent/3) * 3
|
||||
|
||||
value := mag / math.Pow(10, exponent)
|
||||
|
||||
// Handle special case where value is exactly 1000.0
|
||||
// Should return 1 M instead of 1000 k
|
||||
if value == 1000.0 {
|
||||
exponent += 3
|
||||
value = mag / math.Pow(10, exponent)
|
||||
}
|
||||
|
||||
value = math.Copysign(value, input)
|
||||
|
||||
prefix := siPrefixTable[exponent]
|
||||
return value, prefix
|
||||
}
|
||||
|
||||
// SI returns a string with default formatting.
|
||||
//
|
||||
// SI uses Ftoa to format float value, removing trailing zeros.
|
||||
//
|
||||
// See also: ComputeSI, ParseSI.
|
||||
//
|
||||
// e.g. SI(1000000, "B") -> 1 MB
|
||||
// e.g. SI(2.2345e-12, "F") -> 2.2345 pF
|
||||
func SI(input float64, unit string) string {
|
||||
value, prefix := ComputeSI(input)
|
||||
return Ftoa(value) + " " + prefix + unit
|
||||
}
|
||||
|
||||
// SIWithDigits works like SI but limits the resulting string to the
|
||||
// given number of decimal places.
|
||||
//
|
||||
// e.g. SIWithDigits(1000000, 0, "B") -> 1 MB
|
||||
// e.g. SIWithDigits(2.2345e-12, 2, "F") -> 2.23 pF
|
||||
func SIWithDigits(input float64, decimals int, unit string) string {
|
||||
value, prefix := ComputeSI(input)
|
||||
return FtoaWithDigits(value, decimals) + " " + prefix + unit
|
||||
}
|
||||
|
||||
var errInvalid = errors.New("invalid input")
|
||||
|
||||
// ParseSI parses an SI string back into the number and unit.
|
||||
//
|
||||
// See also: SI, ComputeSI.
|
||||
//
|
||||
// e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil)
|
||||
func ParseSI(input string) (float64, string, error) {
|
||||
found := riParseRegex.FindStringSubmatch(input)
|
||||
if len(found) != 4 {
|
||||
return 0, "", errInvalid
|
||||
}
|
||||
mag := revSIPrefixTable[found[2]]
|
||||
unit := found[3]
|
||||
|
||||
base, err := strconv.ParseFloat(found[1], 64)
|
||||
return base * mag, unit, err
|
||||
}
|
||||
117
vendor/github.com/dustin/go-humanize/times.go
generated
vendored
Normal file
117
vendor/github.com/dustin/go-humanize/times.go
generated
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Seconds-based time units
|
||||
const (
|
||||
Day = 24 * time.Hour
|
||||
Week = 7 * Day
|
||||
Month = 30 * Day
|
||||
Year = 12 * Month
|
||||
LongTime = 37 * Year
|
||||
)
|
||||
|
||||
// Time formats a time into a relative string.
|
||||
//
|
||||
// Time(someT) -> "3 weeks ago"
|
||||
func Time(then time.Time) string {
|
||||
return RelTime(then, time.Now(), "ago", "from now")
|
||||
}
|
||||
|
||||
// A RelTimeMagnitude struct contains a relative time point at which
|
||||
// the relative format of time will switch to a new format string. A
|
||||
// slice of these in ascending order by their "D" field is passed to
|
||||
// CustomRelTime to format durations.
|
||||
//
|
||||
// The Format field is a string that may contain a "%s" which will be
|
||||
// replaced with the appropriate signed label (e.g. "ago" or "from
|
||||
// now") and a "%d" that will be replaced by the quantity.
|
||||
//
|
||||
// The DivBy field is the amount of time the time difference must be
|
||||
// divided by in order to display correctly.
|
||||
//
|
||||
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
|
||||
// DivBy should be time.Minute so whatever the duration is will be
|
||||
// expressed in minutes.
|
||||
type RelTimeMagnitude struct {
|
||||
D time.Duration
|
||||
Format string
|
||||
DivBy time.Duration
|
||||
}
|
||||
|
||||
var defaultMagnitudes = []RelTimeMagnitude{
|
||||
{time.Second, "now", time.Second},
|
||||
{2 * time.Second, "1 second %s", 1},
|
||||
{time.Minute, "%d seconds %s", time.Second},
|
||||
{2 * time.Minute, "1 minute %s", 1},
|
||||
{time.Hour, "%d minutes %s", time.Minute},
|
||||
{2 * time.Hour, "1 hour %s", 1},
|
||||
{Day, "%d hours %s", time.Hour},
|
||||
{2 * Day, "1 day %s", 1},
|
||||
{Week, "%d days %s", Day},
|
||||
{2 * Week, "1 week %s", 1},
|
||||
{Month, "%d weeks %s", Week},
|
||||
{2 * Month, "1 month %s", 1},
|
||||
{Year, "%d months %s", Month},
|
||||
{18 * Month, "1 year %s", 1},
|
||||
{2 * Year, "2 years %s", 1},
|
||||
{LongTime, "%d years %s", Year},
|
||||
{math.MaxInt64, "a long while %s", 1},
|
||||
}
|
||||
|
||||
// RelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times and two labels. In addition to the generic time
|
||||
// delta string (e.g. 5 minutes), the labels are used applied so that
|
||||
// the label corresponding to the smaller time is applied.
|
||||
//
|
||||
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
|
||||
func RelTime(a, b time.Time, albl, blbl string) string {
|
||||
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
|
||||
}
|
||||
|
||||
// CustomRelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times two labels and a table of relative time formats.
|
||||
// In addition to the generic time delta string (e.g. 5 minutes), the
|
||||
// labels are used applied so that the label corresponding to the
|
||||
// smaller time is applied.
|
||||
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
|
||||
lbl := albl
|
||||
diff := b.Sub(a)
|
||||
|
||||
if a.After(b) {
|
||||
lbl = blbl
|
||||
diff = a.Sub(b)
|
||||
}
|
||||
|
||||
n := sort.Search(len(magnitudes), func(i int) bool {
|
||||
return magnitudes[i].D > diff
|
||||
})
|
||||
|
||||
if n >= len(magnitudes) {
|
||||
n = len(magnitudes) - 1
|
||||
}
|
||||
mag := magnitudes[n]
|
||||
args := []interface{}{}
|
||||
escaped := false
|
||||
for _, ch := range mag.Format {
|
||||
if escaped {
|
||||
switch ch {
|
||||
case 's':
|
||||
args = append(args, lbl)
|
||||
case 'd':
|
||||
args = append(args, diff/mag.DivBy)
|
||||
}
|
||||
escaped = false
|
||||
} else {
|
||||
escaped = ch == '%'
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(mag.Format, args...)
|
||||
}
|
||||
Reference in New Issue
Block a user