feat: add 7.x source code

This commit is contained in:
Julien Neuhart
2021-08-22 12:52:44 +02:00
parent e457155950
commit 0f5e8fd314
111 changed files with 31188 additions and 0 deletions

187
pkg/gotenberg/cmd.go Normal file
View File

@@ -0,0 +1,187 @@
package gotenberg
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os/exec"
"strings"
"syscall"
"go.uber.org/zap"
)
// Cmd wraps an exec.Cmd.
type Cmd struct {
ctx context.Context
logger *zap.Logger
process *exec.Cmd
}
// Command creates a Cmd without a context. It configures the internal
// exec.Cmd of Cmd so that we may kill its unix process and all its children
// without creating orphans.
//
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
func Command(logger *zap.Logger, binPath string, args ...string) Cmd {
cmd := exec.Command(binPath, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
return Cmd{
ctx: nil,
logger: logger.Named("cmd"),
process: cmd,
}
}
// CommandContext creates a Cmd with a context. It configures the internal
// exec.Cmd of Cmd so that we may kill its unix process and all its children
// without creating orphans.
//
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
func CommandContext(ctx context.Context, logger *zap.Logger, binPath string, args ...string) (Cmd, error) {
if ctx == nil {
return Cmd{}, errors.New("nil context")
}
cmd := exec.CommandContext(ctx, binPath, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
return Cmd{
ctx: ctx,
logger: logger.Named("cmd"),
process: cmd,
}, nil
}
// Start starts the command but does not wait for its completion.
func (cmd Cmd) Start() error {
err := cmd.pipeOutput()
if err != nil {
return fmt.Errorf("pipe unix process output: %w", err)
}
cmd.logger.Debug(fmt.Sprintf("start unix process: %s", strings.Join(cmd.process.Args, " ")))
err = cmd.process.Start()
if err != nil {
return fmt.Errorf("start unix process: %w", err)
}
return nil
}
// Exec executes the command and wait for its completion or until the context
// is done. In any case, it kills the unix process and all its children.
func (cmd Cmd) Exec() error {
if cmd.ctx == nil {
return errors.New("nil context")
}
err := cmd.Start()
if err != nil {
return fmt.Errorf("start command: %w", err)
}
errChan := make(chan error, 1)
go func() {
errChan <- cmd.process.Wait()
}()
select {
case err = <-errChan:
errProc := cmd.Kill()
if errProc != nil {
cmd.logger.Error(errProc.Error())
}
if err == nil {
return nil
}
return fmt.Errorf("unix process error: %w", err)
case <-cmd.ctx.Done():
errProc := cmd.Kill()
if errProc != nil {
cmd.logger.Error(errProc.Error())
}
return fmt.Errorf("context done: %w", cmd.ctx.Err())
}
}
// pipeOutput creates logs entries according to the process stdout and stderr.
// It does nothing if the logging level is not debug.
func (cmd Cmd) pipeOutput() error {
checkedEntry := cmd.logger.Check(zap.DebugLevel, "check for debug level before piping unix process output")
if checkedEntry == nil {
return nil
}
stdout, err := cmd.process.StdoutPipe()
if err != nil {
return fmt.Errorf("pipe unix process stdout: %w", err)
}
stderr, err := cmd.process.StderrPipe()
if err != nil {
return fmt.Errorf("unix process sdterr: %w", err)
}
// logCommandOutput creates logs entries according to a reader
// (either stdout or stderr).
logCommandOutput := func(logger *zap.Logger, reader io.ReadCloser) {
r := bufio.NewReader(reader)
defer reader.Close()
for {
line, _, err := r.ReadLine()
if err != nil {
if err != io.EOF && !strings.Contains(err.Error(), "file already closed") {
logger.Error(fmt.Sprintf("pipe unix process output error: %s", err))
}
break
}
if len(line) != 0 {
logger.Debug(string(line))
}
}
}
go logCommandOutput(cmd.logger.Named("stdout"), stdout)
go logCommandOutput(cmd.logger.Named("stderr"), stderr)
return nil
}
// Kill kills the unix process and all its children without creating orphans.
//
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
func (cmd Cmd) Kill() error {
if cmd.process == nil {
// We cannot use the logger here, because for whatever reason using it
// result to a panic.
// cmd.logger.Debug("no process, skip killing")
return nil
}
err := syscall.Kill(-cmd.process.Process.Pid, syscall.SIGKILL)
if err == nil {
cmd.logger.Debug("unix process killed")
return nil
}
// If the process does not exist anymore, the error is irrelevant.
if strings.Contains(err.Error(), "no such process") {
cmd.logger.Debug("unix process already killed")
return nil
}
return fmt.Errorf("kill unix process: %w", err)
}

220
pkg/gotenberg/cmd_test.go Normal file
View File

@@ -0,0 +1,220 @@
package gotenberg
import (
"context"
"testing"
"time"
"go.uber.org/zap"
)
func TestCommand(t *testing.T) {
cmd := Command(zap.NewNop(), "foo")
if !cmd.process.SysProcAttr.Setpgid {
t.Error("expected Setpgid to be true")
}
}
func TestCommandContext(t *testing.T) {
for i, tc := range []struct {
ctx context.Context
expectErr bool
}{
{
ctx: nil,
expectErr: true,
},
{
ctx: context.TODO(),
},
} {
cmd, err := CommandContext(tc.ctx, zap.NewNop(), "foo")
if err == nil && !cmd.process.SysProcAttr.Setpgid {
t.Fatalf("test %d: expected Setpgid to be true", i)
}
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
}
}
func TestCmd_Start(t *testing.T) {
for i, tc := range []struct {
cmd Cmd
expectErr bool
}{
{
cmd: Command(zap.NewNop(), "foo"),
expectErr: true,
},
{
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
},
} {
err := tc.cmd.Start()
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
}
}
func TestCmd_Exec(t *testing.T) {
for i, tc := range []struct {
cmd Cmd
timeout time.Duration
expectErr bool
}{
{
cmd: Command(zap.NewNop(), "foo"),
expectErr: true,
},
{
cmd: Command(zap.NewNop(), "foo"),
timeout: time.Duration(5) * time.Second,
expectErr: true,
},
{
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
timeout: time.Duration(5) * time.Second,
},
{
cmd: Command(zap.NewNop(), "sleep", "3"),
timeout: time.Duration(2) * time.Second,
expectErr: true,
},
} {
if tc.timeout > 0 {
ctx, cancel := context.WithTimeout(context.TODO(), tc.timeout)
defer cancel()
tc.cmd.ctx = ctx
}
err := tc.cmd.Exec()
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
}
}
func TestCmd_pipeOutput(t *testing.T) {
for i, tc := range []struct {
cmd Cmd
run bool
expectErr bool
}{
{
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
},
{
cmd: func() Cmd {
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
_, err := cmd.process.StdoutPipe()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return cmd
}(),
expectErr: true,
},
{
cmd: func() Cmd {
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
_, err := cmd.process.StderrPipe()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return cmd
}(),
expectErr: true,
},
{
cmd: Command(zap.NewExample(), "echo", "Hello", "World"),
run: true,
},
} {
err := tc.cmd.pipeOutput()
if tc.run {
errStart := tc.cmd.process.Start()
if errStart != nil {
t.Fatalf("test %d: expected no error but got: %v", i, err)
}
}
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
}
}
func TestCmd_Kill(t *testing.T) {
for i, tc := range []struct {
cmd Cmd
expectErr bool
}{
{
cmd: Cmd{logger: zap.NewNop()},
},
{
cmd: func() Cmd {
cmd := Command(zap.NewNop(), "sleep", "60")
err := cmd.process.Start()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return cmd
}(),
},
{
cmd: func() Cmd {
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
err := cmd.process.Run()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return cmd
}(),
},
} {
err := tc.cmd.Kill()
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
}
}

125
pkg/gotenberg/context.go Normal file
View File

@@ -0,0 +1,125 @@
package gotenberg
import (
"fmt"
"reflect"
)
// Context is a struct which helps initializing modules. When provisioning, a
// module may use the context to get other modules that it needs internally.
type Context struct {
flags ParsedFlags
descriptors []ModuleDescriptor
moduleInstances map[string]interface{}
}
// NewContext creates a Context.
// In a module, prefer the Provisioner interface to get a Context.
func NewContext(
flags ParsedFlags,
descriptors []ModuleDescriptor,
) *Context {
return &Context{
flags: flags,
descriptors: descriptors,
moduleInstances: make(map[string]interface{}),
}
}
// ParsedFlags returns the parsed flags.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// flags := ctx.ParsedFlags()
// m.foo = flags.RequiredString("foo")
// }
func (ctx Context) ParsedFlags() ParsedFlags {
return ctx.flags
}
// Module returns a module which satisfies the requested interface.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// mod, _ := ctx.Module(new(ModuleInterface))
// real := mod.(ModuleInterface)
// }
//
// If the module has not yet been initialized, this method
// initializes it. Otherwise, returns the already initialized instance.
func (ctx *Context) Module(kind interface{}) (interface{}, error) {
mods, err := ctx.Modules(kind)
if err != nil {
return nil, fmt.Errorf("get module: %w", err)
}
if len(mods) != 1 {
return nil, fmt.Errorf("expected to have one and only one %s module", kind)
}
return mods[0], nil
}
// Modules returns the list of modules which satisfies the requested interface.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// mods, _ := ctx.Modules(new(ModuleInterface))
// for _, mod := range mods {
// real := mod.(ModuleInterface)
// // ...
// }
// }
//
// If one or more modules have not yet been initialized, this method
// initializes them. Otherwise, returns the already initialized instances.
func (ctx *Context) Modules(kind interface{}) ([]interface{}, error) {
realKind := reflect.TypeOf(kind).Elem()
var mods []interface{}
for _, desc := range ctx.descriptors {
newInstance := desc.New()
if ok := reflect.TypeOf(newInstance).Implements(realKind); ok {
// The module implements the requested interface.
// We check if it has already been initialized.
instance, ok := ctx.moduleInstances[desc.ID]
if ok {
mods = append(mods, instance)
} else {
err := ctx.loadModule(desc.ID, newInstance)
if err != nil {
return nil, err
}
mods = append(mods, newInstance)
}
}
}
return mods, nil
}
// loadModule calls the Provision and/or Validate methods of the requested
// module if it satisfies the Provisioner and/or Validator interfaces.
func (ctx *Context) loadModule(id string, instance interface{}) error {
if prov, ok := instance.(Provisioner); ok {
// The instance can be provisioned.
err := prov.Provision(ctx)
if err != nil {
return fmt.Errorf("provision module %s: %w", id, err)
}
}
if validator, ok := instance.(Validator); ok {
// The instance can be validated.
err := validator.Validate()
if err != nil {
return fmt.Errorf("validate module %s: %w", id, err)
}
}
ctx.moduleInstances[id] = instance
return nil
}

View File

@@ -0,0 +1,195 @@
package gotenberg
import (
"errors"
"testing"
)
func TestNewContext(t *testing.T) {
if NewContext(ParsedFlags{}, nil) == nil {
t.Error("expected a non-nil value")
}
}
func TestContext_ParsedFlags(t *testing.T) {
ctx := NewContext(ParsedFlags{}, nil)
actual := ctx.ParsedFlags()
expect := ParsedFlags{}
if actual != expect {
t.Errorf("expected %v but got %v", expect, actual)
}
}
func TestContext_Module(t *testing.T) {
for i, tc := range []struct {
mods []ModuleDescriptor
kind interface{}
expectErr bool
}{
{
mods: func() []ModuleDescriptor {
mod := struct{ ProtoProvisioner }{}
mod.descriptor = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.provision = func(ctx *Context) error { return errors.New("foo") }
return []ModuleDescriptor{mod.Descriptor()}
}(),
kind: new(Provisioner),
expectErr: true,
},
{
mods: func() []ModuleDescriptor {
mod := struct{ ProtoProvisioner }{}
mod.descriptor = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.provision = func(ctx *Context) error { return nil }
return []ModuleDescriptor{mod.Descriptor(), mod.Descriptor()}
}(),
kind: new(Provisioner),
expectErr: true,
},
{
mods: func() []ModuleDescriptor {
mod := struct{ ProtoProvisioner }{}
mod.descriptor = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.provision = func(ctx *Context) error { return nil }
return []ModuleDescriptor{mod.Descriptor()}
}(),
kind: new(Provisioner),
},
} {
ctx := NewContext(ParsedFlags{}, tc.mods)
_, err := ctx.Module(tc.kind)
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
}
}
func TestContext_Modules(t *testing.T) {
for i, tc := range []struct {
mods []ModuleDescriptor
kind interface{}
expectErr bool
}{
{
mods: func() []ModuleDescriptor {
mod := struct{ ProtoProvisioner }{}
mod.descriptor = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.provision = func(ctx *Context) error { return errors.New("foo") }
return []ModuleDescriptor{mod.Descriptor()}
}(),
kind: new(Provisioner),
expectErr: true,
},
{
mods: func() []ModuleDescriptor {
mod := struct{ ProtoProvisioner }{}
mod.descriptor = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.provision = func(ctx *Context) error { return nil }
return []ModuleDescriptor{mod.Descriptor(), mod.Descriptor()}
}(),
kind: new(Provisioner),
},
{
mods: func() []ModuleDescriptor {
mod := struct{ ProtoProvisioner }{}
mod.descriptor = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.provision = func(ctx *Context) error { return nil }
return []ModuleDescriptor{mod.Descriptor()}
}(),
kind: new(Provisioner),
},
} {
ctx := NewContext(ParsedFlags{}, tc.mods)
_, err := ctx.Modules(tc.kind)
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
}
}
func TestContext_loadModule(t *testing.T) {
for i, tc := range []struct {
instance interface{}
expectErr bool
}{
{
instance: func() interface{} {
mod := struct{ ProtoProvisioner }{}
mod.descriptor = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.provision = func(ctx *Context) error { return errors.New("foo") }
return mod
}(),
expectErr: true,
},
{
instance: func() interface{} {
mod := struct{ ProtoValidator }{}
mod.descriptor = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.validate = func() error { return errors.New("foo") }
return mod
}(),
expectErr: true,
},
{
instance: func() interface{} {
mod := struct{ ProtoValidator }{}
mod.descriptor = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.validate = func() error { return nil }
return mod
}(),
},
} {
ctx := NewContext(ParsedFlags{}, nil)
err := ctx.loadModule("foo", tc.instance)
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
}
}
}

7
pkg/gotenberg/doc.go Normal file
View File

@@ -0,0 +1,7 @@
// Package gotenberg provides most of the logic of the module system.
//
// caddyserver/caddy, licensed under the Apache License 2.0, has significantly
// inspired this module system.
//
// More details are available on https://caddyserver.com/.
package gotenberg

109
pkg/gotenberg/flags.go Normal file
View File

@@ -0,0 +1,109 @@
package gotenberg
import (
"regexp"
"time"
"github.com/labstack/gommon/bytes"
flag "github.com/spf13/pflag"
)
// ParsedFlags wraps a flag.FlagSet so that retrieving the typed values is
// easier.
type ParsedFlags struct {
*flag.FlagSet
}
// MustString returns the string value of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustString(name string) string {
val, err := f.GetString(name)
if err != nil {
panic(err)
}
return val
}
// MustStringSlice returns the string slice value of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustStringSlice(name string) []string {
val, err := f.GetStringSlice(name)
if err != nil {
panic(err)
}
return val
}
// MustBool returns the boolean value of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustBool(name string) bool {
val, err := f.GetBool(name)
if err != nil {
panic(err)
}
return val
}
// MustInt returns the int value of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustInt(name string) int {
val, err := f.GetInt(name)
if err != nil {
panic(err)
}
return val
}
// MustFloat64 returns the float value of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustFloat64(name string) float64 {
val, err := f.GetFloat64(name)
if err != nil {
panic(err)
}
return val
}
// MustDuration returns the time.Duration value of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustDuration(name string) time.Duration {
val, err := f.GetDuration(name)
if err != nil {
panic(err)
}
return val
}
// MustHumanReadableBytesString returns the human-readable bytes string of a
// flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustHumanReadableBytesString(name string) string {
val, err := f.GetString(name)
if err != nil {
panic(err)
}
_, err = bytes.Parse(val)
if err != nil {
panic(err)
}
return val
}
// MustRegexp returns the regular expression of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustRegexp(name string) *regexp.Regexp {
val, err := f.GetString(name)
if err != nil {
panic(err)
}
return regexp.MustCompile(val)
}

378
pkg/gotenberg/flags_test.go Normal file
View File

@@ -0,0 +1,378 @@
package gotenberg
import (
"testing"
"time"
flag "github.com/spf13/pflag"
)
func TestParsedFlags_MustString(t *testing.T) {
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
fs.String("foo", "", "")
err := fs.Parse([]string{"--foo=foo"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
parsedFlags := ParsedFlags{FlagSet: fs}
for i, tc := range []struct {
name string
expectPanic bool
}{
{
name: "foo",
},
{
name: "bar",
expectPanic: true,
},
} {
func() {
if tc.expectPanic {
defer func() {
if r := recover(); r == nil {
t.Errorf("test %d: expected panic but got none", i)
}
}()
}
if !tc.expectPanic {
defer func() {
if r := recover(); r != nil {
t.Errorf("test %d: expected no panic but got: %v", i, r)
}
}()
}
parsedFlags.MustString(tc.name)
}()
}
}
func TestParsedFlags_MustStringSlice(t *testing.T) {
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
fs.StringSlice("foo", make([]string, 0), "")
err := fs.Parse([]string{"--foo=foo", "--foo=bar", "--foo=baz"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
parsedFlags := ParsedFlags{FlagSet: fs}
for i, tc := range []struct {
name string
expectPanic bool
}{
{
name: "foo",
},
{
name: "bar",
expectPanic: true,
},
} {
func() {
if tc.expectPanic {
defer func() {
if r := recover(); r == nil {
t.Errorf("test %d: expected panic but got none", i)
}
}()
}
if !tc.expectPanic {
defer func() {
if r := recover(); r != nil {
t.Errorf("test %d: expected no panic but got: %v", i, r)
}
}()
}
parsedFlags.MustStringSlice(tc.name)
}()
}
}
func TestParsedFlags_MustBool(t *testing.T) {
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
fs.Bool("foo", false, "")
err := fs.Parse([]string{"--foo=true"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
parsedFlags := ParsedFlags{FlagSet: fs}
for i, tc := range []struct {
name string
expectPanic bool
}{
{
name: "foo",
},
{
name: "bar",
expectPanic: true,
},
} {
func() {
if tc.expectPanic {
defer func() {
if r := recover(); r == nil {
t.Errorf("test %d: expected panic but got none", i)
}
}()
}
if !tc.expectPanic {
defer func() {
if r := recover(); r != nil {
t.Errorf("test %d: expected no panic but got: %v", i, r)
}
}()
}
parsedFlags.MustBool(tc.name)
}()
}
}
func TestParsedFlags_MustInt(t *testing.T) {
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
fs.Int("foo", 0, "")
err := fs.Parse([]string{"--foo=1"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
parsedFlags := ParsedFlags{FlagSet: fs}
for i, tc := range []struct {
name string
expectPanic bool
}{
{
name: "foo",
},
{
name: "bar",
expectPanic: true,
},
} {
func() {
if tc.expectPanic {
defer func() {
if r := recover(); r == nil {
t.Errorf("test %d: expected panic but got none", i)
}
}()
}
if !tc.expectPanic {
defer func() {
if r := recover(); r != nil {
t.Errorf("test %d: expected no panic but got: %v", i, r)
}
}()
}
parsedFlags.MustInt(tc.name)
}()
}
}
func TestParsedFlags_MustFloat64(t *testing.T) {
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
fs.Float64("foo", 1.0, "")
err := fs.Parse([]string{"--foo=2.0"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
parsedFlags := ParsedFlags{FlagSet: fs}
for i, tc := range []struct {
name string
expectPanic bool
}{
{
name: "foo",
},
{
name: "bar",
expectPanic: true,
},
} {
func() {
if tc.expectPanic {
defer func() {
if r := recover(); r == nil {
t.Errorf("test %d: expected panic but got none", i)
}
}()
}
if !tc.expectPanic {
defer func() {
if r := recover(); r != nil {
t.Errorf("test %d: expected no panic but got: %v", i, r)
}
}()
}
parsedFlags.MustFloat64(tc.name)
}()
}
}
func TestParsedFlags_MustDuration(t *testing.T) {
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
fs.Duration("foo", time.Duration(1)*time.Second, "")
err := fs.Parse([]string{"--foo=2m"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
parsedFlags := ParsedFlags{FlagSet: fs}
for i, tc := range []struct {
name string
expectPanic bool
}{
{
name: "foo",
},
{
name: "bar",
expectPanic: true,
},
} {
func() {
if tc.expectPanic {
defer func() {
if r := recover(); r == nil {
t.Errorf("test %d: expected panic but got none", i)
}
}()
}
if !tc.expectPanic {
defer func() {
if r := recover(); r != nil {
t.Errorf("test %d: expected no panic but got: %v", i, r)
}
}()
}
parsedFlags.MustDuration(tc.name)
}()
}
}
func TestParsedFlags_MustHumanReadableBytesString(t *testing.T) {
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
fs.String("foo", "1MB", "")
fs.String("bar", "1MB", "")
err := fs.Parse([]string{"--foo=1GB", "--bar=foo"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
parsedFlags := ParsedFlags{FlagSet: fs}
for i, tc := range []struct {
name string
expectPanic bool
}{
{
name: "foo",
},
{
name: "bar",
expectPanic: true,
},
{
name: "baz",
expectPanic: true,
},
} {
func() {
if tc.expectPanic {
defer func() {
if r := recover(); r == nil {
t.Errorf("test %d: expected panic but got none", i)
}
}()
}
if !tc.expectPanic {
defer func() {
if r := recover(); r != nil {
t.Errorf("test %d: expected no panic but got: %v", i, r)
}
}()
}
parsedFlags.MustHumanReadableBytesString(tc.name)
}()
}
}
func TestParsedFlags_MustRegexp(t *testing.T) {
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
fs.String("foo", "", "")
fs.String("bar", "", "")
err := fs.Parse([]string{"--foo=", "--bar=*"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
parsedFlags := ParsedFlags{FlagSet: fs}
for i, tc := range []struct {
name string
expectPanic bool
}{
{
name: "foo",
},
{
name: "bar",
expectPanic: true,
},
{
name: "baz",
expectPanic: true,
},
} {
func() {
if tc.expectPanic {
defer func() {
if r := recover(); r == nil {
t.Errorf("test %d: expected panic but got none", i)
}
}()
}
if !tc.expectPanic {
defer func() {
if r := recover(); r != nil {
t.Errorf("test %d: expected no panic but got: %v", i, r)
}
}()
}
parsedFlags.MustRegexp(tc.name)
}()
}
}

33
pkg/gotenberg/fs.go Normal file
View File

@@ -0,0 +1,33 @@
package gotenberg
import (
"fmt"
"os"
"github.com/google/uuid"
)
// TmpPath returns the default directory to use for temporary files and
// directories. Most if not all files and directories created by the
// application and its dependencies must be based on this default directory.
func TmpPath() string {
return os.TempDir()
}
// NewDirPath returns a random absolute path based on the temporary path.
func NewDirPath() string {
return fmt.Sprintf("%s/%s", TmpPath(), uuid.New())
}
// MkdirAll creates a random directory based on the temporary path and
// returns its absolute path.
func MkdirAll() (string, error) {
path := NewDirPath()
err := os.MkdirAll(path, 0755)
if err != nil {
return "", fmt.Errorf("create directory %s: %w", path, err)
}
return path, nil
}

59
pkg/gotenberg/fs_test.go Normal file
View File

@@ -0,0 +1,59 @@
package gotenberg
import (
"os"
"strings"
"testing"
)
func TestTmpPath(t *testing.T) {
osTempDir := os.TempDir()
tmpPath := TmpPath()
if tmpPath != osTempDir {
t.Errorf("expected path '%s' but got '%s'", osTempDir, tmpPath)
}
}
func TestNewDirPath(t *testing.T) {
newDirPath := NewDirPath()
tmpPath := TmpPath()
if !strings.HasPrefix(newDirPath, tmpPath) {
t.Fatalf("expected path '%s' to start with '%s'", newDirPath, tmpPath)
}
newDirPaths := make([]string, 1000)
for i := range newDirPaths {
newDirPaths[i] = NewDirPath()
}
for i, newDirPath := range newDirPaths {
for j, comparison := range newDirPaths {
if i == j {
continue
}
if newDirPath == comparison {
t.Fatalf("expected path '%s' (index %d) to be unique, but found an identical path on index %d", newDirPath, i, j)
}
}
}
}
func TestMkdirAll(t *testing.T) {
path, err := MkdirAll()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
tmpPath := TmpPath()
if !strings.HasPrefix(path, tmpPath) {
t.Fatalf("expected path '%s' to start with '%s'", path, tmpPath)
}
_, err = os.Stat(path)
if os.IsNotExist(err) {
t.Errorf("expected path '%s' to exist but got: %v", path, err)
}
}

14
pkg/gotenberg/logging.go Normal file
View File

@@ -0,0 +1,14 @@
package gotenberg
import "go.uber.org/zap"
// LoggerProvider is a module interface which exposes a method for creating a
// zap.Logger for other modules.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// provider, _ := ctx.Module(new(gotenberg.LoggerProvider))
// logger, _ := provider.(gotenberg.LoggerProvider).Logger(m)
// }
type LoggerProvider interface {
Logger(mod Module) (*zap.Logger, error)
}

135
pkg/gotenberg/modules.go Normal file
View File

@@ -0,0 +1,135 @@
package gotenberg
import (
"context"
"fmt"
"sort"
"sync"
flag "github.com/spf13/pflag"
)
// Module is a sort of plugin which adds new functionalities to the application
// or other modules.
//
// type YourModule struct {
// property string
// }
//
// func (YourModule) Descriptor() gotenberg.ModuleDescriptor {
// return gotenberg.ModuleDescriptor{
// ID: "your_module",
// FlagSet: func() *flag.FlagSet {
// fs := flag.NewFlagSet("your_module", flag.ExitOnError)
// fs.String("your_module-property", "default value", "flag description")
//
// return fs
// }(),
// New: func() gotenberg.Module { return new(YourModule) },
// }
// }
type Module interface {
Descriptor() ModuleDescriptor
}
// ModuleDescriptor describes your module for the application.
type ModuleDescriptor struct {
// ID is the unique name (snake case) of the module.
// Required.
ID string
// FlagSet is the definition of the flags of the module.
// Optional.
FlagSet *flag.FlagSet
// New returns a new and empty instance of the module's type.
// Required.
New func() Module
}
// Provisioner is a module interface for modules which have to be initialized
// according to flags, environment variables, the context, etc.
type Provisioner interface {
Provision(*Context) error
}
// Validator is a module interface for modules which have to be validated after
// provisioning.
type Validator interface {
Validate() error
}
// App is a module interface for modules which can be started or stopped by the
// application.
type App interface {
Start() error
// StartupMessage returns a custom message to display on startup. If it
// returns an empty string, a default startup message is used instead.
StartupMessage() string
Stop(ctx context.Context) error
}
// MustRegisterModule registers a module.
//
// To register a module, create an init() method in the module main go file:
//
// func init() {
// gotenberg.MustRegisterModule(YourModule{})
// }
//
// Then, in the main command (github.com/gotenberg/gotenberg/v7/cmd/gotenberg),
// import the module:
//
// imports (
// // Gotenberg modules.
// _ "your_module_path"
// )
func MustRegisterModule(mod Module) {
desc := mod.Descriptor()
if desc.ID == "" {
panic("module with an empty ID cannot be registered")
}
if desc.New == nil {
panic("module New function cannot be nil")
}
if val := desc.New(); val == nil {
panic("module New function cannot return a nil instance")
}
descriptorsMu.Lock()
defer descriptorsMu.Unlock()
if _, ok := descriptors[desc.ID]; ok {
panic(fmt.Sprintf("module %s is already registered", desc.ID))
}
descriptors[desc.ID] = desc
}
// GetModuleDescriptors returns the descriptors of all registered modules.
func GetModuleDescriptors() []ModuleDescriptor {
descriptorsMu.RLock()
defer descriptorsMu.RUnlock()
mods := make([]ModuleDescriptor, len(descriptors))
i := 0
for _, desc := range descriptors {
mods[i] = desc
i++
}
sort.Slice(mods, func(i, j int) bool {
return mods[i].ID < mods[j].ID
})
return mods
}
var (
descriptors = make(map[string]ModuleDescriptor)
descriptorsMu sync.RWMutex
)

View File

@@ -0,0 +1,135 @@
package gotenberg
import (
"reflect"
"testing"
)
type ProtoModule struct {
descriptor func() ModuleDescriptor
}
func (mod ProtoModule) Descriptor() ModuleDescriptor {
return mod.descriptor()
}
type ProtoProvisioner struct {
ProtoModule
provision func(ctx *Context) error
}
func (mod ProtoProvisioner) Provision(ctx *Context) error {
return mod.provision(ctx)
}
type ProtoValidator struct {
ProtoModule
validate func() error
}
func (mod ProtoValidator) Validate() error {
return mod.validate()
}
func TestMustRegisterModule(t *testing.T) {
descriptorsMu.RLock()
descriptors = map[string]ModuleDescriptor{
"a": {ID: "a"},
}
descriptorsMu.RUnlock()
for i, tc := range []struct {
ID string
New func() Module
expectPanic bool
}{
{
ID: "",
New: func() Module { return new(ProtoModule) },
expectPanic: true,
},
{
ID: "b",
New: nil,
expectPanic: true,
},
{
ID: "b",
New: func() Module { return nil },
expectPanic: true,
},
{
ID: "a",
New: func() Module { return new(ProtoModule) },
expectPanic: true,
},
{
ID: "b",
New: func() Module { return new(ProtoModule) },
},
} {
func() {
mod := struct{ ProtoModule }{}
mod.descriptor = func() ModuleDescriptor { return ModuleDescriptor{ID: tc.ID, New: tc.New} }
if tc.expectPanic {
defer func() {
if r := recover(); r == nil {
t.Errorf("test %d: expected panic but got none", i)
}
}()
}
if !tc.expectPanic {
defer func() {
if r := recover(); r != nil {
t.Errorf("test %d: expected no panic but got: %v", i, r)
}
}()
}
MustRegisterModule(mod)
}()
}
descriptorsMu.RLock()
descriptors = make(map[string]ModuleDescriptor)
descriptorsMu.RUnlock()
}
func TestGetModuleDescriptors(t *testing.T) {
descriptorsMu.RLock()
descriptors = map[string]ModuleDescriptor{
"d": {ID: "d"},
"c": {ID: "c"},
"b": {ID: "b"},
"a": {ID: "a"},
}
descriptorsMu.RUnlock()
expect := []ModuleDescriptor{
{ID: "a"},
{ID: "b"},
{ID: "c"},
{ID: "d"},
}
actual := GetModuleDescriptors()
if !reflect.DeepEqual(actual, expect) {
t.Errorf("expected %v but got %v", expect, actual)
}
descriptorsMu.RLock()
descriptors = make(map[string]ModuleDescriptor)
descriptorsMu.RUnlock()
}
// Interface guards.
var (
_ Module = (*ProtoModule)(nil)
_ Provisioner = (*ProtoProvisioner)(nil)
_ Module = (*ProtoProvisioner)(nil)
_ Validator = (*ProtoValidator)(nil)
_ Module = (*ProtoValidator)(nil)
)

View File

@@ -0,0 +1,52 @@
package gotenberg
import (
"context"
"errors"
"go.uber.org/zap"
)
var (
// ErrPDFEngineMethodNotAvailable happens if a PDFEngine method is not
// available in the implementation.
ErrPDFEngineMethodNotAvailable = errors.New("method not available")
// ErrPDFFormatNotAvailable happens if a PDFEngine Convert's method does
// not handle a specific format.
ErrPDFFormatNotAvailable = errors.New("PDF format not available")
)
const (
FormatPDFA1a string = "PDF/A-1a"
FormatPDFA1b string = "PDF/A-1b"
FormatPDFA2a string = "PDF/A-2a"
FormatPDFA2b string = "PDF/A-2b"
FormatPDFA2u string = "PDF/A-2u"
FormatPDFA3a string = "PDF/A-3a"
FormatPDFA3b string = "PDF/A-3b"
FormatPDFA3u string = "PDF/A-3u"
)
// PDFEngine is a module interface which exposes methods for manipulating one
// or more PDFs. Implementations may abstract powerful tools like PDFtk, or
// fulfill those methods contracts in Golang directly.
type PDFEngine interface {
// Merge merges the given PDFs into a unique PDF. The pages' order reflects
// order of the given files.
Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
// Convert converts the given PDF to a specific PDF format.
Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
}
// PDFEngineProvider is a module interface which exposes a method for creating a
// PDFEngine for other modules.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// provider, _ := ctx.Module(new(gotenberg.PDFEngineProvider))
// pdfengines, _ := provider.(gotenberg.PDFEngineProvider).PDFEngine()
// }
type PDFEngineProvider interface {
PDFEngine() (PDFEngine, error)
}