mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-14 11:22:15 +01:00
huge refactoring
This commit is contained in:
206
internal/pkg/conf/conf.go
Normal file
206
internal/pkg/conf/conf.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xassert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
const (
|
||||
maximumWaitTimeoutEnvVar string = "MAXIMUM_WAIT_TIMEOUT"
|
||||
maximumWaitDelayEnvVar string = "MAXIMUM_WAIT_DELAY"
|
||||
maximumWebhookURLTimeoutEnvVar string = "MAXIMUM_WEBHOOK_URL_TIMEOUT"
|
||||
defaultWaitTimeoutEnvVar string = "DEFAULT_WAIT_TIMEOUT"
|
||||
defaultWebhookURLTimeoutEnvVar string = "DEFAULT_WEBHOOK_URL_TIMEOUT"
|
||||
defaultListenPortEnvVar string = "DEFAULT_LISTEN_PORT"
|
||||
disableGoogleChromeEnvVar string = "DISABLE_GOOGLE_CHROME"
|
||||
disableUnoconvEnvVar string = "DISABLE_UNOCONV"
|
||||
logLevelEnvVar string = "LOG_LEVEL"
|
||||
)
|
||||
|
||||
// Config contains the application
|
||||
// configuration.
|
||||
type Config struct {
|
||||
maximumWaitTimeout float64
|
||||
maximumWaitDelay float64
|
||||
maximumWebhookURLTimeout float64
|
||||
defaultWaitTimeout float64
|
||||
defaultWebhookURLTimeout float64
|
||||
defaultListenPort int64
|
||||
disableGoogleChrome bool
|
||||
disableUnoconv bool
|
||||
logLevel xlog.Level
|
||||
}
|
||||
|
||||
func defaultConfig() Config {
|
||||
return Config{
|
||||
maximumWaitTimeout: 30.0,
|
||||
maximumWaitDelay: 10.0,
|
||||
maximumWebhookURLTimeout: 30.0,
|
||||
defaultWaitTimeout: 10.0,
|
||||
defaultWebhookURLTimeout: 10.0,
|
||||
defaultListenPort: 3000,
|
||||
disableGoogleChrome: false,
|
||||
disableUnoconv: false,
|
||||
logLevel: xlog.InfoLevel,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
FromEnv returns a Conf according
|
||||
to environment variables.
|
||||
*/
|
||||
func FromEnv() (Config, error) {
|
||||
const op string = "conf.FromEnv"
|
||||
resolver := func() (Config, error) {
|
||||
c := defaultConfig()
|
||||
maximumWaitTimeout, err := xassert.Float64FromEnv(
|
||||
maximumWaitTimeoutEnvVar,
|
||||
c.maximumWaitTimeout,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
c.maximumWaitTimeout = maximumWaitTimeout
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
maximumWaitDelay, err := xassert.Float64FromEnv(
|
||||
maximumWaitDelayEnvVar,
|
||||
c.maximumWaitDelay,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
c.maximumWaitDelay = maximumWaitDelay
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
maximumWebhookURLTimeout, err := xassert.Float64FromEnv(
|
||||
maximumWebhookURLTimeoutEnvVar,
|
||||
c.maximumWebhookURLTimeout,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
)
|
||||
c.maximumWebhookURLTimeout = maximumWebhookURLTimeout
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
defaultWaitTimeout, err := xassert.Float64FromEnv(
|
||||
defaultWaitTimeoutEnvVar,
|
||||
c.defaultWaitTimeout,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
xassert.Float64NotSuperiorTo(c.maximumWaitTimeout),
|
||||
)
|
||||
c.defaultWaitTimeout = defaultWaitTimeout
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
defaultWebhookURLTimeout, err := xassert.Float64FromEnv(
|
||||
defaultWebhookURLTimeoutEnvVar,
|
||||
c.defaultWebhookURLTimeout,
|
||||
xassert.Float64NotInferiorTo(0.0),
|
||||
xassert.Float64NotSuperiorTo(c.defaultWebhookURLTimeout),
|
||||
)
|
||||
c.defaultWebhookURLTimeout = defaultWebhookURLTimeout
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
defaultListenPort, err := xassert.Int64FromEnv(
|
||||
defaultListenPortEnvVar,
|
||||
c.defaultListenPort,
|
||||
xassert.Int64NotInferiorTo(0),
|
||||
xassert.Int64NotSuperiorTo(65535),
|
||||
)
|
||||
c.defaultListenPort = defaultListenPort
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
disableGoogleChrome, err := xassert.BoolFromEnv(
|
||||
disableGoogleChromeEnvVar,
|
||||
c.disableGoogleChrome,
|
||||
)
|
||||
c.disableGoogleChrome = disableGoogleChrome
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
disableUnoconv, err := xassert.BoolFromEnv(
|
||||
disableUnoconvEnvVar,
|
||||
c.disableUnoconv,
|
||||
)
|
||||
c.disableUnoconv = disableUnoconv
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
logLevel, err := xassert.StringFromEnv(
|
||||
logLevelEnvVar,
|
||||
string(c.logLevel),
|
||||
xassert.StringOneOf(xlog.Levels()),
|
||||
)
|
||||
c.logLevel = xlog.MustParseLevel(logLevel)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
result, err := resolver()
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MaximumWaitTimeout returns the maximum
|
||||
// wait timeout from the configuration.
|
||||
func (c Config) MaximumWaitTimeout() float64 {
|
||||
return c.maximumWaitTimeout
|
||||
}
|
||||
|
||||
// MaximumWaitDelay returns the maximum
|
||||
// wait timeout from the configuration.
|
||||
func (c Config) MaximumWaitDelay() float64 {
|
||||
return c.maximumWaitDelay
|
||||
}
|
||||
|
||||
// MaximumWebhookURLTimeout returns the maximum
|
||||
// webhook URL wait timeout from the configuration.
|
||||
func (c Config) MaximumWebhookURLTimeout() float64 {
|
||||
return c.maximumWebhookURLTimeout
|
||||
}
|
||||
|
||||
// DefaultWaitTimeout returns the default
|
||||
// wait timeout from the configuration.
|
||||
func (c Config) DefaultWaitTimeout() float64 {
|
||||
return c.defaultWaitTimeout
|
||||
}
|
||||
|
||||
// DefaultWebhookURLTimeout returns the default
|
||||
// webhook URL wait timeout from the configuration.
|
||||
func (c Config) DefaultWebhookURLTimeout() float64 {
|
||||
return c.defaultWebhookURLTimeout
|
||||
}
|
||||
|
||||
// DefaultListenPort returns the default
|
||||
// listen port from the configuration.
|
||||
func (c Config) DefaultListenPort() int64 {
|
||||
return c.defaultListenPort
|
||||
}
|
||||
|
||||
/*
|
||||
DisableGoogleChrome returns true if
|
||||
Google Chrome is disabled in the
|
||||
configuration.
|
||||
*/
|
||||
func (c Config) DisableGoogleChrome() bool {
|
||||
return c.disableGoogleChrome
|
||||
}
|
||||
|
||||
/*
|
||||
DisableUnoconv returns true if
|
||||
Unoconv is disabled in the
|
||||
configuration.
|
||||
*/
|
||||
func (c Config) DisableUnoconv() bool {
|
||||
return c.disableUnoconv
|
||||
}
|
||||
|
||||
// LogLevel returns the xlog.Level from
|
||||
// the configuration.
|
||||
func (c Config) LogLevel() xlog.Level {
|
||||
return c.logLevel
|
||||
}
|
||||
334
internal/pkg/conf/conf_test.go
Normal file
334
internal/pkg/conf/conf_test.go
Normal file
@@ -0,0 +1,334 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||
)
|
||||
|
||||
func TestEmptyFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// no environment variables set,
|
||||
// values should be equal to default config.
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestMaximumWaitTimeoutFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// MAXIMUM_WAIT_TIMEOUT correctly set.
|
||||
os.Setenv(maximumWaitTimeoutEnvVar, "10.0")
|
||||
expected = defaultConfig()
|
||||
expected.maximumWaitTimeout = 10.0
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(maximumWaitTimeoutEnvVar)
|
||||
// MAXIMUM_WAIT_TIMEOUT wrongly set.
|
||||
os.Setenv(maximumWaitTimeoutEnvVar, "foo")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(maximumWaitTimeoutEnvVar)
|
||||
// MAXIMUM_WAIT_TIMEOUT < 0.
|
||||
os.Setenv(maximumWaitTimeoutEnvVar, "-1.0")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(maximumWaitTimeoutEnvVar)
|
||||
}
|
||||
|
||||
func TestMaximumWaitDelayFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// MAXIMUM_WAIT_DELAY correctly set.
|
||||
os.Setenv(maximumWaitDelayEnvVar, "10.0")
|
||||
expected = defaultConfig()
|
||||
expected.maximumWaitDelay = 10.0
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(maximumWaitDelayEnvVar)
|
||||
// MAXIMUM_WAIT_DELAY wrongly set.
|
||||
os.Setenv(maximumWaitDelayEnvVar, "foo")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(maximumWaitDelayEnvVar)
|
||||
// MAXIMUM_WAIT_DELAY < 0.
|
||||
os.Setenv(maximumWaitDelayEnvVar, "-1.0")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(maximumWaitDelayEnvVar)
|
||||
}
|
||||
|
||||
func TestMaximumWebhookURLTimeoutFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// MAXIMUM_WEBHOOK_URL_TIMEOUT correctly set.
|
||||
os.Setenv(maximumWebhookURLTimeoutEnvVar, "10.0")
|
||||
expected = defaultConfig()
|
||||
expected.maximumWebhookURLTimeout = 10.0
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(maximumWebhookURLTimeoutEnvVar)
|
||||
// MAXIMUM_WEBHOOK_URL_TIMEOUT wrongly set.
|
||||
os.Setenv(maximumWebhookURLTimeoutEnvVar, "foo")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(maximumWebhookURLTimeoutEnvVar)
|
||||
// MAXIMUM_WEBHOOK_URL_TIMEOUT < 0.
|
||||
os.Setenv(maximumWebhookURLTimeoutEnvVar, "-1.0")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(maximumWebhookURLTimeoutEnvVar)
|
||||
}
|
||||
|
||||
func TestDefaultWaitTimeoutFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// DEFAULT_WAIT_TIMEOUT correctly set.
|
||||
os.Setenv(defaultWaitTimeoutEnvVar, "10.0")
|
||||
expected = defaultConfig()
|
||||
expected.defaultWaitTimeout = 10.0
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(defaultWaitTimeoutEnvVar)
|
||||
// DEFAULT_WAIT_TIMEOUT wrongly set.
|
||||
os.Setenv(defaultWaitTimeoutEnvVar, "foo")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(defaultWaitTimeoutEnvVar)
|
||||
// DEFAULT_WAIT_TIMEOUT < 0.
|
||||
os.Setenv(defaultWaitTimeoutEnvVar, "-1.0")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(defaultWaitTimeoutEnvVar)
|
||||
// DEFAULT_WAIT_TIMEOUT > MAXIMUM_WAIT_TIMEOUT.
|
||||
os.Setenv(defaultWaitTimeoutEnvVar, "40.0")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(defaultWaitTimeoutEnvVar)
|
||||
}
|
||||
|
||||
func TestDefaultWebhookURLTimeoutFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// DEFAULT_WEBHOOK_URL_TIMEOUT correctly set.
|
||||
os.Setenv(defaultWebhookURLTimeoutEnvVar, "10.0")
|
||||
expected = defaultConfig()
|
||||
expected.defaultWebhookURLTimeout = 10.0
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(defaultWebhookURLTimeoutEnvVar)
|
||||
// DEFAULT_WEBHOOK_URL_TIMEOUT wrongly set.
|
||||
os.Setenv(defaultWebhookURLTimeoutEnvVar, "foo")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(defaultWebhookURLTimeoutEnvVar)
|
||||
// DEFAULT_WEBHOOK_URL_TIMEOUT < 0.
|
||||
os.Setenv(defaultWebhookURLTimeoutEnvVar, "-1.0")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(defaultWebhookURLTimeoutEnvVar)
|
||||
// DEFAULT_WEBHOOK_URL_TIMEOUT > MAXIMUM_WEBHOOK_URL_TIMEOUT.
|
||||
os.Setenv(defaultWebhookURLTimeoutEnvVar, "40.0")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(defaultWebhookURLTimeoutEnvVar)
|
||||
}
|
||||
|
||||
func TestDefaultListenPortFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// DEFAULT_LISTEN_PORT correctly set.
|
||||
os.Setenv(defaultListenPortEnvVar, "80")
|
||||
expected = defaultConfig()
|
||||
expected.defaultListenPort = 80
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(defaultListenPortEnvVar)
|
||||
// DEFAULT_LISTEN_PORT wrongly set.
|
||||
os.Setenv(defaultListenPortEnvVar, "foo")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(defaultListenPortEnvVar)
|
||||
// DEFAULT_LISTEN_PORT < 0.
|
||||
os.Setenv(defaultListenPortEnvVar, "-1.0")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(defaultListenPortEnvVar)
|
||||
// DEFAULT_LISTEN_PORT > 65535.
|
||||
os.Setenv(defaultListenPortEnvVar, "65536")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(defaultListenPortEnvVar)
|
||||
}
|
||||
|
||||
func TestDisableGoogleChromeFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// DISABLE_GOOGLE_CHROME correctly set.
|
||||
os.Setenv(disableGoogleChromeEnvVar, "1")
|
||||
expected = defaultConfig()
|
||||
expected.disableGoogleChrome = true
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(disableGoogleChromeEnvVar)
|
||||
os.Setenv(disableGoogleChromeEnvVar, "0")
|
||||
expected = defaultConfig()
|
||||
expected.disableGoogleChrome = false
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(disableGoogleChromeEnvVar)
|
||||
// DISABLE_GOOGLE_CHROME wrongly set.
|
||||
os.Setenv(disableGoogleChromeEnvVar, "foo")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(disableGoogleChromeEnvVar)
|
||||
}
|
||||
|
||||
func TestDisableUnoconvFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// DISABLE_UNOCONV correctly set.
|
||||
os.Setenv(disableUnoconvEnvVar, "1")
|
||||
expected = defaultConfig()
|
||||
expected.disableUnoconv = true
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(disableUnoconvEnvVar)
|
||||
os.Setenv(disableUnoconvEnvVar, "0")
|
||||
expected = defaultConfig()
|
||||
expected.disableUnoconv = false
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(disableUnoconvEnvVar)
|
||||
// DISABLE_UNOCONV wrongly set.
|
||||
os.Setenv(disableUnoconvEnvVar, "foo")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(disableUnoconvEnvVar)
|
||||
}
|
||||
|
||||
func TestLogLevelFromEnv(t *testing.T) {
|
||||
var (
|
||||
expected Config
|
||||
result Config
|
||||
err error
|
||||
)
|
||||
// LOG_LEVEL correctly set.
|
||||
os.Setenv(logLevelEnvVar, "DEBUG")
|
||||
expected = defaultConfig()
|
||||
expected.logLevel = xlog.DebugLevel
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(logLevelEnvVar)
|
||||
os.Setenv(logLevelEnvVar, "INFO")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(logLevelEnvVar)
|
||||
os.Setenv(logLevelEnvVar, "ERROR")
|
||||
expected = defaultConfig()
|
||||
expected.logLevel = xlog.ErrorLevel
|
||||
result, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(logLevelEnvVar)
|
||||
// LOG_LEVEL wrongly set.
|
||||
os.Setenv(logLevelEnvVar, "foo")
|
||||
expected = defaultConfig()
|
||||
result, err = FromEnv()
|
||||
xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, expected, result)
|
||||
os.Unsetenv(logLevelEnvVar)
|
||||
}
|
||||
|
||||
func TestGetters(t *testing.T) {
|
||||
result := defaultConfig()
|
||||
assert.Equal(t, result.maximumWaitTimeout, result.MaximumWaitTimeout())
|
||||
assert.Equal(t, result.maximumWaitDelay, result.MaximumWaitDelay())
|
||||
assert.Equal(t, result.maximumWebhookURLTimeout, result.MaximumWebhookURLTimeout())
|
||||
assert.Equal(t, result.defaultWaitTimeout, result.DefaultWaitTimeout())
|
||||
assert.Equal(t, result.defaultWebhookURLTimeout, result.DefaultWebhookURLTimeout())
|
||||
assert.Equal(t, result.defaultListenPort, result.DefaultListenPort())
|
||||
assert.Equal(t, result.disableGoogleChrome, result.DisableGoogleChrome())
|
||||
assert.Equal(t, result.disableUnoconv, result.DisableUnoconv())
|
||||
assert.Equal(t, result.logLevel, result.LogLevel())
|
||||
}
|
||||
3
internal/pkg/conf/doc.go
Normal file
3
internal/pkg/conf/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package conf gathers all
|
||||
// configuration data.
|
||||
package conf
|
||||
@@ -1,178 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultWaitTimeoutEnvVar string = "DEFAULT_WAIT_TIMEOUT"
|
||||
defaultListenPortEnvVar string = "DEFAULT_LISTEN_PORT"
|
||||
disableGoogleChromeEnvVar string = "DISABLE_GOOGLE_CHROME"
|
||||
disableUnoconvEnvVar string = "DISABLE_UNOCONV"
|
||||
logLevelEnvVar string = "LOG_LEVEL"
|
||||
)
|
||||
|
||||
// Config contains the application
|
||||
// configuration.
|
||||
type Config struct {
|
||||
defaultWaitTimeout float64
|
||||
defaultListenPort string
|
||||
enableChromeEndpoints bool
|
||||
enableUnoconvEndpoints bool
|
||||
logLevel logrus.Level
|
||||
}
|
||||
|
||||
func defaultConfig() *Config {
|
||||
return &Config{
|
||||
defaultWaitTimeout: 10,
|
||||
defaultListenPort: "3000",
|
||||
enableChromeEndpoints: true,
|
||||
enableUnoconvEndpoints: true,
|
||||
logLevel: logrus.InfoLevel,
|
||||
}
|
||||
}
|
||||
|
||||
// FromEnv fetches configuration
|
||||
// from environment variables.
|
||||
func FromEnv() (*Config, error) {
|
||||
const op string = "config.FromEnv"
|
||||
c := defaultConfig()
|
||||
defaultWaitTimeout, err := defaultWaitTimeoutFromEnv(defaultWaitTimeoutEnvVar, c.DefaultWaitTimeout())
|
||||
c.defaultWaitTimeout = defaultWaitTimeout
|
||||
if err != nil {
|
||||
return c, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
defaultListenPort, err := defaultListenPortFromEnv(defaultListenPortEnvVar, c.DefaultListenPort())
|
||||
c.defaultListenPort = defaultListenPort
|
||||
if err != nil {
|
||||
return c, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
disableChromeEndpoints, err := boolFromEnv(disableGoogleChromeEnvVar, !c.EnableChromeEndpoints())
|
||||
c.enableChromeEndpoints = !disableChromeEndpoints
|
||||
if err != nil {
|
||||
return c, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
disableUnoconvEndpoints, err := boolFromEnv(disableUnoconvEnvVar, !c.EnableUnoconvEndpoints())
|
||||
c.enableUnoconvEndpoints = !disableUnoconvEndpoints
|
||||
if err != nil {
|
||||
return c, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
logLevel, err := logLevelFromEnv(logLevelEnvVar, c.LogLevel())
|
||||
c.logLevel = logLevel
|
||||
if err != nil {
|
||||
return c, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// DefaultWaitTimeout returns the default
|
||||
// wait timeout from the configuration.
|
||||
func (c *Config) DefaultWaitTimeout() float64 {
|
||||
return c.defaultWaitTimeout
|
||||
}
|
||||
|
||||
// DefaultListenPort returns the default
|
||||
// listen port from the configuration.
|
||||
func (c *Config) DefaultListenPort() string {
|
||||
return c.defaultListenPort
|
||||
}
|
||||
|
||||
// EnableChromeEndpoints returns true if
|
||||
// Chrome endpoints are enabled in the
|
||||
// configuration.
|
||||
func (c *Config) EnableChromeEndpoints() bool {
|
||||
return c.enableChromeEndpoints
|
||||
}
|
||||
|
||||
// EnableUnoconvEndpoints returns true if
|
||||
// Unoconv endpoints are enabled in the
|
||||
// configuration.
|
||||
func (c *Config) EnableUnoconvEndpoints() bool {
|
||||
return c.enableUnoconvEndpoints
|
||||
}
|
||||
|
||||
// LogLevel returns the logrus.Level from
|
||||
// the configuration.
|
||||
func (c *Config) LogLevel() logrus.Level {
|
||||
return c.logLevel
|
||||
}
|
||||
|
||||
func defaultWaitTimeoutFromEnv(envVar string, defaultValue float64) (float64, error) {
|
||||
const op string = "config.defaultWaitTimeoutFromEnv"
|
||||
if v, ok := os.LookupEnv(envVar); ok {
|
||||
waitTimeout, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return defaultValue, &standarderror.Error{
|
||||
Code: standarderror.Invalid,
|
||||
Message: fmt.Sprintf("'%s' is not a float, got '%s'", envVar, v),
|
||||
Op: op,
|
||||
}
|
||||
}
|
||||
return waitTimeout, nil
|
||||
}
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func defaultListenPortFromEnv(envVar string, defaultValue string) (string, error) {
|
||||
const op string = "config.defaultListenPortFromEnv"
|
||||
if v, ok := os.LookupEnv(envVar); ok {
|
||||
portAsUint, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
return defaultValue, &standarderror.Error{
|
||||
Code: standarderror.Invalid,
|
||||
Message: fmt.Sprintf("'%s' is not a uint, got '%s'", envVar, v),
|
||||
Op: op,
|
||||
}
|
||||
}
|
||||
if portAsUint > 65535 {
|
||||
return defaultValue, &standarderror.Error{
|
||||
Code: standarderror.Invalid,
|
||||
Message: fmt.Sprintf("'%s' is not a uint < 65535, got '%d'", envVar, portAsUint),
|
||||
Op: op,
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func boolFromEnv(envVar string, defaultValue bool) (bool, error) {
|
||||
const op string = "config.boolFromEnv"
|
||||
if v, ok := os.LookupEnv(envVar); ok {
|
||||
if v != "1" && v != "0" {
|
||||
return defaultValue, &standarderror.Error{
|
||||
Code: standarderror.Invalid,
|
||||
Message: fmt.Sprintf("'%s' is not '0' or '1', got %s", envVar, v),
|
||||
Op: op,
|
||||
}
|
||||
}
|
||||
return v == "1", nil
|
||||
}
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func logLevelFromEnv(envVar string, defaultValue logrus.Level) (logrus.Level, error) {
|
||||
const op string = "config.logLevelFromEnv"
|
||||
if v, ok := os.LookupEnv(envVar); ok {
|
||||
switch v {
|
||||
case "DEBUG":
|
||||
return logrus.DebugLevel, nil
|
||||
case "INFO":
|
||||
return logrus.InfoLevel, nil
|
||||
case "ERROR":
|
||||
return logrus.ErrorLevel, nil
|
||||
default:
|
||||
return defaultValue, &standarderror.Error{
|
||||
Code: standarderror.Invalid,
|
||||
Message: fmt.Sprintf("'%s' is not 'DEBUG', 'INFO' or 'ERROR', got '%s'", envVar, v),
|
||||
Op: op,
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue, nil
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestDefaultWaitTimeout(t *testing.T) {
|
||||
// should be OK.
|
||||
config, err := FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 10.0, config.DefaultWaitTimeout())
|
||||
os.Setenv(defaultWaitTimeoutEnvVar, "1.5")
|
||||
config, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1.5, config.DefaultWaitTimeout())
|
||||
// should failed.
|
||||
os.Setenv(defaultWaitTimeoutEnvVar, "foo")
|
||||
_, err = FromEnv()
|
||||
assert.NotNil(t, err)
|
||||
standardized := test.RequireStandardError(t, err)
|
||||
assert.Equal(t, standarderror.Invalid, standarderror.Code(standardized))
|
||||
os.Unsetenv(defaultWaitTimeoutEnvVar)
|
||||
}
|
||||
|
||||
func TestDefaultListenPort(t *testing.T) {
|
||||
// should be OK.
|
||||
config, err := FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "3000", config.DefaultListenPort())
|
||||
os.Setenv(defaultListenPortEnvVar, "4000")
|
||||
config, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "4000", config.DefaultListenPort())
|
||||
// should failed.
|
||||
os.Setenv(defaultListenPortEnvVar, "foo")
|
||||
_, err = FromEnv()
|
||||
assert.NotNil(t, err)
|
||||
standardized := test.RequireStandardError(t, err)
|
||||
assert.Equal(t, standarderror.Invalid, standarderror.Code(standardized))
|
||||
os.Setenv(defaultListenPortEnvVar, "100000000")
|
||||
_, err = FromEnv()
|
||||
assert.NotNil(t, err)
|
||||
standardized = test.RequireStandardError(t, err)
|
||||
assert.Equal(t, standarderror.Invalid, standarderror.Code(standardized))
|
||||
os.Unsetenv(defaultListenPortEnvVar)
|
||||
}
|
||||
|
||||
func TestEnableChromeEndpoints(t *testing.T) {
|
||||
// should be OK.
|
||||
config, err := FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, true, config.EnableChromeEndpoints())
|
||||
os.Setenv(disableGoogleChromeEnvVar, "1")
|
||||
config, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, false, config.EnableChromeEndpoints())
|
||||
os.Setenv(disableGoogleChromeEnvVar, "0")
|
||||
config, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, true, config.EnableChromeEndpoints())
|
||||
// should failed.
|
||||
os.Setenv(disableGoogleChromeEnvVar, "true")
|
||||
_, err = FromEnv()
|
||||
assert.NotNil(t, err)
|
||||
standardized := test.RequireStandardError(t, err)
|
||||
assert.Equal(t, standarderror.Invalid, standarderror.Code(standardized))
|
||||
os.Unsetenv(disableGoogleChromeEnvVar)
|
||||
}
|
||||
|
||||
func TestEnableUnoconvEndpoints(t *testing.T) {
|
||||
// should be OK.
|
||||
config, err := FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, true, config.EnableUnoconvEndpoints())
|
||||
os.Setenv(disableUnoconvEnvVar, "1")
|
||||
config, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, false, config.EnableUnoconvEndpoints())
|
||||
os.Setenv(disableUnoconvEnvVar, "0")
|
||||
config, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, true, config.EnableUnoconvEndpoints())
|
||||
// should failed.
|
||||
os.Setenv(disableUnoconvEnvVar, "true")
|
||||
_, err = FromEnv()
|
||||
assert.NotNil(t, err)
|
||||
standardized := test.RequireStandardError(t, err)
|
||||
assert.Equal(t, standarderror.Invalid, standarderror.Code(standardized))
|
||||
os.Unsetenv(disableUnoconvEnvVar)
|
||||
}
|
||||
|
||||
func TestLogLevel(t *testing.T) {
|
||||
// should be OK.
|
||||
config, err := FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, logrus.InfoLevel, config.LogLevel())
|
||||
os.Setenv(logLevelEnvVar, "DEBUG")
|
||||
config, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, logrus.DebugLevel, config.LogLevel())
|
||||
os.Setenv(logLevelEnvVar, "INFO")
|
||||
config, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, logrus.InfoLevel, config.LogLevel())
|
||||
os.Setenv(logLevelEnvVar, "ERROR")
|
||||
config, err = FromEnv()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, logrus.ErrorLevel, config.LogLevel())
|
||||
// should failed.
|
||||
os.Setenv(logLevelEnvVar, "foo")
|
||||
config, err = FromEnv()
|
||||
assert.Equal(t, logrus.InfoLevel, config.LogLevel())
|
||||
assert.NotNil(t, err)
|
||||
standardized := test.RequireStandardError(t, err)
|
||||
assert.Equal(t, standarderror.Invalid, standarderror.Code(standardized))
|
||||
os.Unsetenv(logLevelEnvVar)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package config gathers all
|
||||
// configuration data.
|
||||
package config
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package logger defines a standard
|
||||
// logger for the application.
|
||||
package logger
|
||||
@@ -1,63 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Logger enforces specific log message formats.
|
||||
type Logger struct {
|
||||
entry *logrus.Entry
|
||||
}
|
||||
|
||||
// New initializes the logger.
|
||||
func New(level logrus.Level, trace string) *Logger {
|
||||
l := logrus.New()
|
||||
l.SetLevel(level)
|
||||
if !isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
l.SetFormatter(&logrus.JSONFormatter{})
|
||||
}
|
||||
return &Logger{
|
||||
entry: l.WithField("trace", trace),
|
||||
}
|
||||
}
|
||||
|
||||
// WithFields creates a new logger with
|
||||
// given fields.
|
||||
func (l *Logger) WithFields(fields map[string]interface{}) *Logger {
|
||||
return &Logger{
|
||||
entry: l.entry.WithFields(fields),
|
||||
}
|
||||
}
|
||||
|
||||
// DebugfOp logs a debug message for given
|
||||
// logical operation.
|
||||
func (l *Logger) DebugfOp(op string, format string, args ...interface{}) {
|
||||
l.entry.WithField("op", op).Debugf(format, args...)
|
||||
}
|
||||
|
||||
// InfofOp logs an info message for given
|
||||
// logical operation.
|
||||
func (l *Logger) InfofOp(op string, format string, args ...interface{}) {
|
||||
l.entry.WithField("op", op).Infof(format, args...)
|
||||
}
|
||||
|
||||
// ErrorOp logs an error for given
|
||||
// logical operation.
|
||||
func (l *Logger) ErrorOp(op string, err error) {
|
||||
l.entry.WithField("op", op).Error(err.Error())
|
||||
}
|
||||
|
||||
// ErrorfOp logs an error message for given
|
||||
// logical operation.
|
||||
func (l *Logger) ErrorfOp(op string, message string) {
|
||||
l.entry.WithField("op", op).Error(message)
|
||||
}
|
||||
|
||||
// FatalOp logs an error message for given
|
||||
// logical operation.
|
||||
func (l *Logger) FatalOp(op string, err error) {
|
||||
l.entry.WithField("op", op).Error(err.Error())
|
||||
}
|
||||
@@ -5,45 +5,72 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mafredri/cdp/devtool"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
const chromeWarmupTime time.Duration = 10 * time.Second
|
||||
|
||||
type chrome struct {
|
||||
manager *processManager
|
||||
type chromeProcess struct {
|
||||
logger xlog.Logger
|
||||
}
|
||||
|
||||
// NewChrome returns a Google Chrome
|
||||
// NewChromeProcess returns a Google Chrome
|
||||
// headless process.
|
||||
func NewChrome(logger *logger.Logger) Process {
|
||||
return &chrome{
|
||||
manager: &processManager{logger: logger},
|
||||
func NewChromeProcess(logger xlog.Logger) Process {
|
||||
return chromeProcess{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *chrome) Fullname() string {
|
||||
func (p chromeProcess) Fullname() string {
|
||||
return "Google Chrome headless"
|
||||
}
|
||||
|
||||
func (p *chrome) Start() error {
|
||||
const op string = "pm2.chrome.Start"
|
||||
if err := p.manager.start(p); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
func (p chromeProcess) Start() error {
|
||||
const op string = "pm2.chromeProcess.Start"
|
||||
if err := start(p.logger, p); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *chrome) Shutdown() error {
|
||||
const op string = "pm2.chrome.Shutdown"
|
||||
if err := p.manager.shutdown(p); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
func (p chromeProcess) IsViable() bool {
|
||||
const op string = "pm2.chromeProcess.IsViable"
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
p.logger.DebugfOp(
|
||||
op,
|
||||
"checking '%s' viability via endpoint '%s'",
|
||||
p.Fullname(),
|
||||
"http://localhost:9222/json/version",
|
||||
)
|
||||
v, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||
if err != nil {
|
||||
p.logger.ErrorfOp(
|
||||
op,
|
||||
"'%s' is not viable as endpoint returned '%v'",
|
||||
p.Fullname(),
|
||||
err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
p.logger.DebugfOp(
|
||||
op,
|
||||
"'%s' is viable as endpoint returned '%v'",
|
||||
p.Fullname(),
|
||||
v,
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
func (p chromeProcess) Stop() error {
|
||||
const op string = "pm2.chromeProcess.Stop"
|
||||
if err := stop(p.logger, p); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *chrome) args() []string {
|
||||
func (p chromeProcess) args() []string {
|
||||
return []string{
|
||||
"--no-sandbox",
|
||||
"--headless",
|
||||
@@ -62,47 +89,25 @@ func (p *chrome) args() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *chrome) name() string {
|
||||
func (p chromeProcess) binary() string {
|
||||
return "google-chrome-stable"
|
||||
}
|
||||
|
||||
func (p *chrome) viable() bool {
|
||||
const op string = "pm2.chrome.viable"
|
||||
// check if Google Chrome is correctly running.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
p.manager.logger.DebugfOp(
|
||||
op,
|
||||
"checking liveness via debug version endpoint http://localhost:9222/json/version",
|
||||
func (p chromeProcess) warmup() {
|
||||
const (
|
||||
op string = "pm2.chromeProcess.warmup"
|
||||
warmupTime time.Duration = 10 * time.Second
|
||||
)
|
||||
v, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||
if err != nil {
|
||||
p.manager.logger.DebugfOp(
|
||||
op,
|
||||
"debug version endpoint returned error: %v",
|
||||
err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
p.manager.logger.DebugfOp(
|
||||
p.logger.DebugfOp(
|
||||
op,
|
||||
"debug version endpoint returned version info: %+v",
|
||||
*v,
|
||||
"waiting '%v' for allowing '%s' to warmup",
|
||||
warmupTime,
|
||||
p.Fullname(),
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *chrome) warmup() {
|
||||
const op string = "pm2.chrome.warmup"
|
||||
p.manager.logger.DebugfOp(
|
||||
op,
|
||||
"allowing %v to startup",
|
||||
chromeWarmupTime,
|
||||
)
|
||||
time.Sleep(chromeWarmupTime)
|
||||
time.Sleep(warmupTime)
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = Process(new(chrome))
|
||||
_ = Process(new(chromeProcess))
|
||||
)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package pm2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestChromeStart(t *testing.T) {
|
||||
p := NewChrome(test.CreateTestLogger())
|
||||
err := p.Start()
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestChromeShutdown(t *testing.T) {
|
||||
p := NewChrome(test.CreateTestLogger())
|
||||
err := p.Shutdown()
|
||||
require.Nil(t, err)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Package pm2 facilitates starting external
|
||||
processes on which our API depends.
|
||||
processes on which our application depends.
|
||||
|
||||
For instance, it may start Google Chrome headless and
|
||||
unoconv listener with PM2.
|
||||
|
||||
@@ -1,124 +1,102 @@
|
||||
package pm2
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
)
|
||||
|
||||
const (
|
||||
stoppedState int32 = iota
|
||||
runningState
|
||||
errorState
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
// Process is a type that can start or
|
||||
// shutdown a process with PM2.
|
||||
// stop a process with PM2.
|
||||
type Process interface {
|
||||
Fullname() string
|
||||
Start() error
|
||||
Shutdown() error
|
||||
IsViable() bool
|
||||
Stop() error
|
||||
args() []string
|
||||
name() string
|
||||
viable() bool
|
||||
binary() string
|
||||
warmup()
|
||||
}
|
||||
|
||||
type processManager struct {
|
||||
heuristicState int32
|
||||
logger *logger.Logger
|
||||
}
|
||||
type pm2Command string
|
||||
|
||||
func (m *processManager) start(p Process) error {
|
||||
const op string = "pm2.start"
|
||||
if err := m.pm2(p, "start"); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
p.warmup()
|
||||
if !p.viable() {
|
||||
attempts := 0
|
||||
for attempts < 5 && !p.viable() {
|
||||
if err := m.pm2(p, "restart"); err != nil {
|
||||
m.heuristicState = errorState
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
p.warmup()
|
||||
attempts++
|
||||
const (
|
||||
startCommand pm2Command = "start"
|
||||
restartCommand pm2Command = "restart"
|
||||
stopCommand pm2Command = "stop"
|
||||
logsCommand pm2Command = "logs"
|
||||
)
|
||||
|
||||
func start(logger xlog.Logger, process Process) error {
|
||||
const (
|
||||
op string = "pm2.start"
|
||||
maximumAttempts int = 3
|
||||
)
|
||||
resolver := func() error {
|
||||
// first, we try to start the process.
|
||||
if err := run(logger, startCommand, process); err != nil {
|
||||
return err
|
||||
}
|
||||
if !p.viable() {
|
||||
m.heuristicState = errorState
|
||||
return &standarderror.Error{
|
||||
Op: op,
|
||||
Message: fmt.Sprintf("failed to launch %s", p.Fullname()),
|
||||
// we wait the process to be ready.
|
||||
process.warmup()
|
||||
// if the process failed to start correctly,
|
||||
// we have to restart it.
|
||||
if !process.IsViable() {
|
||||
attempts := 0
|
||||
for attempts < maximumAttempts && !process.IsViable() {
|
||||
if err := run(logger, restartCommand, process); err != nil {
|
||||
return err
|
||||
}
|
||||
process.warmup()
|
||||
attempts++
|
||||
}
|
||||
if !process.IsViable() {
|
||||
return fmt.Errorf("failed to start '%s'", process.Fullname())
|
||||
}
|
||||
}
|
||||
}
|
||||
m.heuristicState = runningState
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *processManager) shutdown(p Process) error {
|
||||
const op string = "pm2.shutdown"
|
||||
if m.heuristicState != runningState {
|
||||
// the process is viable, let's log its
|
||||
// output.
|
||||
if err := run(logger, logsCommand, process); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := m.pm2(p, "stop"); err != nil {
|
||||
m.heuristicState = errorState
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
m.heuristicState = stoppedState
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *processManager) pm2(p Process, cmdName string) error {
|
||||
const op string = "pm2.pm2"
|
||||
cmdArgs := []string{
|
||||
cmdName,
|
||||
p.name(),
|
||||
}
|
||||
if cmdName == "start" {
|
||||
cmdArgs = append(cmdArgs, "--interpreter=none", "--")
|
||||
cmdArgs = append(cmdArgs, p.args()...)
|
||||
}
|
||||
cmd := exec.Command(
|
||||
"pm2",
|
||||
cmdArgs...,
|
||||
)
|
||||
m.logger.DebugfOp(op, "executing command: %s", strings.Join(cmd.Args, " "))
|
||||
processStdOut, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
processStdErr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
readFromPipe := func(outputType string, reader io.ReadCloser) {
|
||||
readFromPipeOp := fmt.Sprintf("pm2.%s.%s", p.name(), outputType)
|
||||
r := bufio.NewReader(reader)
|
||||
defer reader.Close() // nolint: errcheck
|
||||
for {
|
||||
line, _, err := r.ReadLine()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
m.logger.ErrorOp(readFromPipeOp, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
if len(line) != 0 {
|
||||
m.logger.DebugfOp(readFromPipeOp, string(line))
|
||||
}
|
||||
}
|
||||
}
|
||||
go readFromPipe("stdout", processStdOut)
|
||||
go readFromPipe("stderr", processStdErr)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
func stop(logger xlog.Logger, process Process) error {
|
||||
const op string = "pm2.stop"
|
||||
if err := run(logger, stopCommand, process); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func run(logger xlog.Logger, pm2Cmd pm2Command, process Process) error {
|
||||
const op string = "pm2.run"
|
||||
resolver := func() error {
|
||||
args := []string{
|
||||
string(pm2Cmd),
|
||||
process.binary(),
|
||||
}
|
||||
if pm2Cmd == startCommand {
|
||||
args = append(args, "--interpreter=none", "--")
|
||||
args = append(args, process.args()...)
|
||||
}
|
||||
cmd, err := xexec.Command(logger, "pm2", args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
xexec.LogBeforeExecute(logger, cmd)
|
||||
return cmd.Start()
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,73 +3,75 @@ package pm2
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/logger"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
const unoconvWarmupTime time.Duration = 5 * time.Second
|
||||
|
||||
type unoconv struct {
|
||||
manager *processManager
|
||||
type unoconvProcess struct {
|
||||
logger xlog.Logger
|
||||
}
|
||||
|
||||
// NewUnoconv returns a unoconv listener
|
||||
// NewUnoconvProcess returns a unoconv listener
|
||||
// process.
|
||||
func NewUnoconv(logger *logger.Logger) Process {
|
||||
return &unoconv{
|
||||
manager: &processManager{logger: logger},
|
||||
func NewUnoconvProcess(logger xlog.Logger) Process {
|
||||
return unoconvProcess{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *unoconv) Fullname() string {
|
||||
func (p unoconvProcess) Fullname() string {
|
||||
return "unoconv listener"
|
||||
}
|
||||
|
||||
func (p *unoconv) Start() error {
|
||||
const op string = "pm2.unoconv.Start"
|
||||
if err := p.manager.start(p); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
func (p unoconvProcess) Start() error {
|
||||
const op string = "pm2.unoconvProcess.Start"
|
||||
if err := start(p.logger, p); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *unoconv) Shutdown() error {
|
||||
const op string = "pm2.unoconv.Shutdown"
|
||||
if err := p.manager.shutdown(p); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *unoconv) args() []string {
|
||||
return []string{
|
||||
"--listener",
|
||||
"--verbose",
|
||||
}
|
||||
}
|
||||
|
||||
func (p *unoconv) name() string {
|
||||
return "unoconv"
|
||||
}
|
||||
|
||||
func (p *unoconv) viable() bool {
|
||||
func (p unoconvProcess) IsViable() bool {
|
||||
// TODO find a way to check if
|
||||
// the unoconv listener
|
||||
// is correctly started?
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *unoconv) warmup() {
|
||||
const op string = "pm2.unoconv.warmup"
|
||||
p.manager.logger.DebugfOp(
|
||||
op,
|
||||
"allowing %v to startup",
|
||||
unoconvWarmupTime,
|
||||
func (p unoconvProcess) Stop() error {
|
||||
const op string = "pm2.unoconvProcess.Stop"
|
||||
if err := stop(p.logger, p); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p unoconvProcess) args() []string {
|
||||
return []string{
|
||||
"--listener",
|
||||
"--verbose",
|
||||
}
|
||||
}
|
||||
|
||||
func (p unoconvProcess) binary() string {
|
||||
return "unoconv"
|
||||
}
|
||||
|
||||
func (p unoconvProcess) warmup() {
|
||||
const (
|
||||
op string = "pm2.unoconvProcess.warmup"
|
||||
warmupTime time.Duration = 3 * time.Second
|
||||
)
|
||||
time.Sleep(unoconvWarmupTime)
|
||||
p.logger.DebugfOp(
|
||||
op,
|
||||
"waiting '%v' for allowing '%s' to warmup",
|
||||
warmupTime,
|
||||
p.Fullname(),
|
||||
)
|
||||
time.Sleep(warmupTime)
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = Process(new(unoconv))
|
||||
_ = Process(new(unoconvProcess))
|
||||
)
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package pm2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestUnoconvStart(t *testing.T) {
|
||||
p := NewUnoconv(test.CreateTestLogger())
|
||||
err := p.Start()
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestUnoconvShutdown(t *testing.T) {
|
||||
p := NewUnoconv(test.CreateTestLogger())
|
||||
err := p.Shutdown()
|
||||
require.Nil(t, err)
|
||||
}
|
||||
@@ -12,19 +12,22 @@ import (
|
||||
"github.com/mafredri/cdp/protocol/page"
|
||||
"github.com/mafredri/cdp/protocol/target"
|
||||
"github.com/mafredri/cdp/rpcc"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/timeout"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type chrome struct {
|
||||
url string
|
||||
opts *ChromeOptions
|
||||
type chromePrinter struct {
|
||||
logger xlog.Logger
|
||||
url string
|
||||
opts ChromePrinterOptions
|
||||
}
|
||||
|
||||
// ChromeOptions helps customizing the
|
||||
// ChromePrinterOptions helps customizing the
|
||||
// Google Chrome printer behaviour.
|
||||
type ChromeOptions struct {
|
||||
type ChromePrinterOptions struct {
|
||||
WaitTimeout float64
|
||||
WaitDelay float64
|
||||
HeaderHTML string
|
||||
@@ -38,26 +41,27 @@ type ChromeOptions struct {
|
||||
Landscape bool
|
||||
}
|
||||
|
||||
func (p *chrome) Print(destination string) error {
|
||||
const op string = "printer.chrome.Print"
|
||||
ctx, cancel := timeout.Context(p.opts.WaitTimeout + p.opts.WaitDelay)
|
||||
func (p chromePrinter) Print(destination string) error {
|
||||
const op string = "printer.chromePrinter.Print"
|
||||
logOptions(p.logger, p.opts)
|
||||
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout+p.opts.WaitDelay)
|
||||
defer cancel()
|
||||
resolver := func() error {
|
||||
devt, err := devtool.New("http://localhost:9222").Version(ctx)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
return err
|
||||
}
|
||||
// connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol.
|
||||
devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
return err
|
||||
}
|
||||
defer devtConn.Close() // nolint: errcheck
|
||||
// create a new CDP Client that uses conn.
|
||||
devtClient := cdp.NewClient(devtConn)
|
||||
newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
return err
|
||||
}
|
||||
// create a new blank target with the new browser context.
|
||||
createTargetArgs := target.
|
||||
@@ -65,13 +69,13 @@ func (p *chrome) Print(destination string) error {
|
||||
SetBrowserContextID(newContextTarget.BrowserContextID)
|
||||
newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
return err
|
||||
}
|
||||
// connect the client to the new target.
|
||||
newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID)
|
||||
newContextConn, err := rpcc.DialContext(ctx, newTargetWsURL)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
return err
|
||||
}
|
||||
defer newContextConn.Close() // nolint: errcheck
|
||||
// create a new CDP Client that uses newContextConn.
|
||||
@@ -86,10 +90,10 @@ func (p *chrome) Print(destination string) error {
|
||||
func() error { return targetClient.Page.Enable(ctx) },
|
||||
func() error { return targetClient.Runtime.Enable(ctx) },
|
||||
); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
return err
|
||||
}
|
||||
if err := p.navigate(ctx, targetClient); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
return err
|
||||
}
|
||||
print, err := targetClient.Page.PrintToPDF(
|
||||
ctx,
|
||||
@@ -107,58 +111,67 @@ func (p *chrome) Print(destination string) error {
|
||||
SetPrintBackground(true),
|
||||
)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return timeout.Err(ctx, err)
|
||||
return xcontext.MustHandleError(
|
||||
ctx,
|
||||
xerror.New(op, err),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *chrome) navigate(ctx context.Context, client *cdp.Client) error {
|
||||
const op string = "printer.chrome.navigate"
|
||||
// make sure Page events are enabled.
|
||||
if err := client.Page.Enable(ctx); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
func (p chromePrinter) navigate(ctx context.Context, client *cdp.Client) error {
|
||||
const op string = "printer.chromePrinter.navigate"
|
||||
resolver := func() error {
|
||||
// make sure Page events are enabled.
|
||||
if err := client.Page.Enable(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
// make sure Network events are enabled.
|
||||
if err := client.Network.Enable(ctx, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
// create all clients for events.
|
||||
domContentEventFired, err := client.Page.DOMContentEventFired(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer domContentEventFired.Close() // nolint: errcheck
|
||||
loadEventFired, err := client.Page.LoadEventFired(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer loadEventFired.Close() // nolint: errcheck
|
||||
loadingFinished, err := client.Network.LoadingFinished(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer loadingFinished.Close() // nolint: errcheck
|
||||
if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := runBatch(
|
||||
// wait for all events.
|
||||
func() error { _, err := domContentEventFired.Recv(); return err },
|
||||
func() error { _, err := loadEventFired.Recv(); return err },
|
||||
func() error { _, err := loadingFinished.Recv(); return err },
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
// wait for a given amount of time (useful for javascript delay).
|
||||
time.Sleep(xtime.Duration(p.opts.WaitDelay))
|
||||
return nil
|
||||
}
|
||||
// make sure Network events are enabled.
|
||||
if err := client.Network.Enable(ctx, nil); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
// create all clients for events.
|
||||
domContentEventFired, err := client.Page.DOMContentEventFired(ctx)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
defer domContentEventFired.Close() // nolint: errcheck
|
||||
loadEventFired, err := client.Page.LoadEventFired(ctx)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
defer loadEventFired.Close() // nolint: errcheck
|
||||
loadingFinished, err := client.Network.LoadingFinished(ctx)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
defer loadingFinished.Close() // nolint: errcheck
|
||||
if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
if err := runBatch(
|
||||
// wait for all events.
|
||||
func() error { _, err := domContentEventFired.Recv(); return err },
|
||||
func() error { _, err := loadEventFired.Recv(); return err },
|
||||
func() error { _, err := loadingFinished.Recv(); return err },
|
||||
); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
// wait for a given amount of time (useful for javascript delay).
|
||||
time.Sleep(timeout.Duration(p.opts.WaitDelay))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -174,5 +187,5 @@ func runBatch(fn ...func() error) error {
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = Printer(new(chrome))
|
||||
_ = Printer(new(chromePrinter))
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/*
|
||||
Package printer contains structs which convert
|
||||
a specific file type to PDF.
|
||||
*/
|
||||
// Package printer helps converting
|
||||
// a specific file type to PDF.
|
||||
package printer
|
||||
|
||||
@@ -2,13 +2,17 @@ package printer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
// NewHTML returns an HTML printer.
|
||||
func NewHTML(fpath string, opts *ChromeOptions) Printer {
|
||||
// NewHTMLPrinter returns a Printer which
|
||||
// is able to convert an HTML file to PDF.
|
||||
func NewHTMLPrinter(logger xlog.Logger, fpath string, opts ChromePrinterOptions) Printer {
|
||||
URL := fmt.Sprintf("file://%s", fpath)
|
||||
return &chrome{
|
||||
url: URL,
|
||||
opts: opts,
|
||||
return chromePrinter{
|
||||
logger: logger,
|
||||
url: URL,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,37 +7,46 @@ import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/labstack/gommon/random"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
|
||||
)
|
||||
|
||||
// NewMarkdown returns a Markdown printer.
|
||||
func NewMarkdown(fpath string, opts *ChromeOptions) (Printer, error) {
|
||||
const op string = "printer.NewMarkdown"
|
||||
tmpl, err := template.
|
||||
New(filepath.Base(fpath)).
|
||||
Funcs(template.FuncMap{"toHTML": markdownToHTML}).
|
||||
ParseFiles(fpath)
|
||||
// NewMarkdownPrinter returns a Printer which
|
||||
// is able to convert Markdown files to PDF.
|
||||
func NewMarkdownPrinter(logger xlog.Logger, fpath string, opts ChromePrinterOptions) (Printer, error) {
|
||||
const op string = "printer.NewMarkdownPrinter"
|
||||
resolver := func() (string, error) {
|
||||
tmpl, err := template.
|
||||
New(filepath.Base(fpath)).
|
||||
Funcs(template.FuncMap{"toHTML": markdownToHTML}).
|
||||
ParseFiles(fpath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dirPath := filepath.Dir(fpath)
|
||||
data := &templateData{DirPath: dirPath}
|
||||
var buffer bytes.Buffer
|
||||
if err := tmpl.Execute(&buffer, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
baseFilename := xrand.Get()
|
||||
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
|
||||
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("file://%s", dst), nil
|
||||
}
|
||||
URL, err := resolver()
|
||||
if err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
return chromePrinter{}, xerror.New(op, err)
|
||||
}
|
||||
dirPath := filepath.Dir(fpath)
|
||||
data := &templateData{DirPath: dirPath}
|
||||
var buffer bytes.Buffer
|
||||
if err := tmpl.Execute(&buffer, data); err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
baseFilename := random.String(32)
|
||||
dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename)
|
||||
if err := ioutil.WriteFile(dst, buffer.Bytes(), 0644); err != nil {
|
||||
return nil, &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
URL := fmt.Sprintf("file://%s", dst)
|
||||
return &chrome{
|
||||
url: URL,
|
||||
opts: opts,
|
||||
return chromePrinter{
|
||||
logger: logger,
|
||||
url: URL,
|
||||
opts: opts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -50,7 +59,7 @@ func markdownToHTML(dirPath, filename string) (template.HTML, error) {
|
||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
||||
b, err := ioutil.ReadFile(fpath)
|
||||
if err != nil {
|
||||
return "", &standarderror.Error{Op: op, Err: err}
|
||||
return "", xerror.New(op, err)
|
||||
}
|
||||
unsafe := blackfriday.Run(b)
|
||||
content := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||
|
||||
@@ -2,57 +2,71 @@ package printer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/timeout"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
type merge struct {
|
||||
type mergePrinter struct {
|
||||
ctx context.Context
|
||||
logger xlog.Logger
|
||||
fpaths []string
|
||||
opts *MergeOptions
|
||||
opts MergePrinterOptions
|
||||
}
|
||||
|
||||
// MergeOptions helps customizing the
|
||||
// merge printer behaviour.
|
||||
type MergeOptions struct {
|
||||
// MergePrinterOptions helps customizing the
|
||||
// merge Printer behaviour.
|
||||
type MergePrinterOptions struct {
|
||||
WaitTimeout float64
|
||||
}
|
||||
|
||||
// NewMerge returns a merge printer.
|
||||
func NewMerge(fpaths []string, opts *MergeOptions) Printer {
|
||||
return &merge{
|
||||
// NewMergePrinter returns a Printer which
|
||||
// is able to merge PDFs.
|
||||
func NewMergePrinter(logger xlog.Logger, fpaths []string, opts MergePrinterOptions) Printer {
|
||||
return mergePrinter{
|
||||
logger: logger,
|
||||
fpaths: fpaths,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *merge) Print(destination string) error {
|
||||
const op string = "printer.merge.Print"
|
||||
func (p mergePrinter) Print(destination string) error {
|
||||
const op string = "printer.mergePrinter.Print"
|
||||
logOptions(p.logger, p.opts)
|
||||
/*
|
||||
context.Context may be providen from
|
||||
an officePrinter which needs to merge
|
||||
its result files.
|
||||
*/
|
||||
if p.ctx == nil {
|
||||
ctx, cancel := timeout.Context(p.opts.WaitTimeout)
|
||||
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout)
|
||||
defer cancel()
|
||||
p.ctx = ctx
|
||||
}
|
||||
p.logger.DebugfOp(op, "merging '%v'...", p.fpaths)
|
||||
resolver := func() error {
|
||||
var cmdArgs []string
|
||||
cmdArgs = append(cmdArgs, p.fpaths...)
|
||||
cmdArgs = append(cmdArgs, "cat", "output", destination)
|
||||
cmd := exec.CommandContext(p.ctx, "pdftk", cmdArgs...)
|
||||
_, err := cmd.Output()
|
||||
var args []string
|
||||
args = append(args, p.fpaths...)
|
||||
args = append(args, "cat", "output", destination)
|
||||
cmd, err := xexec.CommandContext(p.ctx, p.logger, "pdftk", args...)
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
xexec.LogBeforeExecute(p.logger, cmd)
|
||||
return cmd.Run()
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return timeout.Err(p.ctx, err)
|
||||
return xcontext.MustHandleError(
|
||||
p.ctx,
|
||||
xerror.New(op, err),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = Printer(new(merge))
|
||||
_ = Printer(new(mergePrinter))
|
||||
)
|
||||
|
||||
@@ -4,67 +4,75 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/labstack/gommon/random"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/timeout"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xcontext"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xexec"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xrand"
|
||||
)
|
||||
|
||||
type office struct {
|
||||
type officePrinter struct {
|
||||
logger xlog.Logger
|
||||
fpaths []string
|
||||
opts *OfficeOptions
|
||||
opts OfficePrinterOptions
|
||||
}
|
||||
|
||||
// OfficeOptions helps customizing the
|
||||
// Office printer behaviour.
|
||||
type OfficeOptions struct {
|
||||
// OfficePrinterOptions helps customizing the
|
||||
// Office Printer behaviour.
|
||||
type OfficePrinterOptions struct {
|
||||
WaitTimeout float64
|
||||
Landscape bool
|
||||
}
|
||||
|
||||
// NewOffice returns an Office printer.
|
||||
func NewOffice(fpaths []string, opts *OfficeOptions) Printer {
|
||||
return &office{
|
||||
// NewOfficePrinter returns a Printer which
|
||||
// is able to convert Office documents to PDF.
|
||||
func NewOfficePrinter(logger xlog.Logger, fpaths []string, opts OfficePrinterOptions) Printer {
|
||||
return officePrinter{
|
||||
logger: logger,
|
||||
fpaths: fpaths,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *office) Print(destination string) error {
|
||||
const op string = "printer.office.Print"
|
||||
ctx, cancel := timeout.Context(p.opts.WaitTimeout)
|
||||
func (p officePrinter) Print(destination string) error {
|
||||
const op string = "printer.officePrinter.Print"
|
||||
logOptions(p.logger, p.opts)
|
||||
ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout)
|
||||
defer cancel()
|
||||
fpaths := make([]string, len(p.fpaths))
|
||||
resolver := func() error {
|
||||
fpaths := make([]string, len(p.fpaths))
|
||||
dirPath := filepath.Dir(destination)
|
||||
for i, fpath := range p.fpaths {
|
||||
baseFilename := random.String(32)
|
||||
baseFilename := xrand.Get()
|
||||
tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename)
|
||||
if err := unoconv(ctx, fpath, tmpDest, p.opts); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
p.logger.DebugfOp(op, "converting '%s' to PDF...", fpath)
|
||||
if err := unoconv(ctx, p.logger, fpath, tmpDest, p.opts); err != nil {
|
||||
return err
|
||||
}
|
||||
p.logger.DebugfOp(op, "'%s.pdf' created", baseFilename)
|
||||
fpaths[i] = tmpDest
|
||||
}
|
||||
return nil
|
||||
if len(fpaths) == 1 {
|
||||
p.logger.DebugOp(op, "only one PDF created, nothing to merge")
|
||||
if err := os.Rename(fpaths[0], destination); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
m := mergePrinter{
|
||||
ctx: ctx,
|
||||
fpaths: fpaths,
|
||||
}
|
||||
return m.Print(destination)
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return timeout.Err(ctx, err)
|
||||
}
|
||||
if len(fpaths) == 1 {
|
||||
if err := os.Rename(fpaths[0], destination); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
m := &merge{
|
||||
ctx: ctx,
|
||||
fpaths: fpaths,
|
||||
}
|
||||
if err := m.Print(destination); err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
return xcontext.MustHandleError(
|
||||
ctx,
|
||||
xerror.New(op, err),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -72,31 +80,41 @@ func (p *office) Print(destination string) error {
|
||||
// nolint: gochecknoglobals
|
||||
var mu sync.Mutex
|
||||
|
||||
func unoconv(ctx context.Context, fpath, destination string, opts *OfficeOptions) error {
|
||||
func unoconv(ctx context.Context, logger xlog.Logger, fpath, destination string, opts OfficePrinterOptions) error {
|
||||
const op string = "printer.unoconv"
|
||||
// TODO check if timeout while waiting for the lock.
|
||||
logger.DebugOp(op, "waiting lock to be released...")
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
cmdArgs := []string{
|
||||
"--format",
|
||||
"pdf",
|
||||
logger.DebugOp(op, "lock released")
|
||||
resolver := func() error {
|
||||
args := []string{
|
||||
"--format",
|
||||
"pdf",
|
||||
}
|
||||
if opts.Landscape {
|
||||
args = append(args, "--printer", "PaperOrientation=landscape")
|
||||
}
|
||||
args = append(args, "--output", destination, fpath)
|
||||
cmd, err := xexec.CommandContext(
|
||||
ctx,
|
||||
logger,
|
||||
"unoconv",
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
xexec.LogBeforeExecute(logger, cmd)
|
||||
return cmd.Run()
|
||||
}
|
||||
if opts.Landscape {
|
||||
cmdArgs = append(cmdArgs, "--printer", "PaperOrientation=landscape")
|
||||
}
|
||||
cmdArgs = append(cmdArgs, "--output", destination, fpath)
|
||||
cmd := exec.CommandContext(
|
||||
ctx,
|
||||
"unoconv",
|
||||
cmdArgs...,
|
||||
)
|
||||
_, err := cmd.Output()
|
||||
if err != nil {
|
||||
return &standarderror.Error{Op: op, Err: err}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = Printer(new(office))
|
||||
_ = Printer(new(officePrinter))
|
||||
)
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
package printer
|
||||
|
||||
import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
// Printer is a type that can create a PDF file from a source.
|
||||
// The source is defined in the underlying implementation.
|
||||
type Printer interface {
|
||||
Print(destination string) error
|
||||
}
|
||||
|
||||
func logOptions(logger xlog.Logger, opts interface{}) {
|
||||
const op string = "printer.logOptions"
|
||||
logger.DebugfOp(op, "options: %+v", opts)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package printer
|
||||
|
||||
// NewURL returns a URL printer.
|
||||
func NewURL(url string, opts *ChromeOptions) Printer {
|
||||
return &chrome{
|
||||
url: url,
|
||||
opts: opts,
|
||||
import (
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
// NewURLPrinter returns a Printer which
|
||||
// is able to convert a URL to PDF.
|
||||
func NewURLPrinter(logger xlog.Logger, url string, opts ChromePrinterOptions) Printer {
|
||||
return chromePrinter{
|
||||
logger: logger,
|
||||
url: url,
|
||||
opts: opts,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package random helps generating
|
||||
// a random string.
|
||||
package random
|
||||
@@ -1,7 +0,0 @@
|
||||
/*
|
||||
Package standarderror helps standardizing
|
||||
the errors in the application.
|
||||
|
||||
Credits: https://middlemost.com/failure-is-your-domain/
|
||||
*/
|
||||
package standarderror
|
||||
@@ -1,108 +0,0 @@
|
||||
package standarderror
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
// Internal is a code
|
||||
// for internal errors.
|
||||
Internal = "internal"
|
||||
// Invalid is a code
|
||||
// for validation errors.
|
||||
Invalid = "invalid"
|
||||
// Timeout is a code
|
||||
// for timeout errors.
|
||||
Timeout = "timeout"
|
||||
)
|
||||
|
||||
// Error defines a standard application
|
||||
// error.
|
||||
type Error struct {
|
||||
// Code is a machine-readable
|
||||
// error code.
|
||||
Code string
|
||||
// Message is a human-readable
|
||||
// message.
|
||||
Message string
|
||||
// Op is a logical operation.
|
||||
Op string
|
||||
// Err is a nested error.
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error returns the string representation of the error message.
|
||||
func (err *Error) Error() string {
|
||||
var buf bytes.Buffer
|
||||
// if wrapping an error, print its Error() message.
|
||||
// Otherwise print the error code & message.
|
||||
if err.Err != nil {
|
||||
buf.WriteString(err.Err.Error())
|
||||
} else {
|
||||
if err.Code != "" {
|
||||
fmt.Fprintf(&buf, "<%s> ", err.Code)
|
||||
}
|
||||
buf.WriteString(err.Message)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Code returns the code of the root error, if available.
|
||||
// Otherwise returns Internal.
|
||||
func Code(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
e, ok := err.(*Error)
|
||||
if ok && e.Code != "" {
|
||||
return e.Code
|
||||
}
|
||||
if ok && e.Err != nil {
|
||||
return Code(e.Err)
|
||||
}
|
||||
return Internal
|
||||
}
|
||||
|
||||
const defaultMessage string = "an internal error has occurred: please contact technical support"
|
||||
|
||||
// Message returns the human-readable message of the error, if available.
|
||||
// Otherwise returns a generic error message.
|
||||
func Message(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
e, ok := err.(*Error)
|
||||
if ok && e.Message != "" {
|
||||
return e.Message
|
||||
}
|
||||
if ok && e.Err != nil {
|
||||
return Message(e.Err)
|
||||
}
|
||||
return defaultMessage
|
||||
}
|
||||
|
||||
// Op returns the logical operation of the error, if available.
|
||||
// Otherwise returns an empty string.
|
||||
func Op(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
e, ok := err.(*Error)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if e.Op != "" {
|
||||
fmt.Fprintf(&buf, "%s", e.Op)
|
||||
}
|
||||
if nestedOp := Op(e.Err); nestedOp != "" {
|
||||
fmt.Fprintf(&buf, ": %s", nestedOp)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = error(new(Error))
|
||||
)
|
||||
@@ -1,70 +0,0 @@
|
||||
package standarderror
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func scenario1() error {
|
||||
rootErr := errors.New("root error")
|
||||
nestedErr := &Error{
|
||||
Code: Invalid,
|
||||
Op: "bar",
|
||||
Message: "nested error",
|
||||
Err: rootErr,
|
||||
}
|
||||
err := &Error{
|
||||
Op: "foo",
|
||||
Err: nestedErr,
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func scenario2() error {
|
||||
nestedErr := &Error{
|
||||
Code: Invalid,
|
||||
Op: "bar",
|
||||
Message: "nested error",
|
||||
}
|
||||
err := &Error{
|
||||
Code: Internal,
|
||||
Op: "foo",
|
||||
Err: nestedErr,
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
err := scenario1()
|
||||
assert.Equal(t, "root error", err.Error())
|
||||
err = scenario2()
|
||||
assert.Equal(t, "<invalid> nested error", err.Error())
|
||||
}
|
||||
|
||||
func TestCode(t *testing.T) {
|
||||
assert.Equal(t, "", Code(nil))
|
||||
err := scenario1()
|
||||
assert.Equal(t, Invalid, Code(err))
|
||||
err = scenario2()
|
||||
assert.Equal(t, Internal, Code(err))
|
||||
err = errors.New("some error")
|
||||
assert.Equal(t, Internal, Code(err))
|
||||
}
|
||||
|
||||
func TestMessage(t *testing.T) {
|
||||
assert.Equal(t, "", Message(nil))
|
||||
err := scenario1()
|
||||
assert.Equal(t, "nested error", Message(err))
|
||||
err = errors.New("some error")
|
||||
assert.Equal(t, defaultMessage, Message(err))
|
||||
}
|
||||
|
||||
func TestOp(t *testing.T) {
|
||||
assert.Equal(t, "", Op(nil))
|
||||
err := scenario1()
|
||||
assert.Equal(t, "foo: bar", Op(err))
|
||||
err = errors.New("some error")
|
||||
assert.Equal(t, "", Op(err))
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// Package timeout helps managing
|
||||
// context with timeout.
|
||||
package timeout
|
||||
@@ -1,47 +0,0 @@
|
||||
package timeout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
)
|
||||
|
||||
// Context creates a context with timeout for
|
||||
// given second.
|
||||
func Context(seconds float64) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), Duration(seconds))
|
||||
}
|
||||
|
||||
// Duration creates a duration from seconds.
|
||||
func Duration(seconds float64) time.Duration {
|
||||
return time.Duration(1000*seconds) * time.Millisecond
|
||||
}
|
||||
|
||||
// Err checks if there is an error in the given context
|
||||
// and wraps the previous error inside a standarderror.Error.
|
||||
func Err(ctx context.Context, previousErr error) error {
|
||||
const op string = "timeout.Err"
|
||||
if previousErr == nil {
|
||||
panic(fmt.Sprintf("%s: previous error should not be nil", op))
|
||||
}
|
||||
err := ctx.Err()
|
||||
if err == nil {
|
||||
return previousErr
|
||||
}
|
||||
if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) {
|
||||
return &standarderror.Error{
|
||||
Code: standarderror.Timeout,
|
||||
Message: "context has timed out",
|
||||
Op: op,
|
||||
Err: previousErr,
|
||||
}
|
||||
}
|
||||
return &standarderror.Error{
|
||||
Message: "context finished with an error",
|
||||
Op: op,
|
||||
Err: previousErr,
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package timeout
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/standarderror"
|
||||
"github.com/thecodingmachine/gotenberg/test"
|
||||
)
|
||||
|
||||
func TestDuration(t *testing.T) {
|
||||
expected := time.Duration(1500) * time.Millisecond
|
||||
result := Duration(1.5)
|
||||
assert.Equal(t, expected.String(), result.String())
|
||||
}
|
||||
|
||||
func TestErr(t *testing.T) {
|
||||
previousErr := errors.New("previous error")
|
||||
// should be OK.
|
||||
ctx, cancel := Context(5)
|
||||
defer cancel()
|
||||
assert.NotNil(t, Err(ctx, previousErr))
|
||||
// should timeout.
|
||||
ctx, cancel = Context(0.5)
|
||||
defer cancel()
|
||||
time.Sleep(Duration(1))
|
||||
err := Err(ctx, previousErr)
|
||||
assert.NotNil(t, err)
|
||||
standardized := test.RequireStandardError(t, err)
|
||||
assert.Equal(t, standarderror.Timeout, standardized.Code)
|
||||
// should failed.
|
||||
ctx, cancel = Context(5)
|
||||
cancel()
|
||||
err = Err(ctx, previousErr)
|
||||
assert.NotNil(t, err)
|
||||
standardized = test.RequireStandardError(t, err)
|
||||
assert.Equal(t, standarderror.Internal, standarderror.Code(err))
|
||||
}
|
||||
8
internal/pkg/xassert/doc.go
Normal file
8
internal/pkg/xassert/doc.go
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Package xassert is a helper for converting
|
||||
and/or validating strings.
|
||||
|
||||
All functions return our standard xerror.Error
|
||||
in case of error.
|
||||
*/
|
||||
package xassert
|
||||
88
internal/pkg/xassert/float64.go
Normal file
88
internal/pkg/xassert/float64.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package xassert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
)
|
||||
|
||||
// RuleFloat64 is an interface for
|
||||
// validating a float64.
|
||||
type RuleFloat64 interface {
|
||||
with(key string, value float64)
|
||||
validate() error
|
||||
}
|
||||
|
||||
type baseRuleFloat64 struct {
|
||||
key string
|
||||
value float64
|
||||
}
|
||||
|
||||
func (r *baseRuleFloat64) with(key string, value float64) {
|
||||
r.key = key
|
||||
r.value = value
|
||||
}
|
||||
|
||||
type ruleFloat64NotInferiorTo struct {
|
||||
*baseRuleFloat64
|
||||
lowerBound float64
|
||||
}
|
||||
|
||||
func (r ruleFloat64NotInferiorTo) validate() error {
|
||||
const op string = "xassert.ruleFloat64NotInferiorTo.validate"
|
||||
if r.value < r.lowerBound {
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' should be > '%f', got '%f'", r.key, r.lowerBound, r.value),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Float64NotInferiorTo returns a RuleFloat64 for
|
||||
validating that a float64 is not inferior to
|
||||
given lower bound.
|
||||
*/
|
||||
func Float64NotInferiorTo(lowerBound float64) RuleFloat64 {
|
||||
return ruleFloat64NotInferiorTo{
|
||||
&baseRuleFloat64{},
|
||||
lowerBound,
|
||||
}
|
||||
}
|
||||
|
||||
type ruleFloat64NotSuperiorTo struct {
|
||||
*baseRuleFloat64
|
||||
upperBound float64
|
||||
}
|
||||
|
||||
func (r ruleFloat64NotSuperiorTo) validate() error {
|
||||
const op string = "xassert.ruleFloat64NotSuperiorTo.validate"
|
||||
if r.value > r.upperBound {
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' should be < '%f', got '%f'", r.key, r.upperBound, r.value),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Float64NotSuperiorTo returns a RuleFloat64 for
|
||||
validating that a float64 is not superior to
|
||||
given upper bound.
|
||||
*/
|
||||
func Float64NotSuperiorTo(upperBound float64) RuleFloat64 {
|
||||
return ruleFloat64NotSuperiorTo{
|
||||
&baseRuleFloat64{},
|
||||
upperBound,
|
||||
}
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = RuleFloat64(new(ruleFloat64NotInferiorTo))
|
||||
_ = RuleFloat64(new(ruleFloat64NotSuperiorTo))
|
||||
)
|
||||
32
internal/pkg/xassert/float64_test.go
Normal file
32
internal/pkg/xassert/float64_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package xassert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||
)
|
||||
|
||||
func TestFloat64NotInferiorTo(t *testing.T) {
|
||||
rule := Float64NotInferiorTo(0.0)
|
||||
// should be OK.
|
||||
rule.with("FOO", 10.0)
|
||||
err := rule.validate()
|
||||
assert.Nil(t, err)
|
||||
// should not be OK.
|
||||
rule.with("FOO", -10.0)
|
||||
err = rule.validate()
|
||||
xerrortest.AssertError(t, err)
|
||||
}
|
||||
|
||||
func TestFloat64NotSuperiorTo(t *testing.T) {
|
||||
rule := Float64NotSuperiorTo(0.0)
|
||||
// should be OK.
|
||||
rule.with("FOO", -10.0)
|
||||
err := rule.validate()
|
||||
assert.Nil(t, err)
|
||||
// should not be OK.
|
||||
rule.with("FOO", 10.0)
|
||||
err = rule.validate()
|
||||
xerrortest.AssertError(t, err)
|
||||
}
|
||||
88
internal/pkg/xassert/int64.go
Normal file
88
internal/pkg/xassert/int64.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package xassert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
)
|
||||
|
||||
// RuleInt64 is an interface for
|
||||
// validating an int64.
|
||||
type RuleInt64 interface {
|
||||
with(key string, value int64)
|
||||
validate() error
|
||||
}
|
||||
|
||||
type baseRuleInt64 struct {
|
||||
key string
|
||||
value int64
|
||||
}
|
||||
|
||||
func (r *baseRuleInt64) with(key string, value int64) {
|
||||
r.key = key
|
||||
r.value = value
|
||||
}
|
||||
|
||||
type ruleInt64NotInferiorTo struct {
|
||||
*baseRuleInt64
|
||||
lowerBound int64
|
||||
}
|
||||
|
||||
func (r ruleInt64NotInferiorTo) validate() error {
|
||||
const op string = "xassert.ruleInt64NotInferiorTo.validate"
|
||||
if r.value < r.lowerBound {
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' should be > '%d', got '%d'", r.key, r.lowerBound, r.value),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Int64NotInferiorTo returns a RuleInt64 for
|
||||
validating that an int64 is not inferior to
|
||||
given lower bound.
|
||||
*/
|
||||
func Int64NotInferiorTo(lowerBound int64) RuleInt64 {
|
||||
return &ruleInt64NotInferiorTo{
|
||||
&baseRuleInt64{},
|
||||
lowerBound,
|
||||
}
|
||||
}
|
||||
|
||||
type ruleInt64NotSuperiorTo struct {
|
||||
*baseRuleInt64
|
||||
upperBound int64
|
||||
}
|
||||
|
||||
func (r ruleInt64NotSuperiorTo) validate() error {
|
||||
const op string = "xassert.ruleInt64NotSuperiorTo.validate"
|
||||
if r.value > r.upperBound {
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' should be < '%d', got '%d'", r.key, r.upperBound, r.value),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Int64NotSuperiorTo returns a RuleInt64 for
|
||||
validating that an int64 is not superior to
|
||||
given upper bound.
|
||||
*/
|
||||
func Int64NotSuperiorTo(upperBound int64) RuleInt64 {
|
||||
return ruleInt64NotSuperiorTo{
|
||||
&baseRuleInt64{},
|
||||
upperBound,
|
||||
}
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = RuleInt64(new(ruleInt64NotInferiorTo))
|
||||
_ = RuleInt64(new(ruleInt64NotSuperiorTo))
|
||||
)
|
||||
32
internal/pkg/xassert/int64_test.go
Normal file
32
internal/pkg/xassert/int64_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package xassert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||
)
|
||||
|
||||
func TestInt64NotInferiorTo(t *testing.T) {
|
||||
rule := Int64NotInferiorTo(0)
|
||||
// should be OK.
|
||||
rule.with("FOO", 10)
|
||||
err := rule.validate()
|
||||
assert.Nil(t, err)
|
||||
// should not be OK.
|
||||
rule.with("FOO", -10)
|
||||
err = rule.validate()
|
||||
xerrortest.AssertError(t, err)
|
||||
}
|
||||
|
||||
func TestInt64NotSuperiorTo(t *testing.T) {
|
||||
rule := Int64NotSuperiorTo(0)
|
||||
// should be OK.
|
||||
rule.with("FOO", -10)
|
||||
err := rule.validate()
|
||||
assert.Nil(t, err)
|
||||
// should not be OK.
|
||||
rule.with("FOO", 10)
|
||||
err = rule.validate()
|
||||
xerrortest.AssertError(t, err)
|
||||
}
|
||||
60
internal/pkg/xassert/string.go
Normal file
60
internal/pkg/xassert/string.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package xassert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
)
|
||||
|
||||
// RuleString is an interface for
|
||||
// validating a string.
|
||||
type RuleString interface {
|
||||
with(key, value string)
|
||||
validate() error
|
||||
}
|
||||
|
||||
type baseRuleString struct {
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
func (r *baseRuleString) with(key, value string) {
|
||||
r.key = key
|
||||
r.value = value
|
||||
}
|
||||
|
||||
type ruleStringOneOf struct {
|
||||
*baseRuleString
|
||||
values []string
|
||||
}
|
||||
|
||||
func (r ruleStringOneOf) validate() error {
|
||||
const op string = "xassert.ruleStringOneOf.validate"
|
||||
for _, v := range r.values {
|
||||
if r.value == v {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' should be one of '%v', got '%s'", r.key, r.values, r.value),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
StringOneOf returns a RuleString for
|
||||
validating that a string is one of given
|
||||
values.
|
||||
*/
|
||||
func StringOneOf(values []string) RuleString {
|
||||
return ruleStringOneOf{
|
||||
&baseRuleString{},
|
||||
values,
|
||||
}
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = RuleString(new(ruleStringOneOf))
|
||||
)
|
||||
20
internal/pkg/xassert/string_test.go
Normal file
20
internal/pkg/xassert/string_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package xassert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||
)
|
||||
|
||||
func TestStringOfOne(t *testing.T) {
|
||||
rule := StringOneOf([]string{"foo", "bar", "baz"})
|
||||
// should be OK.
|
||||
rule.with("FOO", "foo")
|
||||
err := rule.validate()
|
||||
assert.Nil(t, err)
|
||||
// should not be OK.
|
||||
rule.with("FOO", "qux")
|
||||
err = rule.validate()
|
||||
xerrortest.AssertError(t, err)
|
||||
}
|
||||
185
internal/pkg/xassert/xassert.go
Normal file
185
internal/pkg/xassert/xassert.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package xassert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
)
|
||||
|
||||
/*
|
||||
String applies validation on a string.
|
||||
|
||||
If string is empty or validation fails,
|
||||
returns the default value.
|
||||
|
||||
The key is used to identify the value.
|
||||
*/
|
||||
func String(key, value, defaultValue string, rules ...RuleString) (string, error) {
|
||||
const op string = "xassert.String"
|
||||
result := defaultValue
|
||||
if value != "" {
|
||||
result = value
|
||||
}
|
||||
for _, rule := range rules {
|
||||
rule.with(key, result)
|
||||
if err := rule.validate(); err != nil {
|
||||
return defaultValue, xerror.New(op, err)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
StringFromEnv returns the value of given environment
|
||||
variable or the default value if not found or
|
||||
validation fails.
|
||||
*/
|
||||
func StringFromEnv(envVar, defaultValue string, rules ...RuleString) (string, error) {
|
||||
const op string = "xassert.StringFromEnv"
|
||||
value := os.Getenv(envVar)
|
||||
result, err := String(envVar, value, defaultValue, rules...)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Int64 tries to convert a string to an int64.
|
||||
|
||||
If string is empty, conversion or validation fails,
|
||||
returns the default value.
|
||||
|
||||
The key is used to identify the value.
|
||||
*/
|
||||
func Int64(key, value string, defaultValue int64, rules ...RuleInt64) (int64, error) {
|
||||
const op string = "xassert.Int64"
|
||||
result := defaultValue
|
||||
if value != "" {
|
||||
parsedValue, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return defaultValue, xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' is not an integer, got '%s'", key, value),
|
||||
err,
|
||||
)
|
||||
}
|
||||
result = parsedValue
|
||||
}
|
||||
for _, rule := range rules {
|
||||
rule.with(key, result)
|
||||
if err := rule.validate(); err != nil {
|
||||
return defaultValue, xerror.New(op, err)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Int64FromEnv returns the int64 representation of the
|
||||
value of given environment variable.
|
||||
|
||||
If not found, empty, conversion or validation fails,
|
||||
returns the default value.
|
||||
*/
|
||||
func Int64FromEnv(envVar string, defaultValue int64, rules ...RuleInt64) (int64, error) {
|
||||
const op string = "xassert.Int64FromEnv"
|
||||
value := os.Getenv(envVar)
|
||||
result, err := Int64(envVar, value, defaultValue, rules...)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Float64 tries to convert a string to a float64.
|
||||
|
||||
If string is empty, conversion or validation fails,
|
||||
returns the default value.
|
||||
|
||||
The key is used to identify the value.
|
||||
*/
|
||||
func Float64(key, value string, defaultValue float64, rules ...RuleFloat64) (float64, error) {
|
||||
const op string = "xassert.Float64"
|
||||
result := defaultValue
|
||||
if value != "" {
|
||||
parsedValue, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return defaultValue, xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' is not a float, got '%s'", key, value),
|
||||
err,
|
||||
)
|
||||
}
|
||||
result = parsedValue
|
||||
}
|
||||
for _, rule := range rules {
|
||||
rule.with(key, result)
|
||||
if err := rule.validate(); err != nil {
|
||||
return defaultValue, xerror.New(op, err)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Float64FromEnv returns the float64 representation of the
|
||||
value of given environment variable.
|
||||
|
||||
If not found, empty, conversion or validation fails,
|
||||
returns the default value.
|
||||
*/
|
||||
func Float64FromEnv(envVar string, defaultValue float64, rules ...RuleFloat64) (float64, error) {
|
||||
const op string = "xassert.Float64FromEnv"
|
||||
value := os.Getenv(envVar)
|
||||
result, err := Float64(envVar, value, defaultValue, rules...)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Bool tries to convert a string to a boolean.
|
||||
|
||||
If string is empty or conversion fails, returns the
|
||||
default value.
|
||||
|
||||
The key is used to identify the value.
|
||||
*/
|
||||
func Bool(key, value string, defaultValue bool) (bool, error) {
|
||||
const op string = "xassert.Bool"
|
||||
result := defaultValue
|
||||
if value != "" {
|
||||
parsedValue, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return defaultValue, xerror.Invalid(
|
||||
op,
|
||||
fmt.Sprintf("'%s' is not a boolean, got '%s'", key, value),
|
||||
err,
|
||||
)
|
||||
}
|
||||
result = parsedValue
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
/*
|
||||
BoolFromEnv returns the boolean representation of the
|
||||
value of given environment variable.
|
||||
|
||||
If not found, empty or conversion fails, returns the
|
||||
default value.
|
||||
*/
|
||||
func BoolFromEnv(envVar string, defaultValue bool) (bool, error) {
|
||||
const op string = "xassert.BoolFromEnv"
|
||||
value := os.Getenv(envVar)
|
||||
result, err := Bool(envVar, value, defaultValue)
|
||||
if err != nil {
|
||||
return result, xerror.New(op, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
289
internal/pkg/xassert/xassert_test.go
Normal file
289
internal/pkg/xassert/xassert_test.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package xassert
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||
)
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
const (
|
||||
defaultValue string = "FOO"
|
||||
)
|
||||
var expected string
|
||||
rule := StringOneOf([]string{"FOO", "BAR"})
|
||||
// empty value, result should be equal
|
||||
// to the default value.
|
||||
v, err := String("foo", "", defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// result should be equal to given value
|
||||
// as it is one of "FOO" and "BAR".
|
||||
expected = "FOO"
|
||||
v, err = String("foo", expected, defaultValue, rule)
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as given value is not
|
||||
// one of "FOO" and "BAR".
|
||||
v, err = String("foo", "BAZ", defaultValue, rule)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
xerrortest.AssertError(t, err)
|
||||
}
|
||||
|
||||
func TestStringFromEnv(t *testing.T) {
|
||||
const (
|
||||
envVar string = "FOO"
|
||||
defaultValue string = "FOO"
|
||||
)
|
||||
var expected string
|
||||
rule := StringOneOf([]string{"FOO", "BAR"})
|
||||
// no environment variable set,
|
||||
// value should be equal to default value.
|
||||
v, err := StringFromEnv(envVar, defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// result should be equal to environment variable
|
||||
// value as it is one of "FOO" and "BAR".
|
||||
expected = "BAR"
|
||||
os.Setenv(envVar, expected)
|
||||
v, err = StringFromEnv(envVar, defaultValue, rule)
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(envVar)
|
||||
// should not be OK as environment variable
|
||||
// value is not one of "FOO" and "BAR".
|
||||
os.Setenv(envVar, "BAZ")
|
||||
v, err = StringFromEnv(envVar, defaultValue, rule)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
xerrortest.AssertError(t, err)
|
||||
os.Unsetenv(envVar)
|
||||
}
|
||||
|
||||
func TestInt64(t *testing.T) {
|
||||
const (
|
||||
defaultValue int64 = 10
|
||||
)
|
||||
var expected int64
|
||||
rule := Int64NotInferiorTo(6)
|
||||
// empty value, result should be equal
|
||||
// to the default value.
|
||||
v, err := Int64("foo", "", defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// result should be equal to given value
|
||||
// but as integer.
|
||||
v, err = Int64("foo", "5", defaultValue)
|
||||
expected = 5
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as given value is not
|
||||
// a string representation of an integer.
|
||||
v, err = Int64("foo", "foo", defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
xerrortest.AssertError(t, err)
|
||||
// should not be OK as given value does not
|
||||
// validate the rule x >= 6.
|
||||
v, err = Int64("foo", "5", defaultValue, rule)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
xerrortest.AssertError(t, err)
|
||||
}
|
||||
|
||||
func TestInt64FromEnv(t *testing.T) {
|
||||
const (
|
||||
envVar string = "FOO"
|
||||
defaultValue int64 = 10
|
||||
)
|
||||
var expected int64
|
||||
rule := Int64NotInferiorTo(6)
|
||||
// no environment variable set,
|
||||
// value should be equal to default value.
|
||||
v, err := Int64FromEnv(envVar, defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// result should be equal to environment variable
|
||||
// value but as integer.
|
||||
os.Setenv(envVar, "5")
|
||||
v, err = Int64FromEnv(envVar, defaultValue)
|
||||
expected = 5
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(envVar)
|
||||
// should not be OK as environment variable
|
||||
// value is not a string representation of an integer.
|
||||
os.Setenv(envVar, "foo")
|
||||
v, err = Int64FromEnv(envVar, defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
xerrortest.AssertError(t, err)
|
||||
os.Unsetenv(envVar)
|
||||
// should not be OK as environment variable
|
||||
// value does not validate the rule x >= 6.
|
||||
os.Setenv(envVar, "5")
|
||||
v, err = Int64FromEnv(envVar, defaultValue, rule)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
xerrortest.AssertError(t, err)
|
||||
os.Unsetenv(envVar)
|
||||
}
|
||||
|
||||
func TestFloat64(t *testing.T) {
|
||||
const defaultValue float64 = 10.0
|
||||
var expected float64
|
||||
rule := Float64NotInferiorTo(6.0)
|
||||
// empty value, result should be equal
|
||||
// to the default value.
|
||||
v, err := Float64("foo", "", defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// result should be equal to given value
|
||||
// but as float.
|
||||
v, err = Float64("foo", "5.5", defaultValue)
|
||||
expected = 5.5
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as given value is not
|
||||
// a string representation of a float.
|
||||
v, err = Float64("foo", "foo", defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
xerrortest.AssertError(t, err)
|
||||
// should not be OK as given value does not
|
||||
// validate the rule x >= 6.
|
||||
v, err = Float64("foo", "5", defaultValue, rule)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
xerrortest.AssertError(t, err)
|
||||
}
|
||||
|
||||
func TestFloat64FromEnv(t *testing.T) {
|
||||
const (
|
||||
envVar string = "FOO"
|
||||
defaultValue float64 = 10.0
|
||||
)
|
||||
var expected float64
|
||||
rule := Float64NotInferiorTo(6.0)
|
||||
// no environment variable set,
|
||||
// value should be equal to default value.
|
||||
v, err := Float64FromEnv(envVar, defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// result should be equal to environment variable
|
||||
// value but as float.
|
||||
os.Setenv(envVar, "5.5")
|
||||
v, err = Float64FromEnv(envVar, defaultValue)
|
||||
expected = 5.5
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(envVar)
|
||||
// should not be OK as environment variable
|
||||
// value is not a string representation of a float.
|
||||
os.Setenv(envVar, "foo")
|
||||
v, err = Float64FromEnv(envVar, defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
xerrortest.AssertError(t, err)
|
||||
os.Unsetenv(envVar)
|
||||
// should not be OK as environment variable
|
||||
// value does not validate the rule x >= 6.
|
||||
os.Setenv(envVar, "5")
|
||||
v, err = Float64FromEnv(envVar, defaultValue, rule)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
xerrortest.AssertError(t, err)
|
||||
os.Unsetenv(envVar)
|
||||
}
|
||||
|
||||
func TestBool(t *testing.T) {
|
||||
const defaultValue bool = true
|
||||
var expected bool
|
||||
// empty value, result should be equal
|
||||
// to the default value.
|
||||
v, err := Bool("foo", "", defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// result should be equal to given value
|
||||
// but as boolean.
|
||||
v, err = Bool("foo", "1", defaultValue)
|
||||
expected = true
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
v, err = Bool("foo", "true", defaultValue)
|
||||
expected = true
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
v, err = Bool("foo", "0", defaultValue)
|
||||
expected = false
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
v, err = Bool("foo", "false", defaultValue)
|
||||
expected = false
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// should not be OK as given value is not
|
||||
// a string representation of a boolean.
|
||||
v, err = Bool("foo", "foo", defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
xerrortest.AssertError(t, err)
|
||||
}
|
||||
|
||||
func TestBoolFromEnv(t *testing.T) {
|
||||
const (
|
||||
envVar string = "FOO"
|
||||
defaultValue bool = true
|
||||
)
|
||||
var expected bool
|
||||
// no environment variable set,
|
||||
// value should be equal to default value.
|
||||
v, err := BoolFromEnv(envVar, defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
// result should be equal to environment variable
|
||||
// value but as boolean.
|
||||
os.Setenv(envVar, "1")
|
||||
v, err = BoolFromEnv(envVar, defaultValue)
|
||||
expected = true
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(envVar)
|
||||
os.Setenv(envVar, "true")
|
||||
v, err = BoolFromEnv(envVar, defaultValue)
|
||||
expected = true
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(envVar)
|
||||
os.Setenv(envVar, "0")
|
||||
v, err = BoolFromEnv(envVar, defaultValue)
|
||||
expected = false
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(envVar)
|
||||
os.Setenv(envVar, "false")
|
||||
v, err = BoolFromEnv(envVar, defaultValue)
|
||||
expected = false
|
||||
assert.Equal(t, expected, v)
|
||||
assert.Nil(t, err)
|
||||
os.Unsetenv(envVar)
|
||||
// should not be OK as environment variable
|
||||
// value is not a string representation of a boolean.
|
||||
os.Setenv(envVar, "foo")
|
||||
v, err = BoolFromEnv(envVar, defaultValue)
|
||||
expected = defaultValue
|
||||
assert.Equal(t, expected, v)
|
||||
xerrortest.AssertError(t, err)
|
||||
os.Unsetenv(envVar)
|
||||
}
|
||||
3
internal/pkg/xcontext/doc.go
Normal file
3
internal/pkg/xcontext/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package xcontext helps managing
|
||||
// context.Context with timeout.
|
||||
package xcontext
|
||||
56
internal/pkg/xcontext/xcontext.go
Normal file
56
internal/pkg/xcontext/xcontext.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package xcontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
|
||||
)
|
||||
|
||||
// WithTimeout creates a context.Context which
|
||||
// times out after given seconds.
|
||||
func WithTimeout(logger xlog.Logger, seconds float64) (context.Context, context.CancelFunc) {
|
||||
const op string = "xcontext.WithTimeout"
|
||||
logger.DebugfOp(op, "creating context with '%.2fs' of timeout...", seconds)
|
||||
return context.WithTimeout(context.Background(), xtime.Duration(seconds))
|
||||
}
|
||||
|
||||
/*
|
||||
MustHandleError checks if there is an error
|
||||
in the given Context.
|
||||
|
||||
If no error, returns the previous error.
|
||||
|
||||
If context.DeadlineExceeded, wraps the previous
|
||||
error inside an xerror.Error with xerror.TimeoutCode.
|
||||
|
||||
Otherwise wraps the previous error inside an
|
||||
xerror.Error.
|
||||
|
||||
It panics if no previous error.
|
||||
*/
|
||||
func MustHandleError(ctx context.Context, previousErr error) error {
|
||||
const op string = "xcontext.MustHandleError"
|
||||
if previousErr == nil {
|
||||
panic(fmt.Sprintf("%s: previous error should not be nil", op))
|
||||
}
|
||||
err := ctx.Err()
|
||||
if err == nil {
|
||||
// we do not wrap the previous error
|
||||
// as it should be wrapped by the caller.
|
||||
return previousErr
|
||||
}
|
||||
// context has timed out
|
||||
if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) {
|
||||
return xerror.Timeout(op, "context has timed out", previousErr)
|
||||
}
|
||||
/*
|
||||
context has another error: we do not
|
||||
wrap the error from the Context as the previous
|
||||
error should contain it.
|
||||
*/
|
||||
return xerror.New(op, previousErr)
|
||||
}
|
||||
43
internal/pkg/xcontext/xcontext_test.go
Normal file
43
internal/pkg/xcontext/xcontext_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package xcontext
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xtime"
|
||||
"github.com/thecodingmachine/gotenberg/test/internalpkg/xerrortest"
|
||||
"github.com/thecodingmachine/gotenberg/test/internalpkg/xlogtest"
|
||||
)
|
||||
|
||||
func TestMustHandleError(t *testing.T) {
|
||||
previousErr := errors.New("previous error")
|
||||
logger := xlogtest.DebugLogger()
|
||||
// context should not have an error.
|
||||
ctx, cancel := WithTimeout(logger, 5)
|
||||
defer cancel()
|
||||
err := MustHandleError(ctx, previousErr)
|
||||
assert.Equal(t, previousErr, err)
|
||||
// should panic.
|
||||
ctx, cancel = WithTimeout(logger, 5)
|
||||
defer cancel()
|
||||
assert.Panics(t, func() {
|
||||
MustHandleError(ctx, nil)
|
||||
})
|
||||
// context should timed out.
|
||||
ctx, cancel = WithTimeout(logger, 0.5)
|
||||
defer cancel()
|
||||
time.Sleep(xtime.Duration(1))
|
||||
err = MustHandleError(ctx, previousErr)
|
||||
xerr := xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, xerror.TimeoutCode, xerror.Code(xerr))
|
||||
// context should have an error different
|
||||
// than context.DeadlineExceeded.
|
||||
ctx, cancel = WithTimeout(logger, 5)
|
||||
cancel()
|
||||
err = MustHandleError(ctx, previousErr)
|
||||
xerr = xerrortest.AssertError(t, err)
|
||||
assert.Equal(t, xerror.InternalCode, xerror.Code(xerr))
|
||||
}
|
||||
7
internal/pkg/xerror/doc.go
Normal file
7
internal/pkg/xerror/doc.go
Normal file
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
Package xerror helps standardizing
|
||||
the errors through the application.
|
||||
|
||||
Credits: https://middlemost.com/failure-is-your-domain/
|
||||
*/
|
||||
package xerror
|
||||
152
internal/pkg/xerror/xerror.go
Normal file
152
internal/pkg/xerror/xerror.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package xerror
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrorCode is machine-readable error code.
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
// InternalCode is an internal error.
|
||||
InternalCode ErrorCode = "internal"
|
||||
// InvalidCode occurs when a validation
|
||||
// failed.
|
||||
InvalidCode ErrorCode = "invalid"
|
||||
// TimeoutCode occurs when something
|
||||
// timed out.
|
||||
TimeoutCode ErrorCode = "timeout"
|
||||
)
|
||||
|
||||
// Error defines our standard application
|
||||
// error.
|
||||
type Error struct {
|
||||
code ErrorCode
|
||||
message string
|
||||
op string
|
||||
err error
|
||||
}
|
||||
|
||||
// Error returns the string representation of the error message.
|
||||
func (e Error) Error() string {
|
||||
var buf bytes.Buffer
|
||||
// if wrapping an error, print its Error() message.
|
||||
// Otherwise print the error code & message.
|
||||
if e.err != nil {
|
||||
buf.WriteString(e.err.Error())
|
||||
} else {
|
||||
if e.code != "" {
|
||||
fmt.Fprintf(&buf, "<%s> ", e.code)
|
||||
}
|
||||
buf.WriteString(e.message)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
/*
|
||||
New returns a xerror.Error.
|
||||
|
||||
Should be used for wrapping an error
|
||||
at the end of a function.
|
||||
*/
|
||||
func New(op string, previous error) error {
|
||||
return &Error{
|
||||
op: op,
|
||||
err: previous,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Invalid returns a xerror.Error.
|
||||
|
||||
Should be used when an input
|
||||
is wrong.
|
||||
*/
|
||||
func Invalid(op, message string, previous error) error {
|
||||
return &Error{
|
||||
code: InvalidCode,
|
||||
message: message,
|
||||
op: op,
|
||||
err: previous,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Timeout returns a xerror.Error.
|
||||
|
||||
Should be used when a timeout occurs.
|
||||
*/
|
||||
func Timeout(op, message string, previous error) error {
|
||||
return &Error{
|
||||
code: TimeoutCode,
|
||||
message: message,
|
||||
op: op,
|
||||
err: previous,
|
||||
}
|
||||
}
|
||||
|
||||
// Code returns the code of the root error, if available.
|
||||
// Otherwise returns InternalCode.
|
||||
func Code(err error) ErrorCode {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
e, ok := err.(*Error)
|
||||
if ok && e.code != "" {
|
||||
return e.code
|
||||
}
|
||||
if ok && e.err != nil {
|
||||
return Code(e.err)
|
||||
}
|
||||
return InternalCode
|
||||
}
|
||||
|
||||
const defaultMessage string = "an internal error has occurred: please contact technical support"
|
||||
|
||||
// Message returns the human-readable message of the error, if available.
|
||||
// Otherwise returns a generic error message.
|
||||
func Message(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
e, ok := err.(*Error)
|
||||
if ok && e.message != "" {
|
||||
return e.message
|
||||
}
|
||||
if ok && e.err != nil {
|
||||
return Message(e.err)
|
||||
}
|
||||
return defaultMessage
|
||||
}
|
||||
|
||||
// Op returns the logical operation of the error, if available.
|
||||
// Otherwise returns an empty string.
|
||||
func Op(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
e, ok := err.(*Error)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
nestedOp := Op(e.err)
|
||||
if nestedOp != "" {
|
||||
// we want to avoid having the same op chained.
|
||||
if e.op != "" && !strings.Contains(nestedOp, e.op) {
|
||||
fmt.Fprintf(&buf, "%s: %s", e.op, nestedOp)
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "%s", nestedOp)
|
||||
}
|
||||
} else if e.op != "" {
|
||||
fmt.Fprintf(&buf, "%s", e.op)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Compile-time checks to ensure type implements desired interfaces.
|
||||
var (
|
||||
_ = error(new(Error))
|
||||
)
|
||||
97
internal/pkg/xerror/xerror_test.go
Normal file
97
internal/pkg/xerror/xerror_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package xerror
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
/*
|
||||
Error 1.0: op = "foo"
|
||||
Error 1.1: op = "bar"
|
||||
Error 1.2: code = "invalid", op = "baz", message = "nested error"
|
||||
Error 1.3: message = "root error"
|
||||
*/
|
||||
func scenario1() error {
|
||||
rootErr := errors.New("root error")
|
||||
nestedErr := Invalid("baz", "nested error", rootErr)
|
||||
wrappingErr := New("bar", nestedErr)
|
||||
return New("foo", wrappingErr)
|
||||
}
|
||||
|
||||
/*
|
||||
Error 2.0: op = "foo"
|
||||
Error 2.1: op = "bar"
|
||||
Error 2.2: code = "timeout", op = "bar", message = "nested error"
|
||||
*/
|
||||
func scenario2() error {
|
||||
nestedErr := Timeout("bar", "nested error", nil)
|
||||
wrappingErr := New("bar", nestedErr)
|
||||
return New("foo", wrappingErr)
|
||||
}
|
||||
|
||||
// Error 3.0: code = "", op = "foo"
|
||||
func scenario3() error {
|
||||
return New("foo", nil)
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
// should return the Error 1.3
|
||||
// message.
|
||||
err := scenario1()
|
||||
assert.Equal(t, "root error", err.Error())
|
||||
// should return the Error 2.2 message with
|
||||
// its code.
|
||||
err = scenario2()
|
||||
assert.Equal(t, "<timeout> nested error", err.Error())
|
||||
}
|
||||
|
||||
func TestCode(t *testing.T) {
|
||||
// should be an empty code if no error.
|
||||
assert.Equal(t, "", fmt.Sprintf("%s", Code(nil)))
|
||||
// should be the code of Error 1.2.
|
||||
err := scenario1()
|
||||
assert.Equal(t, InvalidCode, Code(err))
|
||||
// should be the code of Error 2.2.
|
||||
err = scenario2()
|
||||
assert.Equal(t, TimeoutCode, Code(err))
|
||||
// should be the default code.
|
||||
err = scenario3()
|
||||
assert.Equal(t, InternalCode, Code(err))
|
||||
err = errors.New("some error")
|
||||
assert.Equal(t, InternalCode, Code(err))
|
||||
}
|
||||
|
||||
func TestMessage(t *testing.T) {
|
||||
// should be an empty message if no error.
|
||||
assert.Equal(t, "", Message(nil))
|
||||
// should be the message of Error 1.2.
|
||||
err := scenario1()
|
||||
assert.Equal(t, "nested error", Message(err))
|
||||
// should be the default message.
|
||||
err = errors.New("some error")
|
||||
assert.Equal(t, defaultMessage, Message(err))
|
||||
}
|
||||
|
||||
func TestOp(t *testing.T) {
|
||||
// should be an empty op if no error.
|
||||
assert.Equal(t, "", Op(nil))
|
||||
// should be the chain of op in this order:
|
||||
// Error 1.0 -> Error 1.1 -> Error 1.2.
|
||||
err := scenario1()
|
||||
assert.Equal(t, "foo: bar: baz", Op(err))
|
||||
/*
|
||||
should be the chain of op in this order:
|
||||
Error 2.0 -> Error 2.1.
|
||||
|
||||
As Error 2.1 and Error 2.2 shares the same
|
||||
op, Error 2.2 op is not displayed.
|
||||
*/
|
||||
err = scenario2()
|
||||
assert.Equal(t, "foo: bar", Op(err))
|
||||
// should be an empty op if not Error.
|
||||
err = errors.New("some error")
|
||||
assert.Equal(t, "", Op(err))
|
||||
}
|
||||
8
internal/pkg/xexec/doc.go
Normal file
8
internal/pkg/xexec/doc.go
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Package xexec helps creating exec.Cmd
|
||||
with logging.
|
||||
|
||||
All functions return our standard xerror.Error
|
||||
in case of error.
|
||||
*/
|
||||
package xexec
|
||||
99
internal/pkg/xexec/xexec.go
Normal file
99
internal/pkg/xexec/xexec.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package xexec
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
||||
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
||||
)
|
||||
|
||||
/*
|
||||
Command is a wrapper around exec.Command.
|
||||
|
||||
If given xlog.Logger has a xlog.DebugLevel,
|
||||
also logs the output from the command.
|
||||
*/
|
||||
func Command(logger xlog.Logger, binary string, args ...string) (*exec.Cmd, error) {
|
||||
const op string = "xexec.Command"
|
||||
cmd := exec.Command(binary, args...)
|
||||
if err := pipe(logger, cmd); err != nil {
|
||||
return nil, xerror.New(op, err)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
/*
|
||||
CommandContext is a wrapper around exec.CommandContext.
|
||||
|
||||
If given xlog.Logger has a xlog.DebugLevel,
|
||||
also logs the output from the command.
|
||||
*/
|
||||
func CommandContext(ctx context.Context, logger xlog.Logger, binary string, args ...string) (*exec.Cmd, error) {
|
||||
const op string = "xexec.CommandContext"
|
||||
cmd := exec.CommandContext(ctx, binary, args...)
|
||||
if err := pipe(logger, cmd); err != nil {
|
||||
return nil, xerror.New(op, err)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// LogBeforeExecute logs a command before its execution.
|
||||
func LogBeforeExecute(logger xlog.Logger, cmd *exec.Cmd) {
|
||||
const op string = "xexec.LogBeforeExecute"
|
||||
logger.DebugfOp(op, "executing command: %s", strings.Join(cmd.Args, " "))
|
||||
}
|
||||
|
||||
func pipe(logger xlog.Logger, cmd *exec.Cmd) error {
|
||||
const op string = "xexec.pipe"
|
||||
if logger.Level() != xlog.DebugLevel {
|
||||
return nil
|
||||
}
|
||||
// if xlog.DebugLevel, log the output
|
||||
// from the command.
|
||||
resolver := func() error {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go logCommandOutput(logger, stdout, "stdout", cmd)
|
||||
go logCommandOutput(logger, stderr, "stderr", cmd)
|
||||
return nil
|
||||
}
|
||||
if err := resolver(); err != nil {
|
||||
return xerror.New(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func logCommandOutput(logger xlog.Logger, reader io.ReadCloser, outputType string, cmd *exec.Cmd) {
|
||||
var op string
|
||||
if len(cmd.Args) >= 2 {
|
||||
op = fmt.Sprintf("%s.%s.%s", cmd.Args[0], cmd.Args[1], outputType)
|
||||
} else {
|
||||
// len(cmd.Args) should always be >= 1.
|
||||
op = fmt.Sprintf("%s.%s", cmd.Args[0], outputType)
|
||||
}
|
||||
r := bufio.NewReader(reader)
|
||||
defer reader.Close() // nolint: errcheck
|
||||
for {
|
||||
line, _, err := r.ReadLine()
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
logger.ErrorOp(op, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
if len(line) != 0 {
|
||||
logger.DebugOp(op, string(line))
|
||||
}
|
||||
}
|
||||
}
|
||||
39
internal/pkg/xexec/xexec_test.go
Normal file
39
internal/pkg/xexec/xexec_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package xexec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thecodingmachine/gotenberg/test/internalpkg/xlogtest"
|
||||
)
|
||||
|
||||
func TestCommand(t *testing.T) {
|
||||
logger := xlogtest.DebugLogger()
|
||||
// should pipe command output as
|
||||
// xlog.Logger has a xlog.DebugLevel.
|
||||
cmd, err := Command(logger, "echo", "Hello", "World")
|
||||
assert.Nil(t, err)
|
||||
LogBeforeExecute(logger, cmd)
|
||||
// should not pipe command output as
|
||||
// xlog.Logger has a xlog.InfoLevel.
|
||||
logger = xlogtest.InfoLogger()
|
||||
cmd, err = Command(logger, "echo", "Hello", "World")
|
||||
LogBeforeExecute(logger, cmd)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestCommandContext(t *testing.T) {
|
||||
logger := xlogtest.DebugLogger()
|
||||
// should pipe command output as
|
||||
// xlog.Logger has a xlog.DebugLevel.
|
||||
cmd, err := CommandContext(context.Background(), logger, "echo")
|
||||
assert.Nil(t, err)
|
||||
LogBeforeExecute(logger, cmd)
|
||||
// should not pipe command output as
|
||||
// xlog.Logger has a xlog.InfoLevel.
|
||||
logger = xlogtest.InfoLogger()
|
||||
cmd, err = CommandContext(context.Background(), logger, "echo", "Hello", "World")
|
||||
LogBeforeExecute(logger, cmd)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
17
internal/pkg/xlog/doc.go
Normal file
17
internal/pkg/xlog/doc.go
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
Package xlog defines a standard logger
|
||||
for the application.
|
||||
|
||||
It uses structured logging thanks to
|
||||
https://github.com/sirupsen/logrus.
|
||||
|
||||
All messages have at least two fields:
|
||||
|
||||
A "trace" field which helps to identify
|
||||
messages belonging to the same context.
|
||||
|
||||
An "op" field which helps to identify
|
||||
the logical operation associated
|
||||
with the message.
|
||||
*/
|
||||
package xlog
|
||||
141
internal/pkg/xlog/xlog.go
Normal file
141
internal/pkg/xlog/xlog.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package xlog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Level helps setting the severity
|
||||
// of the messages displayed.
|
||||
type Level string
|
||||
|
||||
const (
|
||||
// DebugLevel is the lowest level.
|
||||
DebugLevel Level = "DEBUG"
|
||||
// InfoLevel is the intermediate level.
|
||||
InfoLevel Level = "INFO"
|
||||
// ErrorLevel is the highest level.
|
||||
ErrorLevel Level = "ERROR"
|
||||
)
|
||||
|
||||
// Logger enforces specific log message formats.
|
||||
type Logger struct {
|
||||
entry *logrus.Entry
|
||||
level Level
|
||||
}
|
||||
|
||||
// New returns a xlog.Logger.
|
||||
func New(level Level, trace string) Logger {
|
||||
l := logrus.New()
|
||||
l.SetLevel(mustLogrusLevel(level))
|
||||
if !isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
l.SetFormatter(&logrus.JSONFormatter{})
|
||||
}
|
||||
return Logger{
|
||||
entry: l.WithField("trace", trace),
|
||||
level: level,
|
||||
}
|
||||
}
|
||||
|
||||
func mustLogrusLevel(level Level) logrus.Level {
|
||||
const op string = "xlog.mustLogrusLevel"
|
||||
switch level {
|
||||
case DebugLevel:
|
||||
return logrus.DebugLevel
|
||||
case InfoLevel:
|
||||
return logrus.InfoLevel
|
||||
case ErrorLevel:
|
||||
return logrus.ErrorLevel
|
||||
default:
|
||||
panic(fmt.Sprintf("%s: '%s' is not associated with any logrus.Level", op, level))
|
||||
}
|
||||
}
|
||||
|
||||
// Levels returns a slice of string
|
||||
// with all severities.
|
||||
func Levels() []string {
|
||||
return []string{
|
||||
string(DebugLevel),
|
||||
string(InfoLevel),
|
||||
string(ErrorLevel),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
MustParseLevel returns the Level corresponding
|
||||
to given string.
|
||||
|
||||
It panics if no correspondence.
|
||||
*/
|
||||
func MustParseLevel(level string) Level {
|
||||
const op string = "xlog.MustParseLevel"
|
||||
switch level {
|
||||
case string(DebugLevel):
|
||||
return DebugLevel
|
||||
case string(InfoLevel):
|
||||
return InfoLevel
|
||||
case string(ErrorLevel):
|
||||
return ErrorLevel
|
||||
default:
|
||||
panic(fmt.Sprintf("%s: '%s' is not one of '%v'", op, level, Levels()))
|
||||
}
|
||||
}
|
||||
|
||||
// Level returns the current Level.
|
||||
func (l Logger) Level() Level {
|
||||
return l.level
|
||||
}
|
||||
|
||||
// WithFields returns a new xlog.Logger with
|
||||
// given fields.
|
||||
func (l Logger) WithFields(fields map[string]interface{}) Logger {
|
||||
return Logger{
|
||||
entry: l.entry.WithFields(fields),
|
||||
level: l.level,
|
||||
}
|
||||
}
|
||||
|
||||
// DebugOp logs a debug message for given
|
||||
// logical operation.
|
||||
func (l Logger) DebugOp(op, message string) {
|
||||
l.entry.WithField("op", op).Debug(message)
|
||||
}
|
||||
|
||||
// DebugfOp logs a debug message for given
|
||||
// logical operation and format.
|
||||
func (l Logger) DebugfOp(op, format string, args ...interface{}) {
|
||||
l.entry.WithField("op", op).Debugf(format, args...)
|
||||
}
|
||||
|
||||
// InfoOp logs an info message for given
|
||||
// logical operation.
|
||||
func (l Logger) InfoOp(op, message string) {
|
||||
l.entry.WithField("op", op).Info(message)
|
||||
}
|
||||
|
||||
// InfofOp logs an info message for given
|
||||
// logical operation and format.
|
||||
func (l Logger) InfofOp(op, format string, args ...interface{}) {
|
||||
l.entry.WithField("op", op).Infof(format, args...)
|
||||
}
|
||||
|
||||
// ErrorOp logs an error for given
|
||||
// logical operation.
|
||||
func (l Logger) ErrorOp(op string, err error) {
|
||||
l.entry.WithField("op", op).Error(err.Error())
|
||||
}
|
||||
|
||||
// ErrorfOp logs an error message for given
|
||||
// logical operation and format.
|
||||
func (l Logger) ErrorfOp(op, format string, args ...interface{}) {
|
||||
l.entry.WithField("op", op).Errorf(format, args...)
|
||||
}
|
||||
|
||||
// FatalOp logs an error for given
|
||||
// logical operation and exit 1.
|
||||
func (l Logger) FatalOp(op string, err error) {
|
||||
l.entry.WithField("op", op).Fatal(err.Error())
|
||||
}
|
||||
3
internal/pkg/xrand/doc.go
Normal file
3
internal/pkg/xrand/doc.go
Normal file
@@ -0,0 +1,3 @@
|
||||
// Package xrand helps generating
|
||||
// random strings.
|
||||
package xrand
|
||||
@@ -1,4 +1,4 @@
|
||||
package random
|
||||
package xrand
|
||||
|
||||
import (
|
||||
"github.com/labstack/gommon/random"
|
||||
@@ -1,4 +1,4 @@
|
||||
package random
|
||||
package xrand
|
||||
|
||||
import (
|
||||
"testing"
|
||||
6
internal/pkg/xtime/doc.go
Normal file
6
internal/pkg/xtime/doc.go
Normal file
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
Package xtime helps generating
|
||||
time.Duration from seconds represented
|
||||
as float64.
|
||||
*/
|
||||
package xtime
|
||||
10
internal/pkg/xtime/xtime.go
Normal file
10
internal/pkg/xtime/xtime.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package xtime
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Duration creates a time.Duration from seconds.
|
||||
func Duration(seconds float64) time.Duration {
|
||||
return time.Duration(1000*seconds) * time.Millisecond
|
||||
}
|
||||
14
internal/pkg/xtime/xtime_test.go
Normal file
14
internal/pkg/xtime/xtime_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package xtime
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDuration(t *testing.T) {
|
||||
expected := time.Duration(1500) * time.Millisecond
|
||||
result := Duration(1.5)
|
||||
assert.Equal(t, expected.String(), result.String())
|
||||
}
|
||||
Reference in New Issue
Block a user