mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-13 10:52:15 +01:00
feat: add 7.x source code
This commit is contained in:
187
pkg/gotenberg/cmd.go
Normal file
187
pkg/gotenberg/cmd.go
Normal 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
220
pkg/gotenberg/cmd_test.go
Normal 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
125
pkg/gotenberg/context.go
Normal 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
|
||||
}
|
||||
195
pkg/gotenberg/context_test.go
Normal file
195
pkg/gotenberg/context_test.go
Normal 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
7
pkg/gotenberg/doc.go
Normal 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
109
pkg/gotenberg/flags.go
Normal 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
378
pkg/gotenberg/flags_test.go
Normal 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
33
pkg/gotenberg/fs.go
Normal 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
59
pkg/gotenberg/fs_test.go
Normal 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
14
pkg/gotenberg/logging.go
Normal 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
135
pkg/gotenberg/modules.go
Normal 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
|
||||
)
|
||||
135
pkg/gotenberg/modules_test.go
Normal file
135
pkg/gotenberg/modules_test.go
Normal 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)
|
||||
)
|
||||
52
pkg/gotenberg/pdfengine.go
Normal file
52
pkg/gotenberg/pdfengine.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user