mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-17 04:32:15 +01:00
feat: add more PDF formats, drastically improve LibreOffice long-running instance management
This commit is contained in:
6
Makefile
6
Makefile
@@ -51,7 +51,8 @@ PROMETHEUS_NAMESPACE=gotenberg
|
|||||||
PROMETHEUS_COLLECT_INTERVAL=1s
|
PROMETHEUS_COLLECT_INTERVAL=1s
|
||||||
PROMETHEUS_DISABLE_ROUTE_LOGGING=false
|
PROMETHEUS_DISABLE_ROUTE_LOGGING=false
|
||||||
PROMETHEUS_DISABLE_COLLECT=false
|
PROMETHEUS_DISABLE_COLLECT=false
|
||||||
UNOCONV_DISABLE_LISTENER=false
|
UNO_LISTENER_START_TIMEOUT=10s
|
||||||
|
UNO_LISTENER_RESTART_THRESHOLD=10
|
||||||
WEBHOOK_ALLOW_LIST=
|
WEBHOOK_ALLOW_LIST=
|
||||||
WEBHOOK_DENY_LIST=
|
WEBHOOK_DENY_LIST=
|
||||||
WEBHOOK_ERROR_ALLOW_LIST=
|
WEBHOOK_ERROR_ALLOW_LIST=
|
||||||
@@ -93,7 +94,8 @@ run: ## Start a Gotenberg container
|
|||||||
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
|
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
|
||||||
--prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \
|
--prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \
|
||||||
--prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \
|
--prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \
|
||||||
--unoconv-disable-listener=$(UNOCONV_DISABLE_LISTENER) \
|
--uno-listener-start-timeout=$(UNO_LISTENER_START_TIMEOUT) \
|
||||||
|
--uno-listener-restart-threshold=$(UNO_LISTENER_RESTART_THRESHOLD) \
|
||||||
--webhook-allow-list=$(WEBHOOK_ALLOW_LIST) \
|
--webhook-allow-list=$(WEBHOOK_ALLOW_LIST) \
|
||||||
--webhook-deny-list=$(WEBHOOK_DENY_LIST) \
|
--webhook-deny-list=$(WEBHOOK_DENY_LIST) \
|
||||||
--webhook-error-allow-list=$(WEBHOOK_ERROR_ALLOW_LIST) \
|
--webhook-error-allow-list=$(WEBHOOK_ERROR_ALLOW_LIST) \
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ COPY --from=builder /home/gotenberg /usr/bin/
|
|||||||
ENV GC_EXCLUDE_SUBSTR "hsperfdata_root,hsperfdata_gotenberg"
|
ENV GC_EXCLUDE_SUBSTR "hsperfdata_root,hsperfdata_gotenberg"
|
||||||
ENV CHROMIUM_BIN_PATH /usr/bin/chromium
|
ENV CHROMIUM_BIN_PATH /usr/bin/chromium
|
||||||
ENV UNOCONV_BIN_PATH /usr/bin/unoconv
|
ENV UNOCONV_BIN_PATH /usr/bin/unoconv
|
||||||
|
ENV LIBREOFFICE_BIN_PATH /usr/lib/libreoffice/program/soffice.bin
|
||||||
ENV PDFTK_BIN_PATH /usr/bin/pdftk
|
ENV PDFTK_BIN_PATH /usr/bin/pdftk
|
||||||
ENV QPDF_BIN_PATH /usr/bin/qpdf
|
ENV QPDF_BIN_PATH /usr/bin/qpdf
|
||||||
|
|
||||||
|
|||||||
@@ -73,22 +73,37 @@ func (cmd Cmd) Start() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait waits for the command to complete. It should be called when using the
|
||||||
|
// Start method, so that the command does not leak zombies.
|
||||||
|
func (cmd Cmd) Wait() error {
|
||||||
|
err := cmd.process.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wait for unix process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Exec executes the command and wait for its completion or until the context
|
// 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.
|
// is done. In any case, it kills the unix process and all its children.
|
||||||
func (cmd Cmd) Exec() error {
|
func (cmd Cmd) Exec() (int, error) {
|
||||||
if cmd.ctx == nil {
|
if cmd.ctx == nil {
|
||||||
return errors.New("nil context")
|
return 10, errors.New("nil context")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := cmd.Start()
|
err := cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("start command: %w", err)
|
if cmd.process.ProcessState == nil {
|
||||||
|
return 131, fmt.Errorf("start command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.process.ProcessState.ExitCode(), fmt.Errorf("start command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
errChan := make(chan error, 1)
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
errChan <- cmd.process.Wait()
|
errChan <- cmd.Wait()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -99,17 +114,21 @@ func (cmd Cmd) Exec() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("unix process error: %w", err)
|
if cmd.process.ProcessState == nil {
|
||||||
|
return 131, fmt.Errorf("unix process error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.process.ProcessState.ExitCode(), fmt.Errorf("unix process error: %w", err)
|
||||||
case <-cmd.ctx.Done():
|
case <-cmd.ctx.Done():
|
||||||
errProc := cmd.Kill()
|
errProc := cmd.Kill()
|
||||||
if errProc != nil {
|
if errProc != nil {
|
||||||
cmd.logger.Error(errProc.Error())
|
cmd.logger.Error(errProc.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("context done: %w", cmd.ctx.Err())
|
return 62, fmt.Errorf("context done: %w", cmd.ctx.Err())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,209 +12,313 @@ func TestCommand(t *testing.T) {
|
|||||||
cmd := Command(zap.NewNop(), "foo")
|
cmd := Command(zap.NewNop(), "foo")
|
||||||
|
|
||||||
if !cmd.process.SysProcAttr.Setpgid {
|
if !cmd.process.SysProcAttr.Setpgid {
|
||||||
t.Error("expected Setpgid to be true")
|
t.Error("expected cmd.process.SysProcAttr.Setpgid to be true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCommandContext(t *testing.T) {
|
func TestCommandContext(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
ctx context.Context
|
name string
|
||||||
expectErr bool
|
ctx context.Context
|
||||||
|
expectCommandContextErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
ctx: nil,
|
name: "nominal behavior",
|
||||||
expectErr: true,
|
ctx: context.Background(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
name: "nil context",
|
||||||
|
expectCommandContextErr: true,
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
cmd, err := CommandContext(tc.ctx, zap.NewNop(), "foo")
|
|
||||||
|
|
||||||
if err == nil && !cmd.process.SysProcAttr.Setpgid {
|
for _, tc := range tests {
|
||||||
t.Fatalf("test %d: expected Setpgid to be true", i)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
}
|
cmd, err := CommandContext(tc.ctx, zap.NewNop(), "foo")
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if err == nil && !cmd.process.SysProcAttr.Setpgid {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Fatal("expected cmd.process.SysProcAttr.Setpgid to be true")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectCommandContextErr && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Error("expected error from CommandContext(), but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !tc.expectCommandContextErr && err != nil {
|
||||||
|
t.Errorf("expected no error from CommandContext(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmd_Start(t *testing.T) {
|
func TestCmd_Start(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
cmd Cmd
|
name string
|
||||||
expectErr bool
|
cmd Cmd
|
||||||
|
expectStartErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "foo"),
|
name: "nominal behavior",
|
||||||
expectErr: true,
|
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
name: "start error",
|
||||||
|
cmd: Command(zap.NewNop(), "foo"),
|
||||||
|
expectStartErr: true,
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
err := tc.cmd.Start()
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
for _, tc := range tests {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
}
|
err := tc.cmd.Start()
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectStartErr && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Error("expected error from cmd.Start(), but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !tc.expectStartErr && err != nil {
|
||||||
|
t.Errorf("expected no error from cmd.Start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmd_Wait(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cmd Cmd
|
||||||
|
expectWaitErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
|
||||||
|
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from cmd.Start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wait error",
|
||||||
|
cmd: func() Cmd {
|
||||||
|
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
|
||||||
|
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from cmd.Start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmd.Kill()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from cmd.Kill(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
expectWaitErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.cmd.Wait()
|
||||||
|
|
||||||
|
if tc.expectWaitErr && err == nil {
|
||||||
|
t.Error("expected error from cmd.Wait(), but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectWaitErr && err != nil {
|
||||||
|
t.Errorf("expected no error from cmd.Wait(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmd_Exec(t *testing.T) {
|
func TestCmd_Exec(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
cmd Cmd
|
name string
|
||||||
timeout time.Duration
|
cmd Cmd
|
||||||
expectErr bool
|
timeout time.Duration
|
||||||
|
expectExecErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "foo"),
|
name: "nominal behavior",
|
||||||
expectErr: true,
|
cmd: func() Cmd {
|
||||||
|
cmd, err := CommandContext(context.Background(), zap.NewNop(), "echo", "Hello", "World")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from CommandContext(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "foo"),
|
name: "nil context",
|
||||||
timeout: time.Duration(5) * time.Second,
|
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||||
expectErr: true,
|
expectExecErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
name: "start error",
|
||||||
timeout: time.Duration(5) * time.Second,
|
cmd: func() Cmd {
|
||||||
|
cmd, err := CommandContext(context.Background(), zap.NewNop(), "foo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from CommandContext(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}(),
|
||||||
|
expectExecErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "sleep", "3"),
|
name: "context done",
|
||||||
timeout: time.Duration(2) * time.Second,
|
cmd: Command(zap.NewNop(), "sleep", "2"),
|
||||||
expectErr: true,
|
timeout: time.Duration(1) * time.Second,
|
||||||
|
expectExecErr: true,
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
if tc.timeout > 0 {
|
|
||||||
ctx, cancel := context.WithTimeout(context.TODO(), tc.timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tc.cmd.ctx = ctx
|
for _, tc := range tests {
|
||||||
}
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.timeout > 0 {
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), tc.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
err := tc.cmd.Exec()
|
tc.cmd.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
_, err := tc.cmd.Exec()
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectExecErr && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Error("expected error from cmd.Exec(), but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !tc.expectExecErr && err != nil {
|
||||||
|
t.Errorf("expected no error from cmd.Exec(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmd_pipeOutput(t *testing.T) {
|
func TestCmd_pipeOutput(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
cmd Cmd
|
name string
|
||||||
run bool
|
cmd Cmd
|
||||||
expectErr bool
|
run bool
|
||||||
|
expectPipeOutputErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
name: "nominal behavior",
|
||||||
|
cmd: Command(zap.NewExample(), "echo", "Hello", "World"),
|
||||||
|
run: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "no debug, no pipe",
|
||||||
|
cmd: Command(zap.NewNop(), "echo", "Hello", "World"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stdout already piped",
|
||||||
cmd: func() Cmd {
|
cmd: func() Cmd {
|
||||||
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
||||||
|
|
||||||
_, err := cmd.process.StdoutPipe()
|
_, err := cmd.process.StdoutPipe()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error from cmd.process.StdoutPipe(), but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectPipeOutputErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "stderr already piped",
|
||||||
cmd: func() Cmd {
|
cmd: func() Cmd {
|
||||||
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
cmd := Command(zap.NewExample(), "echo", "Hello", "World")
|
||||||
_, err := cmd.process.StderrPipe()
|
|
||||||
|
|
||||||
|
_, err := cmd.process.StderrPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error from cmd.process.StderrPipe(), but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectPipeOutputErr: true,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
cmd: Command(zap.NewExample(), "echo", "Hello", "World"),
|
|
||||||
run: true,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
err := tc.cmd.pipeOutput()
|
|
||||||
|
|
||||||
if tc.run {
|
for _, tc := range tests {
|
||||||
errStart := tc.cmd.process.Start()
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.cmd.pipeOutput()
|
||||||
|
|
||||||
if errStart != nil {
|
if tc.run {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
errStart := tc.cmd.process.Start()
|
||||||
|
if errStart != nil {
|
||||||
|
t.Fatalf("expected no error from tc.cmd.process.Start(), but got: %v", errStart)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectPipeOutputErr && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Error("expected error from cmd.pipeOutput(), but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if !tc.expectPipeOutputErr && err != nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Errorf("expected no error from cmd.pipeOutput(), but got: %v", err)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmd_Kill(t *testing.T) {
|
func TestCmd_Kill(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
cmd Cmd
|
name string
|
||||||
expectErr bool
|
cmd Cmd
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
cmd: Cmd{logger: zap.NewNop()},
|
name: "nominal behavior",
|
||||||
},
|
|
||||||
{
|
|
||||||
cmd: func() Cmd {
|
cmd: func() Cmd {
|
||||||
cmd := Command(zap.NewNop(), "sleep", "60")
|
cmd := Command(zap.NewNop(), "sleep", "60")
|
||||||
err := cmd.process.Start()
|
|
||||||
|
|
||||||
|
err := cmd.process.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error from cmd.process.Start(), but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "no process",
|
||||||
|
cmd: Cmd{logger: zap.NewNop()},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "process already killed",
|
||||||
cmd: func() Cmd {
|
cmd: func() Cmd {
|
||||||
cmd := Command(zap.NewNop(), "echo", "Hello", "World")
|
cmd := Command(zap.NewNop(), "sleep", "60")
|
||||||
err := cmd.process.Run()
|
|
||||||
|
|
||||||
|
err := cmd.process.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error from cmd.process.Start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cmd.Kill()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from cmd.Kill(), but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
err := tc.cmd.Kill()
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
for _, tc := range tests {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
}
|
err := tc.cmd.Kill()
|
||||||
|
if err != nil {
|
||||||
if !tc.expectErr && err != nil {
|
t.Errorf("expected no error from cmd.Kill(), but got: %v", err)
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
pkg/gotenberg/mocks.go
Normal file
66
pkg/gotenberg/mocks.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ModuleMock is a mock for the Module interface.
|
||||||
|
type ModuleMock struct {
|
||||||
|
DescriptorMock func() ModuleDescriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod ModuleMock) Descriptor() ModuleDescriptor {
|
||||||
|
return mod.DescriptorMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatorMock is a mock for the Validator interface.
|
||||||
|
type ValidatorMock struct {
|
||||||
|
ValidateMock func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mod ValidatorMock) Validate() error {
|
||||||
|
return mod.ValidateMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDFEngineMock is a mock for the PDFEngine interface.
|
||||||
|
type PDFEngineMock struct {
|
||||||
|
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||||
|
ConvertMock func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (engine PDFEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return engine.MergeMock(ctx, logger, inputPaths, outputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (engine PDFEngineMock) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
|
return engine.ConvertMock(ctx, logger, format, inputPath, outputPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDFEngineProviderMock is a mock for the PDFEngineProvider interface.
|
||||||
|
type PDFEngineProviderMock struct {
|
||||||
|
PDFEngineMock func() (PDFEngine, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider PDFEngineProviderMock) PDFEngine() (PDFEngine, error) {
|
||||||
|
return provider.PDFEngineMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggerProviderMock is a mock for the LoggerProvider interface.
|
||||||
|
type LoggerProviderMock struct {
|
||||||
|
LoggerMock func(mod Module) (*zap.Logger, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider LoggerProviderMock) Logger(mod Module) (*zap.Logger, error) {
|
||||||
|
return provider.LoggerMock(mod)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ Module = (*ModuleMock)(nil)
|
||||||
|
_ Validator = (*ValidatorMock)(nil)
|
||||||
|
_ PDFEngine = (*PDFEngineMock)(nil)
|
||||||
|
_ PDFEngineProvider = (*PDFEngineProviderMock)(nil)
|
||||||
|
_ LoggerProvider = (*LoggerProviderMock)(nil)
|
||||||
|
)
|
||||||
82
pkg/gotenberg/mocks_test.go
Normal file
82
pkg/gotenberg/mocks_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package gotenberg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestModuleMock(t *testing.T) {
|
||||||
|
mock := ModuleMock{
|
||||||
|
DescriptorMock: func() ModuleDescriptor {
|
||||||
|
return ModuleDescriptor{ID: "foo", New: func() Module {
|
||||||
|
return nil
|
||||||
|
}}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if mock.Descriptor().ID != "foo" {
|
||||||
|
t.Errorf("expected ID '%s' from mock.Descriptor(), but got '%s'", "foo", mock.Descriptor().ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatorMock(t *testing.T) {
|
||||||
|
mock := ValidatorMock{
|
||||||
|
ValidateMock: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mock.Validate()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from mock.Validate(), but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPDFEngineMock(t *testing.T) {
|
||||||
|
mock := PDFEngineMock{
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from mock.Merge(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mock.Convert(context.Background(), zap.NewNop(), "", "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from mock.Convert(), but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPDFEngineProvider(t *testing.T) {
|
||||||
|
mock := PDFEngineProviderMock{
|
||||||
|
PDFEngineMock: func() (PDFEngine, error) {
|
||||||
|
return PDFEngineMock{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mock.PDFEngine()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from mock.PDFEngine(), but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerProviderMock(t *testing.T) {
|
||||||
|
mock := LoggerProviderMock{
|
||||||
|
LoggerMock: func(mod Module) (*zap.Logger, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mock.Logger(ModuleMock{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from mock.Logger(), but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -277,6 +277,8 @@ func (ctx Context) OutputFilename(outputPath string) string {
|
|||||||
return fmt.Sprintf("%s%s", filename, filepath.Ext(outputPath))
|
return fmt.Sprintf("%s%s", filename, filepath.Ext(outputPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: move MockContext to mocks.go (and rename it ContextMock).
|
||||||
|
|
||||||
// MockContext is a helper for tests.
|
// MockContext is a helper for tests.
|
||||||
//
|
//
|
||||||
// ctx := &api.MockContext{Context: &api.Context{}}
|
// ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
// Package libreoffice provides a module which adds a route for converting
|
// Package libreoffice provides a module which adds a route for converting
|
||||||
// document to PDF with LibreOffice.
|
// documents to PDF with LibreOffice.
|
||||||
package libreoffice
|
package libreoffice
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ func init() {
|
|||||||
// LibreOffice is a module which provides a route for converting documents to
|
// LibreOffice is a module which provides a route for converting documents to
|
||||||
// PDF with LibreOffice.
|
// PDF with LibreOffice.
|
||||||
type LibreOffice struct {
|
type LibreOffice struct {
|
||||||
unoconv unoconv.API
|
unoAPI uno.API
|
||||||
engine gotenberg.PDFEngine
|
engine gotenberg.PDFEngine
|
||||||
disableRoutes bool
|
disableRoutes bool
|
||||||
}
|
}
|
||||||
@@ -40,17 +40,17 @@ func (mod *LibreOffice) Provision(ctx *gotenberg.Context) error {
|
|||||||
flags := ctx.ParsedFlags()
|
flags := ctx.ParsedFlags()
|
||||||
mod.disableRoutes = flags.MustBool("libreoffice-disable-routes")
|
mod.disableRoutes = flags.MustBool("libreoffice-disable-routes")
|
||||||
|
|
||||||
provider, err := ctx.Module(new(unoconv.Provider))
|
provider, err := ctx.Module(new(uno.Provider))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get unoconv provider: %w", err)
|
return fmt.Errorf("get unoAPI provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uno, err := provider.(unoconv.Provider).Unoconv()
|
unoAPI, err := provider.(uno.Provider).UNO()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get unoconv API: %w", err)
|
return fmt.Errorf("get unoAPI API: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mod.unoconv = uno
|
mod.unoAPI = unoAPI
|
||||||
|
|
||||||
provider, err = ctx.Module(new(gotenberg.PDFEngineProvider))
|
provider, err = ctx.Module(new(gotenberg.PDFEngineProvider))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -74,7 +74,7 @@ func (mod LibreOffice) Routes() ([]api.Route, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return []api.Route{
|
return []api.Route{
|
||||||
convertRoute(mod.unoconv, mod.engine),
|
convertRoute(mod.unoAPI, mod.engine),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +1,14 @@
|
|||||||
package libreoffice
|
package libreoffice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProtoModule struct {
|
|
||||||
descriptor func() gotenberg.ModuleDescriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return mod.descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoUnoconvProvider struct {
|
|
||||||
ProtoModule
|
|
||||||
unoconv func() (unoconv.API, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoUnoconvProvider) Unoconv() (unoconv.API, error) {
|
|
||||||
return mod.unoconv()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoUnoconvAPI struct {
|
|
||||||
pdf func(_ context.Context, _ *zap.Logger, _, _ string, _ unoconv.Options) error
|
|
||||||
extensions func() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoUnoconvAPI) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options unoconv.Options) error {
|
|
||||||
return mod.pdf(ctx, logger, inputPath, outputPath, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoUnoconvAPI) Extensions() []string {
|
|
||||||
return mod.extensions()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoPDFEngineProvider struct {
|
|
||||||
ProtoModule
|
|
||||||
pdfEngine func() (gotenberg.PDFEngine, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoPDFEngineProvider) PDFEngine() (gotenberg.PDFEngine, error) {
|
|
||||||
return mod.pdfEngine()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoPDFEngine struct {
|
|
||||||
merge func(_ context.Context, _ *zap.Logger, _ []string, _ string) error
|
|
||||||
convert func(_ context.Context, _ *zap.Logger, _, _, _ string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoPDFEngine) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return mod.merge(ctx, logger, inputPaths, outputPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoPDFEngine) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
|
||||||
return mod.convert(ctx, logger, format, inputPath, outputPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLibreOffice_Descriptor(t *testing.T) {
|
func TestLibreOffice_Descriptor(t *testing.T) {
|
||||||
descriptor := LibreOffice{}.Descriptor()
|
descriptor := LibreOffice{}.Descriptor()
|
||||||
|
|
||||||
@@ -75,15 +21,38 @@ func TestLibreOffice_Descriptor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLibreOffice_Provision(t *testing.T) {
|
func TestLibreOffice_Provision(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
ctx *gotenberg.Context
|
name string
|
||||||
expectErr bool
|
ctx *gotenberg.Context
|
||||||
|
expectProvisionErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod := struct{ ProtoModule }{}
|
provider1 := struct {
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
uno.ProviderMock
|
||||||
|
}{}
|
||||||
|
provider1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider1
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
provider1.UNOMock = func() (uno.API, error) {
|
||||||
|
return uno.APIMock{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
provider2 := struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.PDFEngineProviderMock
|
||||||
|
}{}
|
||||||
|
provider2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module {
|
||||||
|
return provider2
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
provider2.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
|
||||||
|
return gotenberg.PDFEngineMock{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
@@ -91,41 +60,36 @@ func TestLibreOffice_Provision(t *testing.T) {
|
|||||||
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
mod.Descriptor(),
|
provider1.Descriptor(),
|
||||||
|
provider2.Descriptor(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: func() *gotenberg.Context {
|
name: "no UNO API provider",
|
||||||
mod := struct{ ProtoUnoconvProvider }{}
|
ctx: gotenberg.NewContext(
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ParsedFlags{
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
||||||
}
|
},
|
||||||
mod.unoconv = func() (unoconv.API, error) {
|
[]gotenberg.ModuleDescriptor{},
|
||||||
return nil, errors.New("foo")
|
),
|
||||||
}
|
expectProvisionErr: true,
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
|
||||||
gotenberg.ParsedFlags{
|
|
||||||
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
|
||||||
},
|
|
||||||
[]gotenberg.ModuleDescriptor{
|
|
||||||
mod.Descriptor(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "no API from UNO API provider",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod := struct{ ProtoUnoconvProvider }{}
|
provider := struct {
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
uno.ProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
mod.unoconv = func() (unoconv.API, error) {
|
provider.UNOMock = func() (uno.API, error) {
|
||||||
return struct{ ProtoUnoconvAPI }{}, nil
|
return uno.APIMock{}, errors.New("foo")
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
@@ -133,28 +97,26 @@ func TestLibreOffice_Provision(t *testing.T) {
|
|||||||
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
mod.Descriptor(),
|
provider.Descriptor(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectProvisionErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "no PDF engine provider",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod1 := struct{ ProtoUnoconvProvider }{}
|
provider := struct {
|
||||||
mod1.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }}
|
uno.ProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
mod1.unoconv = func() (unoconv.API, error) {
|
provider.UNOMock = func() (uno.API, error) {
|
||||||
return struct{ ProtoUnoconvAPI }{}, nil
|
return uno.APIMock{}, nil
|
||||||
}
|
|
||||||
|
|
||||||
mod2 := struct{ ProtoPDFEngineProvider }{}
|
|
||||||
mod2.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }}
|
|
||||||
}
|
|
||||||
mod2.pdfEngine = func() (gotenberg.PDFEngine, error) {
|
|
||||||
return nil, errors.New("foo")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
@@ -162,29 +124,39 @@ func TestLibreOffice_Provision(t *testing.T) {
|
|||||||
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
mod1.Descriptor(),
|
provider.Descriptor(),
|
||||||
mod2.Descriptor(),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectProvisionErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "no PDF engine from PDF engine provider",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod1 := struct{ ProtoUnoconvProvider }{}
|
provider1 := struct {
|
||||||
mod1.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod1 }}
|
uno.ProviderMock
|
||||||
|
}{}
|
||||||
|
provider1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider1
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
mod1.unoconv = func() (unoconv.API, error) {
|
provider1.UNOMock = func() (uno.API, error) {
|
||||||
return struct{ ProtoUnoconvAPI }{}, nil
|
return uno.APIMock{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
mod2 := struct{ ProtoPDFEngineProvider }{}
|
provider2 := struct {
|
||||||
mod2.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod2 }}
|
gotenberg.PDFEngineProviderMock
|
||||||
|
}{}
|
||||||
|
provider2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module {
|
||||||
|
return provider2
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
mod2.pdfEngine = func() (gotenberg.PDFEngine, error) {
|
provider2.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
|
||||||
return struct{ ProtoPDFEngine }{}, nil
|
return gotenberg.PDFEngineMock{}, errors.New("foo")
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
@@ -192,59 +164,60 @@ func TestLibreOffice_Provision(t *testing.T) {
|
|||||||
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
FlagSet: new(LibreOffice).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
mod1.Descriptor(),
|
provider1.Descriptor(),
|
||||||
mod2.Descriptor(),
|
provider2.Descriptor(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
|
expectProvisionErr: true,
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
mod := new(LibreOffice)
|
|
||||||
err := mod.Provision(tc.ctx)
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
for _, tc := range tests {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
}
|
mod := new(LibreOffice)
|
||||||
|
err := mod.Provision(tc.ctx)
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectProvisionErr && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Error("expected mod.Provision() error, but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !tc.expectProvisionErr && err != nil {
|
||||||
|
t.Errorf("expected no error from mod.Provision(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLibreOffice_Routes(t *testing.T) {
|
func TestLibreOffice_Routes(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
expectRoutes int
|
name string
|
||||||
disableRoutes bool
|
mod LibreOffice
|
||||||
|
expectRoutesCount int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
expectRoutes: 1,
|
name: "route not disabled",
|
||||||
|
mod: LibreOffice{},
|
||||||
|
expectRoutesCount: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
disableRoutes: true,
|
name: "route disabled",
|
||||||
|
mod: LibreOffice{
|
||||||
|
disableRoutes: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
mod := new(LibreOffice)
|
|
||||||
mod.disableRoutes = tc.disableRoutes
|
|
||||||
|
|
||||||
routes, err := mod.Routes()
|
for _, tc := range tests {
|
||||||
if err != nil {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
routes, err := tc.mod.Routes()
|
||||||
}
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from mod.Routes(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if tc.expectRoutes != len(routes) {
|
if tc.expectRoutesCount != len(routes) {
|
||||||
t.Errorf("test %d: expected %d routes but got %d", i, tc.expectRoutes, len(routes))
|
t.Errorf("expected %d routes from mod.Routes(), but got %d", tc.expectRoutesCount, len(routes))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*ProtoModule)(nil)
|
|
||||||
_ unoconv.Provider = (*ProtoUnoconvProvider)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoUnoconvProvider)(nil)
|
|
||||||
_ unoconv.API = (*ProtoUnoconvAPI)(nil)
|
|
||||||
_ gotenberg.PDFEngineProvider = (*ProtoPDFEngineProvider)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoPDFEngineProvider)(nil)
|
|
||||||
_ gotenberg.PDFEngine = (*ProtoPDFEngine)(nil)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
// Package pdfengine provides a module which abstracts the CLI tool unoconv and
|
// Package pdfengine provides a module which interacts with the UNO
|
||||||
// implements the gotenberg.PDFEngine interface.
|
// (Universal Network Objects) API and implements the gotenberg.PDFEngine
|
||||||
|
// interface.
|
||||||
package pdfengine
|
package pdfengine
|
||||||
|
|||||||
@@ -2,75 +2,76 @@ package pdfengine
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gotenberg.MustRegisterModule(UnoconvPDFEngine{})
|
gotenberg.MustRegisterModule(UNO{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnoconvPDFEngine abstracts the CLI tool unoconv and implements the
|
// UNO interacts with the UNO (Universal Network Objects) API and implements
|
||||||
// gotenberg.PDFEngine interface.
|
// the gotenberg.PDFEngine interface.
|
||||||
type UnoconvPDFEngine struct {
|
type UNO struct {
|
||||||
unoconv unoconv.API
|
unoAPI uno.API
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descriptor returns a UnoconvPDFEngine's module descriptor.
|
// Descriptor returns a UNO's module descriptor.
|
||||||
func (UnoconvPDFEngine) Descriptor() gotenberg.ModuleDescriptor {
|
func (UNO) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{
|
return gotenberg.ModuleDescriptor{
|
||||||
ID: "unoconv-pdfengine",
|
ID: "uno-pdfengine",
|
||||||
New: func() gotenberg.Module { return new(UnoconvPDFEngine) },
|
New: func() gotenberg.Module { return new(UNO) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision sets the module properties.
|
// Provision sets the module properties.
|
||||||
func (engine *UnoconvPDFEngine) Provision(ctx *gotenberg.Context) error {
|
func (engine *UNO) Provision(ctx *gotenberg.Context) error {
|
||||||
provider, err := ctx.Module(new(unoconv.Provider))
|
provider, err := ctx.Module(new(uno.Provider))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get unoconv provider: %w", err)
|
return fmt.Errorf("get unoconv provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uno, err := provider.(unoconv.Provider).Unoconv()
|
unoAPI, err := provider.(uno.Provider).UNO()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get unoconv API: %w", err)
|
return fmt.Errorf("get unoconv API: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
engine.unoconv = uno
|
engine.unoAPI = unoAPI
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge is not available for this PDF engine.
|
// Merge is not available for this PDF engine.
|
||||||
func (engine UnoconvPDFEngine) Merge(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
func (engine UNO) Merge(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
||||||
return fmt.Errorf("merge PDFs with unoconv: %w", gotenberg.ErrPDFEngineMethodNotAvailable)
|
return fmt.Errorf("merge PDFs with unoconv: %w", gotenberg.ErrPDFEngineMethodNotAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert converts the given PDF to a specific PDF format. Currently, only the
|
// Convert converts the given PDF to a specific PDF format. Currently, only the
|
||||||
// PDF/A-1 format is available. If another PDF format is requested, it returns
|
// PDF/A-1a, PDF/A-2b and PDF/A-3b formats are available. If another PDF format
|
||||||
// a gotenberg.ErrPDFFormatNotAvailable error.
|
// is requested, it returns a gotenberg.ErrPDFFormatNotAvailable error.
|
||||||
func (engine UnoconvPDFEngine) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
func (engine UNO) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
if format != gotenberg.FormatPDFA1a {
|
err := engine.unoAPI.PDF(ctx, logger, inputPath, outputPath, uno.Options{
|
||||||
return fmt.Errorf("convert PDF to '%s' with unoconv: %w", format, gotenberg.ErrPDFFormatNotAvailable)
|
PDFformat: format,
|
||||||
}
|
|
||||||
|
|
||||||
err := engine.unoconv.PDF(ctx, logger, inputPath, outputPath, unoconv.Options{
|
|
||||||
PDFArchive: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, uno.ErrInvalidPDFformat) {
|
||||||
|
return fmt.Errorf("convert PDF to '%s' with unoconv: %w", format, gotenberg.ErrPDFFormatNotAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Errorf("convert PDF to '%s' with unoconv: %w", format, err)
|
return fmt.Errorf("convert PDF to '%s' with unoconv: %w", format, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*UnoconvPDFEngine)(nil)
|
_ gotenberg.Module = (*UNO)(nil)
|
||||||
_ gotenberg.Provisioner = (*UnoconvPDFEngine)(nil)
|
_ gotenberg.Provisioner = (*UNO)(nil)
|
||||||
_ gotenberg.PDFEngine = (*UnoconvPDFEngine)(nil)
|
_ gotenberg.PDFEngine = (*UNO)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,191 +7,168 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
|
||||||
flag "github.com/spf13/pflag"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProtoModule struct {
|
func TestUNO_Descriptor(t *testing.T) {
|
||||||
descriptor func() gotenberg.ModuleDescriptor
|
descriptor := UNO{}.Descriptor()
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return mod.descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoUnoconvProvider struct {
|
|
||||||
ProtoModule
|
|
||||||
unoconv func() (unoconv.API, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoUnoconvProvider) Unoconv() (unoconv.API, error) {
|
|
||||||
return mod.unoconv()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoUnoconvAPI struct {
|
|
||||||
pdf func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options unoconv.Options) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoUnoconvAPI) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options unoconv.Options) error {
|
|
||||||
return mod.pdf(ctx, logger, inputPath, outputPath, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoUnoconvAPI) Extensions() []string {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconvPDFEngine_Descriptor(t *testing.T) {
|
|
||||||
descriptor := UnoconvPDFEngine{}.Descriptor()
|
|
||||||
|
|
||||||
actual := reflect.TypeOf(descriptor.New())
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
expect := reflect.TypeOf(new(UnoconvPDFEngine))
|
expect := reflect.TypeOf(new(UNO))
|
||||||
|
|
||||||
if actual != expect {
|
if actual != expect {
|
||||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnoconvPDFEngine_Provision(t *testing.T) {
|
func TestUNO_Provider(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
ctx *gotenberg.Context
|
name string
|
||||||
expectErr bool
|
ctx *gotenberg.Context
|
||||||
|
expectProvisionErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod := struct{ ProtoModule }{}
|
provider := struct {
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
uno.ProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
provider.UNOMock = func() (uno.API, error) {
|
||||||
|
return uno.APIMock{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
gotenberg.ParsedFlags{
|
gotenberg.ParsedFlags{
|
||||||
FlagSet: flag.NewFlagSet("foo", flag.ExitOnError),
|
FlagSet: new(UNO).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
mod.Descriptor(),
|
provider.Descriptor(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: func() *gotenberg.Context {
|
name: "no UNO API provider",
|
||||||
mod := struct{ ProtoUnoconvProvider }{}
|
ctx: gotenberg.NewContext(
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ParsedFlags{
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
FlagSet: new(UNO).Descriptor().FlagSet,
|
||||||
}
|
},
|
||||||
mod.unoconv = func() (unoconv.API, error) {
|
[]gotenberg.ModuleDescriptor{},
|
||||||
return nil, errors.New("foo")
|
),
|
||||||
}
|
expectProvisionErr: true,
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
|
||||||
gotenberg.ParsedFlags{
|
|
||||||
FlagSet: flag.NewFlagSet("foo", flag.ExitOnError),
|
|
||||||
},
|
|
||||||
[]gotenberg.ModuleDescriptor{
|
|
||||||
mod.Descriptor(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "no API from UNO API provider",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
mod := struct{ ProtoUnoconvProvider }{}
|
provider := struct {
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
uno.ProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
}
|
}
|
||||||
mod.unoconv = func() (unoconv.API, error) {
|
provider.UNOMock = func() (uno.API, error) {
|
||||||
return struct{ ProtoUnoconvAPI }{}, nil
|
return uno.APIMock{}, errors.New("foo")
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
gotenberg.ParsedFlags{
|
gotenberg.ParsedFlags{
|
||||||
FlagSet: flag.NewFlagSet("foo", flag.ExitOnError),
|
FlagSet: new(UNO).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
mod.Descriptor(),
|
provider.Descriptor(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
|
expectProvisionErr: true,
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
mod := new(UnoconvPDFEngine)
|
|
||||||
err := mod.Provision(tc.ctx)
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
for _, tc := range tests {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
}
|
mod := new(UNO)
|
||||||
|
err := mod.Provision(tc.ctx)
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectProvisionErr && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Error("expected mod.Provision() error, but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !tc.expectProvisionErr && err != nil {
|
||||||
|
t.Errorf("expected no error from mod.Provision(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnoconvPDFEngine_Merge(t *testing.T) {
|
func TestUNO_Merge(t *testing.T) {
|
||||||
mod := new(UnoconvPDFEngine)
|
mod := new(UNO)
|
||||||
err := mod.Merge(context.TODO(), zap.NewNop(), nil, "")
|
err := mod.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||||
|
|
||||||
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
|
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
|
||||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
|
t.Errorf("expected error %v from mod.Merge(), but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnoconvPDFEngine_Convert(t *testing.T) {
|
func TestUNO_Convert(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
api unoconv.API
|
name string
|
||||||
format string
|
mod UNO
|
||||||
expectErr bool
|
expectConvertErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
format: "",
|
name: "nominal behavior",
|
||||||
expectErr: true,
|
mod: UNO{
|
||||||
|
unoAPI: uno.APIMock{
|
||||||
|
PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
api: func() unoconv.API {
|
name: "invalid PDF format",
|
||||||
unoconvAPI := struct{ ProtoUnoconvAPI }{}
|
mod: UNO{
|
||||||
unoconvAPI.pdf = func(_ context.Context, _ *zap.Logger, _, __ string, _ unoconv.Options) error {
|
unoAPI: uno.APIMock{
|
||||||
return errors.New("foo")
|
PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error {
|
||||||
}
|
return uno.ErrInvalidPDFformat
|
||||||
|
},
|
||||||
return unoconvAPI
|
},
|
||||||
}(),
|
},
|
||||||
format: gotenberg.FormatPDFA1a,
|
expectConvertErr: true,
|
||||||
expectErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
api: func() unoconv.API {
|
name: "convert fail",
|
||||||
unoconvAPI := struct{ ProtoUnoconvAPI }{}
|
mod: UNO{
|
||||||
unoconvAPI.pdf = func(_ context.Context, _ *zap.Logger, _, __ string, _ unoconv.Options) error {
|
unoAPI: uno.APIMock{
|
||||||
return nil
|
PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options uno.Options) error {
|
||||||
}
|
return errors.New("foo")
|
||||||
|
},
|
||||||
return unoconvAPI
|
},
|
||||||
}(),
|
},
|
||||||
format: gotenberg.FormatPDFA1a,
|
expectConvertErr: true,
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
mod := new(UnoconvPDFEngine)
|
|
||||||
mod.unoconv = tc.api
|
|
||||||
|
|
||||||
err := mod.Convert(context.TODO(), zap.NewNop(), tc.format, "", "")
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.mod.Convert(context.Background(), zap.NewNop(), "", "", "")
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
if tc.expectConvertErr && err == nil {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Errorf("expected mod.Convert() error, but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if !tc.expectConvertErr && err != nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Fatalf("expected no error from mod.Convert(), but got: %v", err)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*ProtoModule)(nil)
|
|
||||||
_ unoconv.Provider = (*ProtoUnoconvProvider)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoUnoconvProvider)(nil)
|
|
||||||
_ unoconv.API = (*ProtoUnoconvAPI)(nil)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import (
|
|||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// convertRoute returns an api.Route which can convert LibreOffice documents
|
// convertRoute returns an api.Route which can convert LibreOffice documents
|
||||||
// to PDF.
|
// to PDF.
|
||||||
func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
|
func convertRoute(unoAPI uno.API, engine gotenberg.PDFEngine) api.Route {
|
||||||
return api.Route{
|
return api.Route{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Path: "/forms/libreoffice/convert",
|
Path: "/forms/libreoffice/convert",
|
||||||
@@ -27,15 +27,17 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
|
|||||||
landscape bool
|
landscape bool
|
||||||
nativePageRanges string
|
nativePageRanges string
|
||||||
nativePDFA1aFormat bool
|
nativePDFA1aFormat bool
|
||||||
|
nativePDFformat string
|
||||||
PDFformat string
|
PDFformat string
|
||||||
merge bool
|
merge bool
|
||||||
)
|
)
|
||||||
|
|
||||||
err := ctx.FormData().
|
err := ctx.FormData().
|
||||||
MandatoryPaths(uno.Extensions(), &inputPaths).
|
MandatoryPaths(unoAPI.Extensions(), &inputPaths).
|
||||||
Bool("landscape", &landscape, false).
|
Bool("landscape", &landscape, false).
|
||||||
String("nativePageRanges", &nativePageRanges, "").
|
String("nativePageRanges", &nativePageRanges, "").
|
||||||
Bool("nativePdfA1aFormat", &nativePDFA1aFormat, false).
|
Bool("nativePdfA1aFormat", &nativePDFA1aFormat, false).
|
||||||
|
String("nativePdfFormat", &nativePDFformat, "").
|
||||||
String("pdfFormat", &PDFformat, "").
|
String("pdfFormat", &PDFformat, "").
|
||||||
Bool("merge", &merge, false).
|
Bool("merge", &merge, false).
|
||||||
Validate()
|
Validate()
|
||||||
@@ -44,13 +46,35 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
|
|||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if nativePDFA1aFormat {
|
||||||
|
ctx.Log().Warn("'nativePdfA1aFormat' is deprecated; prefer 'nativePdfFormat' or 'pdfFormat' form fields instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
if nativePDFA1aFormat && nativePDFformat != "" {
|
||||||
|
return api.WrapError(
|
||||||
|
errors.New("got both 'nativePdfFormat' and 'nativePdfA1aFormat' form fields"),
|
||||||
|
api.NewSentinelHTTPError(http.StatusBadRequest, "Both 'nativePdfFormat' and 'nativePdfA1aFormat' form fields are provided"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if nativePDFA1aFormat && PDFformat != "" {
|
if nativePDFA1aFormat && PDFformat != "" {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
errors.New("got both 'pdfFormat' and 'nativePdfA1aFormat' form values"),
|
errors.New("got both 'pdfFormat' and 'nativePdfA1aFormat' form fields"),
|
||||||
api.NewSentinelHTTPError(http.StatusBadRequest, "Both 'pdfFormat' and 'nativePdfA1aFormat' form values are provided"),
|
api.NewSentinelHTTPError(http.StatusBadRequest, "Both 'pdfFormat' and 'nativePdfA1aFormat' form fields are provided"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if nativePDFformat != "" && PDFformat != "" {
|
||||||
|
return api.WrapError(
|
||||||
|
errors.New("got both 'pdfFormat' and 'nativePdfFormat' form fields"),
|
||||||
|
api.NewSentinelHTTPError(http.StatusBadRequest, "Both 'pdfFormat' and 'nativePdfFormat' form fields are provided"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if nativePDFA1aFormat {
|
||||||
|
nativePDFformat = gotenberg.FormatPDFA1a
|
||||||
|
}
|
||||||
|
|
||||||
// Alright, let's convert each document to PDF.
|
// Alright, let's convert each document to PDF.
|
||||||
|
|
||||||
outputPaths := make([]string, len(inputPaths))
|
outputPaths := make([]string, len(inputPaths))
|
||||||
@@ -58,16 +82,16 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
|
|||||||
for i, inputPath := range inputPaths {
|
for i, inputPath := range inputPaths {
|
||||||
outputPaths[i] = ctx.GeneratePath(".pdf")
|
outputPaths[i] = ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
options := unoconv.Options{
|
options := uno.Options{
|
||||||
Landscape: landscape,
|
Landscape: landscape,
|
||||||
PageRanges: nativePageRanges,
|
PageRanges: nativePageRanges,
|
||||||
PDFArchive: nativePDFA1aFormat,
|
PDFformat: nativePDFformat,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = uno.PDF(ctx, ctx.Log(), inputPath, outputPaths[i], options)
|
err = unoAPI.PDF(ctx, ctx.Log(), inputPath, outputPaths[i], options)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, unoconv.ErrMalformedPageRanges) {
|
if errors.Is(err, uno.ErrMalformedPageRanges) {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
fmt.Errorf("convert to PDF: %w", err),
|
fmt.Errorf("convert to PDF: %w", err),
|
||||||
api.NewSentinelHTTPError(http.StatusBadRequest, fmt.Sprintf("Malformed page ranges '%s' (nativePageRanges)", options.PageRanges)),
|
api.NewSentinelHTTPError(http.StatusBadRequest, fmt.Sprintf("Malformed page ranges '%s' (nativePageRanges)", options.PageRanges)),
|
||||||
@@ -92,10 +116,8 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
|
|||||||
// Now, let's check if the client want to convert this result
|
// Now, let's check if the client want to convert this result
|
||||||
// PDF to a specific PDF format.
|
// PDF to a specific PDF format.
|
||||||
|
|
||||||
// Note: nativePdfA1aFormat has not been specified if we reach
|
// Note: nativePdfA1aFormat/nativePdfFormat have not been
|
||||||
// this part of the code. Indeed, the handler returns early on
|
// specified if PDFformat is not empty.
|
||||||
// an error if both nativePdfA1aFormat and pdfFormat are
|
|
||||||
// present.
|
|
||||||
|
|
||||||
if PDFformat != "" {
|
if PDFformat != "" {
|
||||||
convertInputPath := outputPath
|
convertInputPath := outputPath
|
||||||
@@ -135,9 +157,8 @@ func convertRoute(uno unoconv.API, engine gotenberg.PDFEngine) api.Route {
|
|||||||
// Ok, we don't have to merge the PDFs. Let's check if the client
|
// Ok, we don't have to merge the PDFs. Let's check if the client
|
||||||
// want to convert each PDF to a specific PDF format.
|
// want to convert each PDF to a specific PDF format.
|
||||||
|
|
||||||
// Note: nativePdfA1aFormat has not been specified if we reach this
|
// Note: nativePdfA1aFormat/nativePdfFormat have not been
|
||||||
// part of the code. Indeed, the handler returns early on an error
|
// specified if PDFformat is not empty.
|
||||||
// if both nativePdfA1aFormat and pdfFormat are present.
|
|
||||||
|
|
||||||
if PDFformat != "" {
|
if PDFformat != "" {
|
||||||
convertOutputPaths := make([]string, len(outputPaths))
|
convertOutputPaths := make([]string, len(outputPaths))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
3
pkg/modules/libreoffice/uno/doc.go
Normal file
3
pkg/modules/libreoffice/uno/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package uno provides a module which interacts with the UNO
|
||||||
|
// (Universal Network Objects) API.
|
||||||
|
package uno
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package unoconv
|
package uno
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -9,18 +9,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func freePort(logger *zap.Logger) (int, error) {
|
func freePort(logger *zap.Logger) (int, error) {
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
netListener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("listen on the local network address: %w", err)
|
return 0, fmt.Errorf("listen on the local network address: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
err := listener.Close()
|
err := netListener.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("close listener: %s", err.Error()))
|
logger.Error(fmt.Sprintf("close network listener: %s", err.Error()))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
addr := listener.Addr().String()
|
addr := netListener.Addr().String()
|
||||||
|
|
||||||
_, portStr, err := net.SplitHostPort(addr)
|
_, portStr, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
248
pkg/modules/libreoffice/uno/listener.go
Normal file
248
pkg/modules/libreoffice/uno/listener.go
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
package uno
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listener interface {
|
||||||
|
start(logger *zap.Logger) error
|
||||||
|
stop(logger *zap.Logger) error
|
||||||
|
lock(ctx context.Context, logger *zap.Logger) error
|
||||||
|
unlock(logger *zap.Logger) error
|
||||||
|
port() int
|
||||||
|
queue() int
|
||||||
|
healthy() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type libreOfficeListener struct {
|
||||||
|
binPath string
|
||||||
|
startTimeout time.Duration
|
||||||
|
threshold int
|
||||||
|
|
||||||
|
socketPort int
|
||||||
|
userProfileDirPath string
|
||||||
|
cmd gotenberg.Cmd
|
||||||
|
cfgMu sync.RWMutex
|
||||||
|
|
||||||
|
usage int
|
||||||
|
queueLength int
|
||||||
|
queueLengthMu sync.RWMutex
|
||||||
|
lockChan chan struct{}
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLibreOfficeListener(logger *zap.Logger, binPath string, startTimeout time.Duration, threshold int) listener {
|
||||||
|
return &libreOfficeListener{
|
||||||
|
binPath: binPath,
|
||||||
|
startTimeout: startTimeout,
|
||||||
|
threshold: threshold,
|
||||||
|
lockChan: make(chan struct{}, 1),
|
||||||
|
logger: logger.Named("listener"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener *libreOfficeListener) start(logger *zap.Logger) error {
|
||||||
|
port, err := freePort(logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get free port: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good to know: when the supervisor manages the LibreOffice listener,
|
||||||
|
// the garbage collector might delete the next directory while it is
|
||||||
|
// still running. It does seem to cause any issue though.
|
||||||
|
userProfileDirPath := gotenberg.NewDirPath()
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"--headless",
|
||||||
|
"--invisible",
|
||||||
|
"--nocrashreport",
|
||||||
|
"--nodefault",
|
||||||
|
"--nologo",
|
||||||
|
"--nofirststartwizard",
|
||||||
|
"--norestore",
|
||||||
|
fmt.Sprintf("-env:UserInstallation=file://%s", userProfileDirPath),
|
||||||
|
fmt.Sprintf("--accept=socket,host=127.0.0.1,port=%d,tcpNoDelay=1;urp;StarOffice.ComponentContext", port),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), listener.startTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd, err := gotenberg.CommandContext(ctx, logger, listener.binPath, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create LibreOffice listener command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For whatever reason, LibreOffice requires a first start before being
|
||||||
|
// able to run as a daemon.
|
||||||
|
exitCode, err := cmd.Exec()
|
||||||
|
if err != nil && exitCode != 81 {
|
||||||
|
return fmt.Errorf("execute LibreOffice listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("got exit code 81, e.g., LibreOffice listener first start")
|
||||||
|
|
||||||
|
// Second start (daemon).
|
||||||
|
cmd = gotenberg.Command(logger, listener.binPath, args...)
|
||||||
|
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("start LibreOffice listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// As the LibreOffice socket may take some time to be available, we have to
|
||||||
|
// ensure that it is indeed accepting connections.
|
||||||
|
logger.Debug("waiting for the LibreOffice listener socket to be available...")
|
||||||
|
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return fmt.Errorf("waiting for the LibreOffice listener socket to be available: %w", ctx.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), time.Duration(1)*time.Second)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("LibreOffice listener socket available")
|
||||||
|
|
||||||
|
listener.cfgMu.Lock()
|
||||||
|
listener.socketPort = port
|
||||||
|
listener.userProfileDirPath = userProfileDirPath
|
||||||
|
listener.cmd = cmd
|
||||||
|
listener.cfgMu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener *libreOfficeListener) stop(logger *zap.Logger) error {
|
||||||
|
listener.cfgMu.RLock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
defer listener.cfgMu.RUnlock()
|
||||||
|
|
||||||
|
err := os.RemoveAll(listener.userProfileDirPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("remove LibreOffice listener user profile directory: %v", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := listener.cmd.Kill()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("kill LibreOffice listener process: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's wait to make sure the process is no more.
|
||||||
|
err = listener.cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug(fmt.Sprintf("wait for the LibreOffice listener: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener *libreOfficeListener) lock(ctx context.Context, logger *zap.Logger) error {
|
||||||
|
listener.queueLengthMu.Lock()
|
||||||
|
listener.queueLength += 1
|
||||||
|
listener.queueLengthMu.Unlock()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case listener.lockChan <- struct{}{}:
|
||||||
|
logger.Debug("LibreOffice listener lock acquired")
|
||||||
|
|
||||||
|
listener.queueLengthMu.Lock()
|
||||||
|
listener.queueLength -= 1
|
||||||
|
listener.queueLengthMu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Debug("failed to acquire LibreOffice listener lock before deadline")
|
||||||
|
|
||||||
|
listener.queueLengthMu.Lock()
|
||||||
|
listener.queueLength -= 1
|
||||||
|
listener.queueLengthMu.Unlock()
|
||||||
|
|
||||||
|
return fmt.Errorf("acquire LibreOffice listener lock: %w", ctx.Err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener *libreOfficeListener) unlock(logger *zap.Logger) error {
|
||||||
|
restart := func() error {
|
||||||
|
err := listener.stop(logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stop LibreOffice listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = listener.start(logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("start LibreOffice listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.usage = 0
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
<-listener.lockChan
|
||||||
|
logger.Debug("LibreOffice listener lock released")
|
||||||
|
}()
|
||||||
|
|
||||||
|
if !listener.healthy() {
|
||||||
|
logger.Debug("LibreOffice listener is unhealthy, restarting it...")
|
||||||
|
|
||||||
|
err := restart()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("restart LibreOffice listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.usage += 1
|
||||||
|
if listener.usage < listener.threshold {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("LibreOffice listener threshold reached, restarting it...")
|
||||||
|
|
||||||
|
err := restart()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("restart LibreOffice listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener *libreOfficeListener) port() int {
|
||||||
|
listener.cfgMu.RLock()
|
||||||
|
defer listener.cfgMu.RUnlock()
|
||||||
|
|
||||||
|
return listener.socketPort
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener *libreOfficeListener) queue() int {
|
||||||
|
listener.queueLengthMu.RLock()
|
||||||
|
defer listener.queueLengthMu.RUnlock()
|
||||||
|
|
||||||
|
return listener.queueLength
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener *libreOfficeListener) healthy() bool {
|
||||||
|
_, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", listener.port()), time.Duration(1)*time.Second)
|
||||||
|
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ listener = (*libreOfficeListener)(nil)
|
||||||
|
)
|
||||||
288
pkg/modules/libreoffice/uno/listener_test.go
Normal file
288
pkg/modules/libreoffice/uno/listener_test.go
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
package uno
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListener_start(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
listener listener
|
||||||
|
expectStartErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
listener: newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-exit code 81 on first start",
|
||||||
|
listener: newLibreOfficeListener(zap.NewNop(), "foo", time.Duration(10)*time.Second, 10),
|
||||||
|
expectStartErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.listener.start(zap.NewNop())
|
||||||
|
|
||||||
|
if tc.expectStartErr && err == nil {
|
||||||
|
t.Fatalf("expected listener.start() error, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectStartErr && err != nil {
|
||||||
|
t.Fatalf("expected no error from listener.start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if tc.listener.healthy() {
|
||||||
|
t.Error("expected a non-running LibreOffice listener")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tc.listener.stop(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from listener.stop(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListener_stop(t *testing.T) {
|
||||||
|
listener := newLibreOfficeListener(
|
||||||
|
zap.NewNop(),
|
||||||
|
os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
time.Duration(10)*time.Second,
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := listener.start(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from listener.start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = listener.stop(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from listener.stop(), but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListener_lock(t *testing.T) {
|
||||||
|
listener := newLibreOfficeListener(
|
||||||
|
zap.NewNop(),
|
||||||
|
os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
time.Duration(10)*time.Second,
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
|
||||||
|
|
||||||
|
err := listener.lock(ctx, zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from listener.lock(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err = listener.lock(ctx, zap.NewNop())
|
||||||
|
if !errors.Is(err, context.Canceled) {
|
||||||
|
t.Errorf("expected %v error, but got: %v", context.Canceled, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListener_unlock(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
listener listener
|
||||||
|
teardown func(listener listener) error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
listener: func() listener {
|
||||||
|
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10)
|
||||||
|
|
||||||
|
err := listener.start(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from listener.start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
return listener
|
||||||
|
}(),
|
||||||
|
teardown: func(listener listener) error {
|
||||||
|
return listener.stop(zap.NewNop())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unhealthy listener",
|
||||||
|
listener: func() listener {
|
||||||
|
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 10)
|
||||||
|
|
||||||
|
err := listener.start(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from listener.start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = listener.stop(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from listener.stop(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return listener
|
||||||
|
}(),
|
||||||
|
teardown: func(listener listener) error {
|
||||||
|
return listener.stop(zap.NewNop())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "threshold reached",
|
||||||
|
listener: func() listener {
|
||||||
|
listener := newLibreOfficeListener(zap.NewNop(), os.Getenv("LIBREOFFICE_BIN_PATH"), time.Duration(10)*time.Second, 1)
|
||||||
|
|
||||||
|
err := listener.start(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from listener.start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
return listener
|
||||||
|
}(),
|
||||||
|
teardown: func(listener listener) error {
|
||||||
|
return listener.stop(zap.NewNop())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := tc.listener.lock(ctx, zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from listener.lock(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tc.listener.unlock(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from listener.unlock(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tc.teardown(tc.listener)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from tc.teardown(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListener_port(t *testing.T) {
|
||||||
|
listener := newLibreOfficeListener(
|
||||||
|
zap.NewNop(),
|
||||||
|
os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
time.Duration(10)*time.Second,
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := listener.start(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from listener.start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
port := listener.port()
|
||||||
|
if port == 0 {
|
||||||
|
t.Error("expected a non-zero value from listener.port")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = listener.stop(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from listener.stop(), but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListener_queue(t *testing.T) {
|
||||||
|
listener := newLibreOfficeListener(
|
||||||
|
zap.NewNop(),
|
||||||
|
os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
time.Duration(10)*time.Second,
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
|
||||||
|
queueLength := listener.queue()
|
||||||
|
if queueLength != 0 {
|
||||||
|
t.Fatalf("expected a zero value from listener.queue(), but got %d", queueLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
|
||||||
|
|
||||||
|
err := listener.lock(ctx, zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from listener.lock(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
queueLength = listener.queue()
|
||||||
|
if queueLength != 0 {
|
||||||
|
t.Fatalf("expected a zero value from listener.queue(), but got %d", queueLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = listener.lock(ctx, zap.NewNop())
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(100) * time.Millisecond)
|
||||||
|
|
||||||
|
queueLength = listener.queue()
|
||||||
|
if queueLength != 1 {
|
||||||
|
t.Fatalf("expected 1 from listener.queue(), but got %d", queueLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = listener.lock(ctx, zap.NewNop())
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(100) * time.Millisecond)
|
||||||
|
|
||||||
|
queueLength = listener.queue()
|
||||||
|
if queueLength != 2 {
|
||||||
|
t.Fatalf("expected 2 from listener.queue(), but got %d", queueLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(100) * time.Millisecond)
|
||||||
|
|
||||||
|
queueLength = listener.queue()
|
||||||
|
if queueLength != 0 {
|
||||||
|
t.Fatalf("expected a zero value from listener.queue(), but got %d", queueLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListener_healthy(t *testing.T) {
|
||||||
|
listener := newLibreOfficeListener(
|
||||||
|
zap.NewNop(),
|
||||||
|
os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
time.Duration(10)*time.Second,
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := listener.start(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from listener.start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !listener.healthy() {
|
||||||
|
t.Error("expected an healthy LibreOffice listener")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = listener.stop(zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from listener.stop(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if listener.healthy() {
|
||||||
|
t.Errorf("expected a non-healthy LibreOffice listener")
|
||||||
|
}
|
||||||
|
}
|
||||||
36
pkg/modules/libreoffice/uno/mocks.go
Normal file
36
pkg/modules/libreoffice/uno/mocks.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package uno
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// APIMock is a mock for the API interface.
|
||||||
|
type APIMock struct {
|
||||||
|
PDFMock func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
|
||||||
|
ExtensionsMock func() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api APIMock) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
||||||
|
return api.PDFMock(ctx, logger, inputPath, outputPath, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api APIMock) Extensions() []string {
|
||||||
|
return api.ExtensionsMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProviderMock is a mock for the Provider interface.
|
||||||
|
type ProviderMock struct {
|
||||||
|
UNOMock func() (API, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider ProviderMock) UNO() (API, error) {
|
||||||
|
return provider.UNOMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ API = (*APIMock)(nil)
|
||||||
|
_ Provider = (*ProviderMock)(nil)
|
||||||
|
)
|
||||||
42
pkg/modules/libreoffice/uno/mocks_test.go
Normal file
42
pkg/modules/libreoffice/uno/mocks_test.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package uno
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPIMock(t *testing.T) {
|
||||||
|
mock := APIMock{
|
||||||
|
PDFMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExtensionsMock: func() []string {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mock.PDF(context.Background(), zap.NewNop(), "", "", Options{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from mock.PDF(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := mock.Extensions()
|
||||||
|
if ext != nil {
|
||||||
|
t.Errorf("expected no extensions from mock.Extensions(), but got: %+v", ext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderMock(t *testing.T) {
|
||||||
|
mock := ProviderMock{
|
||||||
|
UNOMock: func() (API, error) {
|
||||||
|
return APIMock{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mock.UNO()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from mock.UNO(), but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
522
pkg/modules/libreoffice/uno/uno.go
Normal file
522
pkg/modules/libreoffice/uno/uno.go
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
package uno
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alexliesenfeld/health"
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gotenberg.MustRegisterModule(UNO{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidPDFformat happens if the PDF format option cannot be handled
|
||||||
|
// by LibreOffice.
|
||||||
|
ErrInvalidPDFformat = errors.New("invalid PDF format")
|
||||||
|
|
||||||
|
// ErrMalformedPageRanges happens if the page ranges option cannot be
|
||||||
|
// interpreted by LibreOffice.
|
||||||
|
ErrMalformedPageRanges = errors.New("page ranges are malformed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// UNO is a module which provides an API to interact with LibreOffice.
|
||||||
|
type UNO struct {
|
||||||
|
unoconvBinPath string
|
||||||
|
libreOfficeBinPath string
|
||||||
|
libreOfficeStartTimeout time.Duration
|
||||||
|
libreOfficeRestartThreshold int
|
||||||
|
|
||||||
|
listener listener
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options gathers available options when converting a document to PDF.
|
||||||
|
type Options struct {
|
||||||
|
// Landscape allows to change the orientation of the resulting PDF.
|
||||||
|
// Optional.
|
||||||
|
Landscape bool
|
||||||
|
|
||||||
|
// PageRanges allows to select the pages to convert.
|
||||||
|
// TODO: should prefer a method form PDFEngine.
|
||||||
|
// Optional.
|
||||||
|
PageRanges string
|
||||||
|
|
||||||
|
// PDFformat allows to convert the resulting PDF to PDF/A-1a, PDF/A-2b, or
|
||||||
|
// PDF/A-3b.
|
||||||
|
// Optional.
|
||||||
|
PDFformat string
|
||||||
|
}
|
||||||
|
|
||||||
|
// API is an abstraction on top of uno.
|
||||||
|
type API interface {
|
||||||
|
PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
|
||||||
|
Extensions() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider is a module interface which exposes a method for creating an API
|
||||||
|
// for other modules.
|
||||||
|
//
|
||||||
|
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||||
|
// provider, _ := ctx.Module(new(uno.Provider))
|
||||||
|
// unoAPI, _ := provider.(uno.Provider).UNO()
|
||||||
|
// }
|
||||||
|
type Provider interface {
|
||||||
|
UNO() (API, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptor returns a UNO's module descriptor.
|
||||||
|
func (UNO) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{
|
||||||
|
ID: "uno",
|
||||||
|
FlagSet: func() *flag.FlagSet {
|
||||||
|
fs := flag.NewFlagSet("uno", flag.ExitOnError)
|
||||||
|
fs.Duration("uno-listener-start-timeout", time.Duration(10)*time.Second, "Time limit for starting the LibreOffice listener")
|
||||||
|
fs.Int("uno-listener-restart-threshold", 10, "Operations limit after which the LibreOffice listener is restarted - 0 means no long-running LibreOffice listener")
|
||||||
|
fs.Bool("unoconv-disable-listener", false, "Do not start a long-running listener - save resources in detriment of unitary performance")
|
||||||
|
|
||||||
|
err := fs.MarkDeprecated("unoconv-disable-listener", "use uno-listener-restart-threshold with 0 instead")
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("create deprecated flags for the uno module: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
New: func() gotenberg.Module { return new(UNO) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision sets the module properties. It returns an error if the environment
|
||||||
|
// variables UNOCONV_BIN_PATH and LIBREOFFICE_BIN_PATH are not set.
|
||||||
|
func (mod *UNO) Provision(ctx *gotenberg.Context) error {
|
||||||
|
flags := ctx.ParsedFlags()
|
||||||
|
mod.libreOfficeStartTimeout = flags.MustDuration("uno-listener-start-timeout")
|
||||||
|
mod.libreOfficeRestartThreshold = flags.MustInt("uno-listener-restart-threshold")
|
||||||
|
|
||||||
|
disableListener := flags.MustBool("unoconv-disable-listener")
|
||||||
|
if disableListener {
|
||||||
|
mod.libreOfficeRestartThreshold = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
unoconvBinPath, ok := os.LookupEnv("UNOCONV_BIN_PATH")
|
||||||
|
if !ok {
|
||||||
|
return errors.New("UNOCONV_BIN_PATH environment variable is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
mod.unoconvBinPath = unoconvBinPath
|
||||||
|
|
||||||
|
libreOfficeBinPath, ok := os.LookupEnv("LIBREOFFICE_BIN_PATH")
|
||||||
|
if !ok {
|
||||||
|
return errors.New("LIBREOFFICE_BIN_PATH environment variable is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
mod.libreOfficeBinPath = libreOfficeBinPath
|
||||||
|
|
||||||
|
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get logger provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err := loggerProvider.(gotenberg.LoggerProvider).Logger(mod)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get logger: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mod.logger = logger
|
||||||
|
|
||||||
|
mod.listener = newLibreOfficeListener(
|
||||||
|
mod.logger,
|
||||||
|
mod.libreOfficeBinPath,
|
||||||
|
mod.libreOfficeStartTimeout,
|
||||||
|
mod.libreOfficeRestartThreshold,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the module properties.
|
||||||
|
func (mod UNO) Validate() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, statErr := os.Stat(mod.unoconvBinPath)
|
||||||
|
if os.IsNotExist(statErr) {
|
||||||
|
err = multierr.Append(err, fmt.Errorf("unoconv binary path does not exist: %w", statErr))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, statErr = os.Stat(mod.libreOfficeBinPath)
|
||||||
|
if os.IsNotExist(statErr) {
|
||||||
|
err = multierr.Append(err, fmt.Errorf("LibreOffice binary path does not exist: %w", statErr))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the long-running LibreOffice listener if the threshold is
|
||||||
|
// superior to zero.
|
||||||
|
func (mod UNO) Start() error {
|
||||||
|
if mod.libreOfficeRestartThreshold == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mod.listener.start(mod.logger)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("start long-running LibreOffice listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartupMessage returns a custom startup message.
|
||||||
|
func (mod UNO) StartupMessage() string {
|
||||||
|
if mod.libreOfficeRestartThreshold == 0 {
|
||||||
|
return "Long-running LibreOffice listener disabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Long-running LibreOffice listener started"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the long-running LibreOffice Listener if it exists.
|
||||||
|
func (mod UNO) Stop(ctx context.Context) error {
|
||||||
|
if mod.libreOfficeRestartThreshold == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block until the context is done so that other module may gracefully stop
|
||||||
|
// before we do a shutdown cleanup.
|
||||||
|
mod.logger.Debug("wait for the end of grace duration")
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
err := mod.listener.stop(mod.logger)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("stop long-running LibreOffice supervisor")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics returns the metrics.
|
||||||
|
func (mod UNO) Metrics() ([]gotenberg.Metric, error) {
|
||||||
|
return []gotenberg.Metric{
|
||||||
|
{
|
||||||
|
Name: "unoconv_active_instances_count",
|
||||||
|
Description: "Current number of active unoconv instances.",
|
||||||
|
Read: func() float64 {
|
||||||
|
activeInstancesCountMu.RLock()
|
||||||
|
defer activeInstancesCountMu.RUnlock()
|
||||||
|
|
||||||
|
return activeInstancesCount
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "libreoffice_listener_active_instances_count",
|
||||||
|
Description: "Current number of active LibreOffice listener instances.",
|
||||||
|
Read: func() float64 {
|
||||||
|
if mod.libreOfficeRestartThreshold == 0 {
|
||||||
|
listenerActiveInstancesCountMu.RLock()
|
||||||
|
defer listenerActiveInstancesCountMu.RUnlock()
|
||||||
|
|
||||||
|
return listenerActiveInstancesCount
|
||||||
|
}
|
||||||
|
|
||||||
|
if mod.listener.healthy() {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "unoconv_listener_active_instances_count",
|
||||||
|
Description: "Current number of active unoconv listener instances - deprecated, prefer libreoffice_listener_active_instances_count.",
|
||||||
|
Read: func() float64 {
|
||||||
|
if mod.libreOfficeRestartThreshold == 0 {
|
||||||
|
listenerActiveInstancesCountMu.RLock()
|
||||||
|
defer listenerActiveInstancesCountMu.RUnlock()
|
||||||
|
|
||||||
|
return listenerActiveInstancesCount
|
||||||
|
}
|
||||||
|
|
||||||
|
if mod.listener.healthy() {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "libreoffice_listener_queue_length",
|
||||||
|
Description: "Current number of processes in the queue.",
|
||||||
|
Read: func() float64 {
|
||||||
|
return float64(mod.listener.queue())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "unoconv_listener_queue_length",
|
||||||
|
Description: "Current number of processes in the queue - deprecated, prefer libreoffice_listener_queue_length.",
|
||||||
|
Read: func() float64 {
|
||||||
|
return float64(mod.listener.queue())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks adds a health check that verifies the health of the long-running
|
||||||
|
// LibreOffice listener.
|
||||||
|
func (mod UNO) Checks() ([]health.CheckerOption, error) {
|
||||||
|
if mod.libreOfficeRestartThreshold == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []health.CheckerOption{
|
||||||
|
health.WithCheck(health.Check{
|
||||||
|
Name: "uno",
|
||||||
|
Check: func(_ context.Context) error {
|
||||||
|
if mod.listener.healthy() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("long-running LibreOffice listener unhealthy")
|
||||||
|
},
|
||||||
|
// The long-running LibreOffice listener may be restarting, so we
|
||||||
|
// wait a given amount of time until we consider the module
|
||||||
|
// unavailable.
|
||||||
|
MaxTimeInError: mod.libreOfficeStartTimeout,
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF converts a document to PDF.
|
||||||
|
//
|
||||||
|
// If there is no long-running LibreOffice listener, it creates a dedicated
|
||||||
|
// LibreOffice instance for the conversion. Substantial calls to this method
|
||||||
|
// may increase CPU and memory usage drastically
|
||||||
|
//
|
||||||
|
// If there is a long-running LibreOffice listener, the conversion performance
|
||||||
|
// improves substantially. However, it cannot perform parallel operations.
|
||||||
|
func (mod UNO) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
||||||
|
args := []string{
|
||||||
|
"--no-launch",
|
||||||
|
"--format",
|
||||||
|
"pdf",
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mod.libreOfficeRestartThreshold {
|
||||||
|
case 0:
|
||||||
|
listener := newLibreOfficeListener(logger, mod.libreOfficeBinPath, mod.libreOfficeStartTimeout, 0)
|
||||||
|
|
||||||
|
err := listener.start(logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("start LibreOffice listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := listener.stop(logger)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("stop LibreOffice listener: %v", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
args = append(args, "--port", fmt.Sprintf("%d", listener.port()))
|
||||||
|
default:
|
||||||
|
err := mod.listener.lock(ctx, logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("lock long-running LibreOffice listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
go func() {
|
||||||
|
err := mod.listener.unlock(logger)
|
||||||
|
if err != nil {
|
||||||
|
mod.logger.Error(fmt.Sprintf("unlock long-running LibreOffice listener: %v", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// If the LibreOffice listener is restarting while acquiring the lock,
|
||||||
|
// the port will change. It's therefore important to add the port args
|
||||||
|
// after we acquire the lock.
|
||||||
|
args = append(args, "--port", fmt.Sprintf("%d", mod.listener.port()))
|
||||||
|
}
|
||||||
|
|
||||||
|
checkedEntry := logger.Check(zap.DebugLevel, "check for debug level before setting high verbosity")
|
||||||
|
if checkedEntry != nil {
|
||||||
|
args = append(args, "-vvv")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Landscape {
|
||||||
|
args = append(args, "--printer", "PaperOrientation=landscape")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.PageRanges != "" {
|
||||||
|
args = append(args, "--export", fmt.Sprintf("PageRange=%s", options.PageRanges))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch options.PDFformat {
|
||||||
|
case "":
|
||||||
|
case gotenberg.FormatPDFA1a:
|
||||||
|
args = append(args, "--export", "SelectPdfVersion=1")
|
||||||
|
case gotenberg.FormatPDFA2b:
|
||||||
|
args = append(args, "--export", "SelectPdfVersion=2")
|
||||||
|
case gotenberg.FormatPDFA3b:
|
||||||
|
args = append(args, "--export", "SelectPdfVersion=3")
|
||||||
|
default:
|
||||||
|
return ErrInvalidPDFformat
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, "--output", outputPath, inputPath)
|
||||||
|
|
||||||
|
cmd, err := gotenberg.CommandContext(ctx, logger, mod.unoconvBinPath, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create unoconv command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("print to PDF with: %+v", options))
|
||||||
|
|
||||||
|
activeInstancesCountMu.Lock()
|
||||||
|
activeInstancesCount += 1
|
||||||
|
activeInstancesCountMu.Unlock()
|
||||||
|
|
||||||
|
exitCode, err := cmd.Exec()
|
||||||
|
|
||||||
|
activeInstancesCountMu.Lock()
|
||||||
|
activeInstancesCount -= 1
|
||||||
|
activeInstancesCountMu.Unlock()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unoconv/LibreOffice errors are not explicit.
|
||||||
|
// That's why we have to make an educated guess according to the exit code
|
||||||
|
// and given inputs.
|
||||||
|
|
||||||
|
if exitCode == 5 && options.PageRanges != "" {
|
||||||
|
return ErrMalformedPageRanges
|
||||||
|
}
|
||||||
|
|
||||||
|
// Possible errors:
|
||||||
|
// 1. Unoconv/LibreOffice failed for some reason.
|
||||||
|
// 2. Context done.
|
||||||
|
//
|
||||||
|
// On the second scenario, LibreOffice might not have time to remove some
|
||||||
|
// of its temporary files, as it has been killed without warning. The
|
||||||
|
// garbage collector will delete them for us (if the module is loaded).
|
||||||
|
return fmt.Errorf("unoconv PDF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extensions returns the file extensions available for conversions.
|
||||||
|
func (mod UNO) Extensions() []string {
|
||||||
|
return []string{
|
||||||
|
".bib",
|
||||||
|
".doc",
|
||||||
|
".xml",
|
||||||
|
".docx",
|
||||||
|
".fodt",
|
||||||
|
".html",
|
||||||
|
".ltx",
|
||||||
|
".txt",
|
||||||
|
".odt",
|
||||||
|
".ott",
|
||||||
|
".pdb",
|
||||||
|
".pdf",
|
||||||
|
".psw",
|
||||||
|
".rtf",
|
||||||
|
".sdw",
|
||||||
|
".stw",
|
||||||
|
".sxw",
|
||||||
|
".uot",
|
||||||
|
".vor",
|
||||||
|
".wps",
|
||||||
|
".epub",
|
||||||
|
".png",
|
||||||
|
".bmp",
|
||||||
|
".emf",
|
||||||
|
".eps",
|
||||||
|
".fodg",
|
||||||
|
".gif",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".met",
|
||||||
|
".odd",
|
||||||
|
".otg",
|
||||||
|
".pbm",
|
||||||
|
".pct",
|
||||||
|
".pgm",
|
||||||
|
".ppm",
|
||||||
|
".ras",
|
||||||
|
".std",
|
||||||
|
".svg",
|
||||||
|
".svm",
|
||||||
|
".swf",
|
||||||
|
".sxd",
|
||||||
|
".sxw",
|
||||||
|
".tif",
|
||||||
|
".tiff",
|
||||||
|
".xhtml",
|
||||||
|
".xpm",
|
||||||
|
".odp",
|
||||||
|
".fodp",
|
||||||
|
".potm",
|
||||||
|
".pot",
|
||||||
|
".pptx",
|
||||||
|
".pps",
|
||||||
|
".ppt",
|
||||||
|
".pwp",
|
||||||
|
".sda",
|
||||||
|
".sdd",
|
||||||
|
".sti",
|
||||||
|
".sxi",
|
||||||
|
".uop",
|
||||||
|
".wmf",
|
||||||
|
".csv",
|
||||||
|
".dbf",
|
||||||
|
".dif",
|
||||||
|
".fods",
|
||||||
|
".ods",
|
||||||
|
".ots",
|
||||||
|
".pxl",
|
||||||
|
".sdc",
|
||||||
|
".slk",
|
||||||
|
".stc",
|
||||||
|
".sxc",
|
||||||
|
".uos",
|
||||||
|
".xls",
|
||||||
|
".xlt",
|
||||||
|
".xlsx",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UNO returns an API for interacting with LibreOffice.
|
||||||
|
func (mod UNO) UNO() (API, error) {
|
||||||
|
return mod, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
listenerActiveInstancesCount float64
|
||||||
|
listenerActiveInstancesCountMu sync.RWMutex
|
||||||
|
activeInstancesCount float64
|
||||||
|
activeInstancesCountMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ gotenberg.Module = (*UNO)(nil)
|
||||||
|
_ gotenberg.Provisioner = (*UNO)(nil)
|
||||||
|
_ gotenberg.Validator = (*UNO)(nil)
|
||||||
|
_ gotenberg.App = (*UNO)(nil)
|
||||||
|
_ gotenberg.MetricsProvider = (*UNO)(nil)
|
||||||
|
_ api.HealthChecker = (*UNO)(nil)
|
||||||
|
_ API = (*UNO)(nil)
|
||||||
|
_ Provider = (*UNO)(nil)
|
||||||
|
)
|
||||||
865
pkg/modules/libreoffice/uno/uno_test.go
Normal file
865
pkg/modules/libreoffice/uno/uno_test.go
Normal file
@@ -0,0 +1,865 @@
|
|||||||
|
package uno
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alexliesenfeld/health"
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUNO_Descriptor(t *testing.T) {
|
||||||
|
descriptor := UNO{}.Descriptor()
|
||||||
|
|
||||||
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
|
expect := reflect.TypeOf(new(UNO))
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUNO_Provision(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx *gotenberg.Context
|
||||||
|
expectProvisionErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
provider := struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.LoggerProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return zap.NewNop(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(UNO).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
provider.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "threshold from deprecated flag --unoconv-disable-listener",
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
provider := struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.LoggerProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return zap.NewNop(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: func() *flag.FlagSet {
|
||||||
|
fs := new(UNO).Descriptor().FlagSet
|
||||||
|
err := fs.Parse([]string{"--unoconv-disable-listener=true"})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
provider.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no logger provider",
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(UNO).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectProvisionErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no logger from logger provider",
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
provider := struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.LoggerProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(UNO).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
provider.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectProvisionErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
mod := new(UNO)
|
||||||
|
err := mod.Provision(tc.ctx)
|
||||||
|
|
||||||
|
if tc.expectProvisionErr && err == nil {
|
||||||
|
t.Errorf("expected mod.Provision() error, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectProvisionErr && err != nil {
|
||||||
|
t.Errorf("expected no error from mod.Provision(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUNO_Validate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
unoconvBinPath string
|
||||||
|
libreOfficeBinPath string
|
||||||
|
expectValidateErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unoconv bin path does not exist",
|
||||||
|
unoconvBinPath: "/foo",
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
expectValidateErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LibreOffice bin path does not exist",
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: "/foo",
|
||||||
|
expectValidateErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
mod := UNO{
|
||||||
|
unoconvBinPath: tc.unoconvBinPath,
|
||||||
|
libreOfficeBinPath: tc.libreOfficeBinPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mod.Validate()
|
||||||
|
|
||||||
|
if tc.expectValidateErr && err == nil {
|
||||||
|
t.Errorf("expected mod.Validate() error, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectValidateErr && err != nil {
|
||||||
|
t.Errorf("expected no error from mod.Validate(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUNO_Start(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mod UNO
|
||||||
|
expectStartErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 10,
|
||||||
|
listener: listenerMock{
|
||||||
|
startMock: func(logger *zap.Logger) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no long-running LibreOffice listener",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "start error",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 10,
|
||||||
|
listener: listenerMock{
|
||||||
|
startMock: func(logger *zap.Logger) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectStartErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.mod.Start()
|
||||||
|
|
||||||
|
if tc.expectStartErr && err == nil {
|
||||||
|
t.Errorf("expected mod.Start() error, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectStartErr && err != nil {
|
||||||
|
t.Errorf("expected no error from mod.Start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUNO_StartupMessage(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mod UNO
|
||||||
|
expectMessage string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "long-running LibreOffice listener started",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 10,
|
||||||
|
},
|
||||||
|
expectMessage: "Long-running LibreOffice listener started",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "long-running LibreOffice listener disabled",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
expectMessage: "Long-running LibreOffice listener disabled",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
actual := tc.mod.StartupMessage()
|
||||||
|
|
||||||
|
if tc.expectMessage != actual {
|
||||||
|
t.Errorf("expected '%s' from mod.StartupMessage(), but got '%s'", tc.expectMessage, actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUNO_Stop(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mod UNO
|
||||||
|
expectStopErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 10,
|
||||||
|
listener: listenerMock{
|
||||||
|
stopMock: func(logger *zap.Logger) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no long-running LibreOffice listener",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stop error",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 10,
|
||||||
|
listener: listenerMock{
|
||||||
|
stopMock: func(logger *zap.Logger) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
},
|
||||||
|
expectStopErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err := tc.mod.Stop(ctx)
|
||||||
|
|
||||||
|
if tc.expectStopErr && err == nil {
|
||||||
|
t.Errorf("expected mod.Stop() error, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectStopErr && err != nil {
|
||||||
|
t.Errorf("expected no error from mod.Stop(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUNO_Metrics(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mod UNO
|
||||||
|
expectUnoconvActiveInstancesCount float64
|
||||||
|
expectLibreOfficeListenerActiveInstancesCount float64
|
||||||
|
expectLibreOfficeListenerQueueLength float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with healthy long-running LibreOffice listener",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 10,
|
||||||
|
listener: listenerMock{
|
||||||
|
queueMock: func() int {
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
healthyMock: func() bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectLibreOfficeListenerActiveInstancesCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with unhealthy long-running LibreOffice listener",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 10,
|
||||||
|
listener: listenerMock{
|
||||||
|
queueMock: func() int {
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
healthyMock: func() bool {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with no long-running LibreOffice listener",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
listener: listenerMock{
|
||||||
|
queueMock: func() int {
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
healthyMock: func() bool {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with a queue of 3",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
listener: listenerMock{
|
||||||
|
queueMock: func() int {
|
||||||
|
return 3
|
||||||
|
},
|
||||||
|
healthyMock: func() bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectLibreOfficeListenerQueueLength: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
metrics, err := tc.mod.Metrics()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from mod.Metrics(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, metric := range metrics {
|
||||||
|
switch metric.Name {
|
||||||
|
case "unoconv_active_instances_count":
|
||||||
|
actual := metric.Read()
|
||||||
|
if actual != tc.expectUnoconvActiveInstancesCount {
|
||||||
|
t.Errorf("expected 'unoconv_active_instances_count' to be %.0f, but got %.0f", tc.expectUnoconvActiveInstancesCount, actual)
|
||||||
|
}
|
||||||
|
case "libreoffice_listener_active_instances_count":
|
||||||
|
actual := metric.Read()
|
||||||
|
if actual != tc.expectLibreOfficeListenerActiveInstancesCount {
|
||||||
|
t.Errorf("expected 'libreoffice_listener_active_instances_count' to be %.0f, but got %.0f", tc.expectLibreOfficeListenerActiveInstancesCount, actual)
|
||||||
|
}
|
||||||
|
case "unoconv_listener_active_instances_count":
|
||||||
|
actual := metric.Read()
|
||||||
|
if actual != tc.expectLibreOfficeListenerActiveInstancesCount {
|
||||||
|
t.Errorf("expected 'unoconv_listener_active_instances_count' to be %.0f, but got %.0f", tc.expectLibreOfficeListenerActiveInstancesCount, actual)
|
||||||
|
}
|
||||||
|
case "libreoffice_listener_queue_length":
|
||||||
|
actual := metric.Read()
|
||||||
|
if actual != tc.expectLibreOfficeListenerQueueLength {
|
||||||
|
t.Errorf("expected 'libreoffice_listener_queue_length' to be %.0f, but got %.0f", tc.expectLibreOfficeListenerQueueLength, actual)
|
||||||
|
}
|
||||||
|
case "unoconv_listener_queue_length":
|
||||||
|
actual := metric.Read()
|
||||||
|
if actual != tc.expectLibreOfficeListenerQueueLength {
|
||||||
|
t.Errorf("expected 'unoconv_listener_queue_length' to be %.0f, but got %.0f", tc.expectLibreOfficeListenerQueueLength, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUNO_Checks(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mod UNO
|
||||||
|
expectAvailabilityStatus health.AvailabilityStatus
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no long-running LibreOffice listener",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with healthy long-running LibreOffice listener",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 10,
|
||||||
|
listener: listenerMock{
|
||||||
|
healthyMock: func() bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectAvailabilityStatus: health.StatusUp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with unhealthy long-running LibreOffice listener",
|
||||||
|
mod: UNO{
|
||||||
|
libreOfficeRestartThreshold: 10,
|
||||||
|
listener: listenerMock{
|
||||||
|
healthyMock: func() bool {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectAvailabilityStatus: health.StatusDown,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
checks, err := tc.mod.Checks()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from mod.Checks(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(checks) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(checks) != 1 {
|
||||||
|
t.Fatalf("expected 1 check from mod.Checks(), but got %d", len(checks))
|
||||||
|
}
|
||||||
|
|
||||||
|
checker := health.NewChecker(checks...)
|
||||||
|
result := checker.Check(context.Background())
|
||||||
|
|
||||||
|
if result.Status != tc.expectAvailabilityStatus {
|
||||||
|
t.Errorf("expected '%s' as availability status, but got '%s'", tc.expectAvailabilityStatus, result.Status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUNO_PDF(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mod UNO
|
||||||
|
ctx context.Context
|
||||||
|
logger *zap.Logger
|
||||||
|
inputPath string
|
||||||
|
options Options
|
||||||
|
expectPDFErr bool
|
||||||
|
teardown func(mod UNO) error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nominal behavior with no long-running LibreOffice listener",
|
||||||
|
mod: UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nominal behavior with a long-running LibreOffice listener",
|
||||||
|
mod: func() UNO {
|
||||||
|
mod := UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 10,
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
}
|
||||||
|
mod.listener = newLibreOfficeListener(
|
||||||
|
mod.logger,
|
||||||
|
mod.libreOfficeBinPath,
|
||||||
|
mod.libreOfficeStartTimeout,
|
||||||
|
mod.libreOfficeRestartThreshold,
|
||||||
|
)
|
||||||
|
|
||||||
|
err := mod.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from mod.Start(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mod
|
||||||
|
}(),
|
||||||
|
ctx: context.Background(),
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
teardown: func(mod UNO) error {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
return mod.Stop(ctx)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "convert with a debug logger",
|
||||||
|
mod: UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
logger: zap.NewExample(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "convert with landscape",
|
||||||
|
mod: UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
options: Options{
|
||||||
|
Landscape: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "convert with page ranges",
|
||||||
|
mod: UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
options: Options{
|
||||||
|
PageRanges: "1-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "convert with invalid page ranges",
|
||||||
|
mod: UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
options: Options{
|
||||||
|
PageRanges: "foo",
|
||||||
|
},
|
||||||
|
expectPDFErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "convert to PDF/A-1a",
|
||||||
|
mod: UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
options: Options{
|
||||||
|
PDFformat: gotenberg.FormatPDFA1a,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "convert to PDF/A-2b",
|
||||||
|
mod: UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
options: Options{
|
||||||
|
PDFformat: gotenberg.FormatPDFA2b,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "convert to PDF/A-3b",
|
||||||
|
mod: UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
options: Options{
|
||||||
|
PDFformat: gotenberg.FormatPDFA3b,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "convert to invalid PDF format",
|
||||||
|
mod: UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
options: Options{
|
||||||
|
PDFformat: "foo",
|
||||||
|
},
|
||||||
|
expectPDFErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil context",
|
||||||
|
mod: UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
ctx: nil,
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
expectPDFErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired context",
|
||||||
|
mod: UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 0,
|
||||||
|
},
|
||||||
|
ctx: func() context.Context {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
expectPDFErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cannot lock long-running LibreOffice listener",
|
||||||
|
mod: UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 10,
|
||||||
|
listener: listenerMock{
|
||||||
|
lockMock: func(ctx context.Context, logger *zap.Logger) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
expectPDFErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cannot unlock long-running LibreOffice listener",
|
||||||
|
mod: UNO{
|
||||||
|
unoconvBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||||
|
libreOfficeBinPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||||
|
libreOfficeStartTimeout: time.Duration(10) * time.Second,
|
||||||
|
libreOfficeRestartThreshold: 10,
|
||||||
|
listener: listenerMock{
|
||||||
|
lockMock: func(ctx context.Context, logger *zap.Logger) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
unlockMock: func(logger *zap.Logger) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
portMock: func() int {
|
||||||
|
return 2002
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
},
|
||||||
|
ctx: nil,
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
||||||
|
expectPDFErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if tc.teardown == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := tc.teardown(tc.mod)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from tc.teardown(), but got: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
outputDir, err := gotenberg.MkdirAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from gotenberg.MkdirAll(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := os.RemoveAll(outputDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from os.RemoveAll(), but got: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = tc.mod.PDF(tc.ctx, tc.logger, tc.inputPath, outputDir+"/foo.pdf", tc.options)
|
||||||
|
|
||||||
|
if tc.expectPDFErr && err == nil {
|
||||||
|
t.Fatalf("expected mod.PDF() error, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectPDFErr && err != nil {
|
||||||
|
t.Fatalf("expected no error from mod.PDF(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUNO_Extensions(t *testing.T) {
|
||||||
|
mod := new(UNO)
|
||||||
|
extensions := mod.Extensions()
|
||||||
|
|
||||||
|
actual := len(extensions)
|
||||||
|
expect := 76
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected %d extensions, but got %d", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUNO_UNO(t *testing.T) {
|
||||||
|
mod := new(UNO)
|
||||||
|
|
||||||
|
_, err := mod.UNO()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from mod.UNO(), but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type listenerMock struct {
|
||||||
|
startMock func(logger *zap.Logger) error
|
||||||
|
stopMock func(logger *zap.Logger) error
|
||||||
|
lockMock func(ctx context.Context, logger *zap.Logger) error
|
||||||
|
unlockMock func(logger *zap.Logger) error
|
||||||
|
portMock func() int
|
||||||
|
queueMock func() int
|
||||||
|
healthyMock func() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener listenerMock) start(logger *zap.Logger) error {
|
||||||
|
return listener.startMock(logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener listenerMock) stop(logger *zap.Logger) error {
|
||||||
|
return listener.stopMock(logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener listenerMock) lock(ctx context.Context, logger *zap.Logger) error {
|
||||||
|
return listener.lockMock(ctx, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener listenerMock) unlock(logger *zap.Logger) error {
|
||||||
|
return listener.unlockMock(logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener listenerMock) port() int {
|
||||||
|
return listener.portMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener listenerMock) queue() int {
|
||||||
|
return listener.queueMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener listenerMock) healthy() bool {
|
||||||
|
return listener.healthyMock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards.
|
||||||
|
var (
|
||||||
|
_ listener = (*listenerMock)(nil)
|
||||||
|
)
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
// Package unoconv provides a module which abstracts the CLI tool unoconv.
|
|
||||||
package unoconv
|
|
||||||
@@ -1,482 +0,0 @@
|
|||||||
package unoconv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
flag "github.com/spf13/pflag"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
gotenberg.MustRegisterModule(Unoconv{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrMalformedPageRanges happens if the page ranges option cannot be
|
|
||||||
// interpreted by LibreOffice.
|
|
||||||
var ErrMalformedPageRanges = errors.New("page ranges are malformed")
|
|
||||||
|
|
||||||
// Unoconv is a module which provides an API to interact with unoconv.
|
|
||||||
type Unoconv struct {
|
|
||||||
binPath string
|
|
||||||
disableListener bool
|
|
||||||
|
|
||||||
listenerCmd gotenberg.Cmd
|
|
||||||
listenerPort int
|
|
||||||
logger *zap.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options gathers available options when converting a document to PDF.
|
|
||||||
type Options struct {
|
|
||||||
// Landscape allows to change the orientation of the resulting PDF.
|
|
||||||
// Optional.
|
|
||||||
Landscape bool
|
|
||||||
|
|
||||||
// PageRanges allows to select the pages to convert.
|
|
||||||
// TODO: should prefer a method form PDFEngine.
|
|
||||||
// Optional.
|
|
||||||
PageRanges string
|
|
||||||
|
|
||||||
// PDFArchive allows to convert the resulting PDF to PDF/A-1a.
|
|
||||||
// In a module, prefer the Convert method from the gotenberg.PDFEngine
|
|
||||||
// interface.
|
|
||||||
// Optional.
|
|
||||||
PDFArchive bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// API is an abstraction on top of unoconv.
|
|
||||||
//
|
|
||||||
// See https://github.com/unoconv/unoconv.
|
|
||||||
type API interface {
|
|
||||||
PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
|
|
||||||
Extensions() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider is a module interface which exposes a method for creating an API
|
|
||||||
// for other modules.
|
|
||||||
//
|
|
||||||
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
|
||||||
// provider, _ := ctx.Module(new(unoconv.Provider))
|
|
||||||
// uno, _ := provider.(unoconv.Provider).Unoconv()
|
|
||||||
// }
|
|
||||||
type Provider interface {
|
|
||||||
Unoconv() (API, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Descriptor returns a Unoconv's module descriptor.
|
|
||||||
func (Unoconv) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{
|
|
||||||
ID: "unoconv",
|
|
||||||
FlagSet: func() *flag.FlagSet {
|
|
||||||
fs := flag.NewFlagSet("unoconv", flag.ExitOnError)
|
|
||||||
fs.Bool("unoconv-disable-listener", false, "Do not start a unoconv listener - save resources in detriment of performance")
|
|
||||||
|
|
||||||
return fs
|
|
||||||
}(),
|
|
||||||
New: func() gotenberg.Module { return new(Unoconv) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provision sets the module properties. It returns an error if the environment
|
|
||||||
// variable UNOCONV_BIN_PATH is not set.
|
|
||||||
func (mod *Unoconv) Provision(ctx *gotenberg.Context) error {
|
|
||||||
flags := ctx.ParsedFlags()
|
|
||||||
mod.disableListener = flags.MustBool("unoconv-disable-listener")
|
|
||||||
|
|
||||||
binPath, ok := os.LookupEnv("UNOCONV_BIN_PATH")
|
|
||||||
if !ok {
|
|
||||||
return errors.New("UNOCONV_BIN_PATH environment variable is not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.binPath = binPath
|
|
||||||
|
|
||||||
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get logger provider: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger, err := loggerProvider.(gotenberg.LoggerProvider).Logger(mod)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get logger: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.logger = logger
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate validates the module properties.
|
|
||||||
func (mod Unoconv) Validate() error {
|
|
||||||
_, err := os.Stat(mod.binPath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("unoconv binary path does not exist: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod *Unoconv) Start() error {
|
|
||||||
if mod.disableListener {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
port, err := freePort(mod.logger)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get free port: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.listenerPort = port
|
|
||||||
|
|
||||||
args := []string{
|
|
||||||
"--listener",
|
|
||||||
"--user-profile",
|
|
||||||
// Just to make sure LibreOffice does not leak files in an unknown
|
|
||||||
// directory. The directory will be removed anyway by the garbage
|
|
||||||
// collector.
|
|
||||||
fmt.Sprintf("//%s", gotenberg.NewDirPath()),
|
|
||||||
"--port",
|
|
||||||
fmt.Sprintf("%d", mod.listenerPort),
|
|
||||||
}
|
|
||||||
|
|
||||||
checkedEntry := mod.logger.Check(zap.DebugLevel, "check for debug level before setting high verbosity")
|
|
||||||
if checkedEntry != nil {
|
|
||||||
args = append(args, "-vvv")
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.listenerCmd = gotenberg.Command(mod.logger, mod.binPath, args...)
|
|
||||||
|
|
||||||
err = mod.listenerCmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("start unoconv listener: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
listenerActiveInstancesCountMu.Lock()
|
|
||||||
listenerActiveInstancesCount += 1
|
|
||||||
listenerActiveInstancesCountMu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartupMessage returns a custom startup message.
|
|
||||||
func (mod Unoconv) StartupMessage() string {
|
|
||||||
if mod.disableListener {
|
|
||||||
return "listener disabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("listener started on port %d", mod.listenerPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops the HTTP server.
|
|
||||||
func (mod *Unoconv) Stop(ctx context.Context) error {
|
|
||||||
if mod.disableListener {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok := ctx.Deadline()
|
|
||||||
if !ok {
|
|
||||||
return errors.New("no context dead line")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block until the context is done so that other module may gracefully stop
|
|
||||||
// before we do a shutdown cleanup.
|
|
||||||
mod.logger.Debug("wait for the end of grace duration")
|
|
||||||
|
|
||||||
<-ctx.Done()
|
|
||||||
|
|
||||||
err := mod.listenerCmd.Kill()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("kill unoconv listener: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
listenerActiveInstancesCountMu.Lock()
|
|
||||||
listenerActiveInstancesCount -= 1
|
|
||||||
listenerActiveInstancesCountMu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metrics returns the metrics.
|
|
||||||
func (mod Unoconv) Metrics() ([]gotenberg.Metric, error) {
|
|
||||||
return []gotenberg.Metric{
|
|
||||||
{
|
|
||||||
Name: "unoconv_active_instances_count",
|
|
||||||
Description: "Current number of active unoconv instances.",
|
|
||||||
Read: func() float64 {
|
|
||||||
activeInstancesCountMu.RLock()
|
|
||||||
defer activeInstancesCountMu.RUnlock()
|
|
||||||
|
|
||||||
return activeInstancesCount
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "unoconv_listener_active_instances_count",
|
|
||||||
Description: "Current number of active unoconv listener instances.",
|
|
||||||
Read: func() float64 {
|
|
||||||
listenerActiveInstancesCountMu.RLock()
|
|
||||||
defer listenerActiveInstancesCountMu.RUnlock()
|
|
||||||
|
|
||||||
return listenerActiveInstancesCount
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "unoconv_listener_queue_length",
|
|
||||||
Description: "Current number of processes in the queue.",
|
|
||||||
Read: func() float64 {
|
|
||||||
listenerQueueLengthMu.RLock()
|
|
||||||
defer listenerQueueLengthMu.RUnlock()
|
|
||||||
|
|
||||||
return listenerQueueLength
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unoconv returns an API for interacting with unoconv.
|
|
||||||
func (mod *Unoconv) Unoconv() (API, error) {
|
|
||||||
return mod, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PDF converts a document to PDF.
|
|
||||||
//
|
|
||||||
// In stateless mode, it creates a dedicated LibreOffice instance thanks to a
|
|
||||||
// custom user profile directory and a free port. Substantial calls to this
|
|
||||||
// method may increase CPU and memory usage drastically. In such a scenario,
|
|
||||||
// the given context may also be done before the end of the conversion.
|
|
||||||
//
|
|
||||||
// In listener mode, it calls the unoconv listener to interact with
|
|
||||||
// LibreOffice, improving substantially the performance. However, it cannot
|
|
||||||
// perform parallel operations and have to wait for the lock to be available.
|
|
||||||
func (mod Unoconv) PDF(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
|
||||||
args := []string{
|
|
||||||
"--format",
|
|
||||||
"pdf",
|
|
||||||
}
|
|
||||||
|
|
||||||
var userProfileDirPath string
|
|
||||||
|
|
||||||
if mod.disableListener {
|
|
||||||
port, err := freePort(logger)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get free port: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
userProfileDirPath = gotenberg.NewDirPath()
|
|
||||||
|
|
||||||
args = append(args,
|
|
||||||
"--port",
|
|
||||||
fmt.Sprintf("%d", port),
|
|
||||||
"--user-profile",
|
|
||||||
fmt.Sprintf("//%s", userProfileDirPath),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
args = append(args, "--port", fmt.Sprintf("%d", mod.listenerPort))
|
|
||||||
}
|
|
||||||
|
|
||||||
checkedEntry := logger.Check(zap.DebugLevel, "check for debug level before setting high verbosity")
|
|
||||||
if checkedEntry != nil {
|
|
||||||
args = append(args, "-vvv")
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.Landscape {
|
|
||||||
args = append(args, "--printer", "PaperOrientation=landscape")
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.PageRanges != "" {
|
|
||||||
args = append(args, "--export", fmt.Sprintf("PageRange=%s", options.PageRanges))
|
|
||||||
}
|
|
||||||
|
|
||||||
if options.PDFArchive {
|
|
||||||
args = append(args, "--export", "SelectPdfVersion=1")
|
|
||||||
}
|
|
||||||
|
|
||||||
args = append(args, "--output", outputPath, inputPath)
|
|
||||||
|
|
||||||
if !mod.disableListener {
|
|
||||||
listenerQueueLengthMu.Lock()
|
|
||||||
listenerQueueLength += 1
|
|
||||||
listenerQueueLengthMu.Unlock()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case listenerLock <- struct{}{}:
|
|
||||||
logger.Debug("unoconv lock acquired")
|
|
||||||
|
|
||||||
listenerQueueLengthMu.Lock()
|
|
||||||
listenerQueueLength -= 1
|
|
||||||
listenerQueueLengthMu.Unlock()
|
|
||||||
|
|
||||||
break
|
|
||||||
case <-ctx.Done():
|
|
||||||
logger.Debug("failed to acquire the unoconv lock before deadline")
|
|
||||||
|
|
||||||
listenerQueueLengthMu.Lock()
|
|
||||||
listenerQueueLength -= 1
|
|
||||||
listenerQueueLengthMu.Unlock()
|
|
||||||
|
|
||||||
return fmt.Errorf("acquire unoconv lock: %w", ctx.Err())
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
<-listenerLock
|
|
||||||
logger.Debug("unoconv lock released")
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd, err := gotenberg.CommandContext(ctx, logger, mod.binPath, args...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create unoconv command: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug(fmt.Sprintf("print to PDF with: %+v", options))
|
|
||||||
|
|
||||||
activeInstancesCountMu.Lock()
|
|
||||||
activeInstancesCount += 1
|
|
||||||
activeInstancesCountMu.Unlock()
|
|
||||||
|
|
||||||
err = cmd.Exec()
|
|
||||||
|
|
||||||
activeInstancesCountMu.Lock()
|
|
||||||
activeInstancesCount -= 1
|
|
||||||
activeInstancesCountMu.Unlock()
|
|
||||||
|
|
||||||
if mod.disableListener {
|
|
||||||
// Always remove the user profile directory created by LibreOffice.
|
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/192.
|
|
||||||
go func() {
|
|
||||||
logger.Debug(fmt.Sprintf("remove user profile directory '%s'", userProfileDirPath))
|
|
||||||
|
|
||||||
err := os.RemoveAll(userProfileDirPath)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(fmt.Sprintf("remove user profile directory: %s", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unoconv/LibreOffice errors are not explicit.
|
|
||||||
// That's why we have to make an educated guess according to the exit code
|
|
||||||
// and given inputs.
|
|
||||||
|
|
||||||
if strings.Contains(err.Error(), "exit status 5") && options.PageRanges != "" {
|
|
||||||
return ErrMalformedPageRanges
|
|
||||||
}
|
|
||||||
|
|
||||||
// Possible errors:
|
|
||||||
// 1. Unoconv/LibreOffice failed for some reason.
|
|
||||||
// 2. Context done.
|
|
||||||
//
|
|
||||||
// On the second scenario, LibreOffice might not had time to remove some of
|
|
||||||
// its temporary files, as it has been killed without warning. The garbage
|
|
||||||
// collector will delete them for us (if the module is loaded).
|
|
||||||
return fmt.Errorf("unoconv PDF: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extensions returns the file extensions available with unoconv.
|
|
||||||
func (mod Unoconv) Extensions() []string {
|
|
||||||
return []string{
|
|
||||||
".bib",
|
|
||||||
".doc",
|
|
||||||
".xml",
|
|
||||||
".docx",
|
|
||||||
".fodt",
|
|
||||||
".html",
|
|
||||||
".ltx",
|
|
||||||
".txt",
|
|
||||||
".odt",
|
|
||||||
".ott",
|
|
||||||
".pdb",
|
|
||||||
".pdf",
|
|
||||||
".psw",
|
|
||||||
".rtf",
|
|
||||||
".sdw",
|
|
||||||
".stw",
|
|
||||||
".sxw",
|
|
||||||
".uot",
|
|
||||||
".vor",
|
|
||||||
".wps",
|
|
||||||
".epub",
|
|
||||||
".png",
|
|
||||||
".bmp",
|
|
||||||
".emf",
|
|
||||||
".eps",
|
|
||||||
".fodg",
|
|
||||||
".gif",
|
|
||||||
".jpg",
|
|
||||||
".jpeg",
|
|
||||||
".met",
|
|
||||||
".odd",
|
|
||||||
".otg",
|
|
||||||
".pbm",
|
|
||||||
".pct",
|
|
||||||
".pgm",
|
|
||||||
".ppm",
|
|
||||||
".ras",
|
|
||||||
".std",
|
|
||||||
".svg",
|
|
||||||
".svm",
|
|
||||||
".swf",
|
|
||||||
".sxd",
|
|
||||||
".sxw",
|
|
||||||
".tif",
|
|
||||||
".tiff",
|
|
||||||
".xhtml",
|
|
||||||
".xpm",
|
|
||||||
".odp",
|
|
||||||
".fodp",
|
|
||||||
".potm",
|
|
||||||
".pot",
|
|
||||||
".pptx",
|
|
||||||
".pps",
|
|
||||||
".ppt",
|
|
||||||
".pwp",
|
|
||||||
".sda",
|
|
||||||
".sdd",
|
|
||||||
".sti",
|
|
||||||
".sxi",
|
|
||||||
".uop",
|
|
||||||
".wmf",
|
|
||||||
".csv",
|
|
||||||
".dbf",
|
|
||||||
".dif",
|
|
||||||
".fods",
|
|
||||||
".ods",
|
|
||||||
".ots",
|
|
||||||
".pxl",
|
|
||||||
".sdc",
|
|
||||||
".slk",
|
|
||||||
".stc",
|
|
||||||
".sxc",
|
|
||||||
".uos",
|
|
||||||
".xls",
|
|
||||||
".xlt",
|
|
||||||
".xlsx",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
listenerLock = make(chan struct{}, 1)
|
|
||||||
listenerQueueLength float64
|
|
||||||
listenerQueueLengthMu sync.RWMutex
|
|
||||||
listenerActiveInstancesCount float64
|
|
||||||
listenerActiveInstancesCountMu sync.RWMutex
|
|
||||||
activeInstancesCount float64
|
|
||||||
activeInstancesCountMu sync.RWMutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*Unoconv)(nil)
|
|
||||||
_ gotenberg.Provisioner = (*Unoconv)(nil)
|
|
||||||
_ gotenberg.Validator = (*Unoconv)(nil)
|
|
||||||
_ gotenberg.App = (*Unoconv)(nil)
|
|
||||||
_ gotenberg.MetricsProvider = (*Unoconv)(nil)
|
|
||||||
_ API = (*Unoconv)(nil)
|
|
||||||
_ Provider = (*Unoconv)(nil)
|
|
||||||
)
|
|
||||||
@@ -1,453 +0,0 @@
|
|||||||
package unoconv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProtoModule struct {
|
|
||||||
descriptor func() gotenberg.ModuleDescriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return mod.descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoValidator struct {
|
|
||||||
ProtoModule
|
|
||||||
validate func() error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoValidator) Validate() error {
|
|
||||||
return mod.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoLoggerProvider struct {
|
|
||||||
ProtoModule
|
|
||||||
logger func(mod gotenberg.Module) (*zap.Logger, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory ProtoLoggerProvider) Logger(mod gotenberg.Module) (*zap.Logger, error) {
|
|
||||||
return factory.logger(mod)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_Descriptor(t *testing.T) {
|
|
||||||
descriptor := Unoconv{}.Descriptor()
|
|
||||||
|
|
||||||
actual := reflect.TypeOf(descriptor.New())
|
|
||||||
expect := reflect.TypeOf(new(Unoconv))
|
|
||||||
|
|
||||||
if actual != expect {
|
|
||||||
t.Errorf("expected '%'s' but got '%s'", expect, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_Provision(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
ctx *gotenberg.Context
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
ctx: gotenberg.NewContext(
|
|
||||||
gotenberg.ParsedFlags{FlagSet: new(Unoconv).Descriptor().FlagSet},
|
|
||||||
make([]gotenberg.ModuleDescriptor, 0),
|
|
||||||
),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
mod := struct {
|
|
||||||
ProtoLoggerProvider
|
|
||||||
}{}
|
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
|
||||||
}
|
|
||||||
mod.logger = func(mod gotenberg.Module) (*zap.Logger, error) { return nil, errors.New("foo") }
|
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
|
||||||
gotenberg.ParsedFlags{
|
|
||||||
FlagSet: new(Unoconv).Descriptor().FlagSet,
|
|
||||||
},
|
|
||||||
[]gotenberg.ModuleDescriptor{
|
|
||||||
mod.Descriptor(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
mod := struct {
|
|
||||||
ProtoLoggerProvider
|
|
||||||
}{}
|
|
||||||
mod.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return mod }}
|
|
||||||
}
|
|
||||||
mod.logger = func(mod gotenberg.Module) (*zap.Logger, error) { return zap.NewNop(), nil }
|
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
|
||||||
gotenberg.ParsedFlags{
|
|
||||||
FlagSet: new(Unoconv).Descriptor().FlagSet,
|
|
||||||
},
|
|
||||||
[]gotenberg.ModuleDescriptor{
|
|
||||||
mod.Descriptor(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
mod := new(Unoconv)
|
|
||||||
err := mod.Provision(tc.ctx)
|
|
||||||
|
|
||||||
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 TestUnoconv_Validate(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
binPath string
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
binPath: "/foo",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
mod := Unoconv{
|
|
||||||
binPath: tc.binPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := mod.Validate()
|
|
||||||
|
|
||||||
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 TestUnoconv_Start(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
mod *Unoconv
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
mod: &Unoconv{
|
|
||||||
disableListener: true,
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mod: &Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
disableListener: false,
|
|
||||||
logger: zap.NewExample(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
func() {
|
|
||||||
err := tc.mod.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(1)*time.Nanosecond)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err = tc.mod.Stop(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("test %d: expected not error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_StartupMessage(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
disableListener bool
|
|
||||||
expectMessage string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
disableListener: true,
|
|
||||||
expectMessage: "listener disabled",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expectMessage: "listener started on port 0",
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
mod := Unoconv{
|
|
||||||
disableListener: tc.disableListener,
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := mod.StartupMessage()
|
|
||||||
if actual != tc.expectMessage {
|
|
||||||
t.Errorf("test %d: expected '%s' but got '%s'", i, tc.expectMessage, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_Stop(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
start bool
|
|
||||||
disableListener bool
|
|
||||||
timeout time.Duration
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
disableListener: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
start: true,
|
|
||||||
timeout: time.Duration(1) * time.Nanosecond,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
func() {
|
|
||||||
mod := &Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
disableListener: tc.disableListener,
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.start {
|
|
||||||
err := mod.Start()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if tc.timeout == 0 {
|
|
||||||
err = mod.Stop(context.TODO())
|
|
||||||
} else {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tc.timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err = mod.Stop(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 TestUnoconv_Metrics(t *testing.T) {
|
|
||||||
metrics, err := new(Unoconv).Metrics()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(metrics) != 3 {
|
|
||||||
t.Fatalf("expected %d metrics, but got %d", 1, len(metrics))
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := metrics[0].Read()
|
|
||||||
if actual != 0 {
|
|
||||||
t.Errorf("expected %d unoconv instances, but got %f", 0, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual = metrics[1].Read()
|
|
||||||
if actual != 0 {
|
|
||||||
t.Errorf("expected %d unoconv listener instances, but got %f", 0, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual = metrics[2].Read()
|
|
||||||
if actual != 0 {
|
|
||||||
t.Errorf("expected %d processes in the queue, but got %f", 0, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_Unoconv(t *testing.T) {
|
|
||||||
mod := new(Unoconv)
|
|
||||||
|
|
||||||
_, err := mod.Unoconv()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_PDF(t *testing.T) {
|
|
||||||
for i, tc := range []struct {
|
|
||||||
ctx context.Context
|
|
||||||
mod Unoconv
|
|
||||||
logger *zap.Logger
|
|
||||||
inputPath string
|
|
||||||
options Options
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
mod: Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
disableListener: true,
|
|
||||||
},
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: context.Background(),
|
|
||||||
mod: Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
disableListener: true,
|
|
||||||
},
|
|
||||||
logger: zap.NewExample(),
|
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
|
||||||
options: Options{
|
|
||||||
Landscape: true,
|
|
||||||
PageRanges: "1-2",
|
|
||||||
PDFArchive: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: context.Background(),
|
|
||||||
mod: Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
disableListener: true,
|
|
||||||
},
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
|
||||||
options: Options{
|
|
||||||
PageRanges: "foo",
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() context.Context {
|
|
||||||
ctx, cancel := context.WithCancel(context.TODO())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
mod: Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
disableListener: true,
|
|
||||||
},
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: context.Background(),
|
|
||||||
mod: Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
},
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() context.Context {
|
|
||||||
ctx, cancel := context.WithCancel(context.TODO())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
mod: Unoconv{
|
|
||||||
binPath: os.Getenv("UNOCONV_BIN_PATH"),
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
},
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
inputPath: "/tests/test/testdata/libreoffice/sample1.docx",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
func() {
|
|
||||||
outputDir, err := gotenberg.MkdirAll()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
err := os.RemoveAll(outputDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if !tc.mod.disableListener {
|
|
||||||
err = tc.mod.Start()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's give it some room to start.
|
|
||||||
time.Sleep(time.Duration(1) * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tc.mod.PDF(tc.ctx, tc.logger, tc.inputPath, outputDir+"/foo.pdf", tc.options)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.mod.disableListener {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(1)*time.Nanosecond)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
err = tc.mod.Stop(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnoconv_Extensions(t *testing.T) {
|
|
||||||
mod := new(Unoconv)
|
|
||||||
extensions := mod.Extensions()
|
|
||||||
|
|
||||||
actual := len(extensions)
|
|
||||||
expect := 76
|
|
||||||
|
|
||||||
if actual != expect {
|
|
||||||
t.Errorf("expected %d extensions but got %d", expect, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*ProtoModule)(nil)
|
|
||||||
_ gotenberg.Validator = (*ProtoValidator)(nil)
|
|
||||||
_ gotenberg.LoggerProvider = (*ProtoLoggerProvider)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoLoggerProvider)(nil)
|
|
||||||
)
|
|
||||||
@@ -3,213 +3,178 @@ package pdfengines
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewMultiPDFEngines(t *testing.T) {
|
|
||||||
engine1 := &ProtoPDFEngine{
|
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
engine2 := &ProtoPDFEngine{
|
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
multi := newMultiPDFEngines(engine1, engine2)
|
|
||||||
|
|
||||||
if len(multi.engines) != 2 {
|
|
||||||
t.Fatalf("expected %d engines but got %d", 2, len(multi.engines))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(engine1, multi.engines[0]) {
|
|
||||||
t.Errorf("expected %v, but got: %v", engine1, multi.engines[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(engine2, multi.engines[1]) {
|
|
||||||
t.Errorf("expected %v, but got: %v", engine2, multi.engines[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultiPDFEngines_Merge(t *testing.T) {
|
func TestMultiPDFEngines_Merge(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
ctx context.Context
|
name string
|
||||||
engines []gotenberg.PDFEngine
|
engine *multiPDFEngines
|
||||||
expectErr bool
|
ctx context.Context
|
||||||
|
expectMergeErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
name: "nominal behavior",
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engine: newMultiPDFEngines(
|
||||||
return []gotenberg.PDFEngine{
|
gotenberg.PDFEngineMock{
|
||||||
ProtoPDFEngine{
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
return nil
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}(),
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
name: "at least one engine does not return an error",
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engine: newMultiPDFEngines(
|
||||||
return []gotenberg.PDFEngine{
|
gotenberg.PDFEngineMock{
|
||||||
ProtoPDFEngine{
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
return errors.New("foo")
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ProtoPDFEngine{
|
},
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
gotenberg.PDFEngineMock{
|
||||||
return nil
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
},
|
return nil
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}(),
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
name: "all engines return an error",
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engine: newMultiPDFEngines(
|
||||||
return []gotenberg.PDFEngine{
|
gotenberg.PDFEngineMock{
|
||||||
ProtoPDFEngine{
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
return errors.New("foo")
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ProtoPDFEngine{
|
},
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
gotenberg.PDFEngineMock{
|
||||||
return errors.New("bar")
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
},
|
return errors.New("foo")
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}(),
|
),
|
||||||
expectErr: true,
|
ctx: context.Background(),
|
||||||
|
expectMergeErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "context expired",
|
||||||
|
engine: newMultiPDFEngines(
|
||||||
|
gotenberg.PDFEngineMock{
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
ctx: func() context.Context {
|
ctx: func() context.Context {
|
||||||
ctx, cancel := context.WithCancel(context.TODO())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
cancel()
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engines: func() []gotenberg.PDFEngine {
|
expectMergeErr: true,
|
||||||
return []gotenberg.PDFEngine{
|
|
||||||
ProtoPDFEngine{
|
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
multi := newMultiPDFEngines(tc.engines...)
|
|
||||||
err := multi.Merge(tc.ctx, nil, nil, "")
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
for _, tc := range tests {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
}
|
err := tc.engine.Merge(tc.ctx, zap.NewNop(), nil, "")
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectMergeErr && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Errorf("expected engine.Merge() error, but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !tc.expectMergeErr && err != nil {
|
||||||
|
t.Errorf("expected no error from engine.Merge(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiPDFEngines_Convert(t *testing.T) {
|
func TestMultiPDFEngines_Convert(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
ctx context.Context
|
name string
|
||||||
engines []gotenberg.PDFEngine
|
engine *multiPDFEngines
|
||||||
expectErr bool
|
ctx context.Context
|
||||||
|
expectConvertErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
name: "nominal behavior",
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engine: newMultiPDFEngines(
|
||||||
return []gotenberg.PDFEngine{
|
gotenberg.PDFEngineMock{
|
||||||
ProtoPDFEngine{
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
return nil
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}(),
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
name: "at least one engine does not return an error",
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engine: newMultiPDFEngines(
|
||||||
return []gotenberg.PDFEngine{
|
gotenberg.PDFEngineMock{
|
||||||
ProtoPDFEngine{
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
return errors.New("foo")
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ProtoPDFEngine{
|
},
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
gotenberg.PDFEngineMock{
|
||||||
return nil
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
},
|
return nil
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}(),
|
),
|
||||||
|
ctx: context.Background(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ctx: context.TODO(),
|
name: "all engines return an error",
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engine: newMultiPDFEngines(
|
||||||
return []gotenberg.PDFEngine{
|
gotenberg.PDFEngineMock{
|
||||||
ProtoPDFEngine{
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
return errors.New("foo")
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
ProtoPDFEngine{
|
},
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
gotenberg.PDFEngineMock{
|
||||||
return errors.New("bar")
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
},
|
return errors.New("foo")
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}(),
|
),
|
||||||
expectErr: true,
|
ctx: context.Background(),
|
||||||
|
expectConvertErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "context expired",
|
||||||
|
engine: newMultiPDFEngines(
|
||||||
|
gotenberg.PDFEngineMock{
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
ctx: func() context.Context {
|
ctx: func() context.Context {
|
||||||
ctx, cancel := context.WithCancel(context.TODO())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
cancel()
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engines: func() []gotenberg.PDFEngine {
|
expectConvertErr: true,
|
||||||
return []gotenberg.PDFEngine{
|
|
||||||
ProtoPDFEngine{
|
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
multi := newMultiPDFEngines(tc.engines...)
|
|
||||||
err := multi.Convert(tc.ctx, nil, "", "", "")
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
for _, tc := range tests {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
}
|
err := tc.engine.Convert(tc.ctx, zap.NewNop(), "", "", "")
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
if tc.expectConvertErr && err == nil {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
t.Errorf("expected engine.Convert() error, but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !tc.expectConvertErr && err != nil {
|
||||||
|
t.Errorf("expected no error from engine.Convert(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,18 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
|
|||||||
names := flags.MustStringSlice("pdfengines-engines")
|
names := flags.MustStringSlice("pdfengines-engines")
|
||||||
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
||||||
|
|
||||||
|
loggerProvider, err := ctx.Module(new(gotenberg.LoggerProvider))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get logger provider: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err := loggerProvider.(gotenberg.LoggerProvider).Logger(mod)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get logger: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger = logger.Named("pdfengines")
|
||||||
|
|
||||||
engines, err := ctx.Modules(new(gotenberg.PDFEngine))
|
engines, err := ctx.Modules(new(gotenberg.PDFEngine))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get PDF engines: %w", err)
|
return fmt.Errorf("get PDF engines: %w", err)
|
||||||
@@ -68,6 +80,13 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
|
|||||||
// Selection from user.
|
// Selection from user.
|
||||||
mod.names = names
|
mod.names = names
|
||||||
|
|
||||||
|
for i, name := range names {
|
||||||
|
logger.Warn("unoconv-pdfengine is deprecated; prefer uno-pdfengine instead")
|
||||||
|
if name == "unoconv-pdfengine" {
|
||||||
|
mod.names[i] = "uno-pdfengine"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package pdfengines
|
package pdfengines
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -11,38 +10,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProtoModule struct {
|
func TestPDFEngines_Descriptor(t *testing.T) {
|
||||||
descriptor func() gotenberg.ModuleDescriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoModule) Descriptor() gotenberg.ModuleDescriptor {
|
|
||||||
return mod.descriptor()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoValidator struct {
|
|
||||||
ProtoModule
|
|
||||||
validate func() error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoValidator) Validate() error {
|
|
||||||
return mod.validate()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtoPDFEngine struct {
|
|
||||||
ProtoValidator
|
|
||||||
merge func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
|
||||||
convert func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoPDFEngine) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
|
||||||
return mod.merge(ctx, logger, inputPaths, outputPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mod ProtoPDFEngine) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
|
||||||
return mod.convert(ctx, logger, format, inputPath, outputPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPDFEngine_Descriptor(t *testing.T) {
|
|
||||||
descriptor := PDFEngines{}.Descriptor()
|
descriptor := PDFEngines{}.Descriptor()
|
||||||
|
|
||||||
actual := reflect.TypeOf(descriptor.New())
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
@@ -53,71 +21,98 @@ func TestPDFEngine_Descriptor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFEngine_Provision(t *testing.T) {
|
func TestPDFEngines_Provision(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
ctx *gotenberg.Context
|
name string
|
||||||
expectNames []string
|
ctx *gotenberg.Context
|
||||||
expectEnginesCount int
|
expectPDFEngineNames []string
|
||||||
expectErr bool
|
expectProvisionErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
name: "no selection from user",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
engine := struct{ ProtoPDFEngine }{}
|
provider := struct {
|
||||||
engine.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }}
|
gotenberg.LoggerProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return zap.NewNop(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
engine := struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.ValidatorMock
|
||||||
|
gotenberg.PDFEngineMock
|
||||||
|
}{}
|
||||||
|
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
|
||||||
|
}
|
||||||
|
engine.ValidateMock = func() error {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
engine.validate = func() error { return errors.New("foo") }
|
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
gotenberg.ParsedFlags{
|
gotenberg.ParsedFlags{
|
||||||
FlagSet: new(PDFEngines).Descriptor().FlagSet,
|
FlagSet: new(PDFEngines).Descriptor().FlagSet,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
provider.Descriptor(),
|
||||||
engine.Descriptor(),
|
engine.Descriptor(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectPDFEngineNames: []string{"bar"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "selection from user",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
engine := struct{ ProtoPDFEngine }{}
|
provider := struct {
|
||||||
engine.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }}
|
gotenberg.LoggerProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return zap.NewNop(), nil
|
||||||
}
|
}
|
||||||
engine.validate = func() error { return nil }
|
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
engine1 := struct {
|
||||||
gotenberg.ParsedFlags{
|
gotenberg.ModuleMock
|
||||||
FlagSet: new(PDFEngines).Descriptor().FlagSet,
|
gotenberg.ValidatorMock
|
||||||
},
|
gotenberg.PDFEngineMock
|
||||||
[]gotenberg.ModuleDescriptor{
|
}{}
|
||||||
engine.Descriptor(),
|
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
},
|
|
||||||
)
|
|
||||||
}(),
|
|
||||||
expectNames: []string{"foo"},
|
|
||||||
expectEnginesCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *gotenberg.Context {
|
|
||||||
engine1 := struct{ ProtoPDFEngine }{}
|
|
||||||
engine1.descriptor = func() gotenberg.ModuleDescriptor {
|
|
||||||
return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }}
|
return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }}
|
||||||
}
|
}
|
||||||
engine1.validate = func() error { return nil }
|
engine1.ValidateMock = func() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
engine2 := struct{ ProtoPDFEngine }{}
|
engine2 := struct {
|
||||||
engine2.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.ValidatorMock
|
||||||
|
gotenberg.PDFEngineMock
|
||||||
|
}{}
|
||||||
|
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }}
|
return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }}
|
||||||
}
|
}
|
||||||
engine2.validate = func() error { return nil }
|
engine2.ValidateMock = func() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
fs := new(PDFEngines).Descriptor().FlagSet
|
fs := new(PDFEngines).Descriptor().FlagSet
|
||||||
err := fs.Parse([]string{"--pdfengines-engines=b", "--pdfengines-engines=a"})
|
err := fs.Parse([]string{"--pdfengines-engines=b", "--pdfengines-engines=a"})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
@@ -125,33 +120,47 @@ func TestPDFEngine_Provision(t *testing.T) {
|
|||||||
FlagSet: fs,
|
FlagSet: fs,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
provider.Descriptor(),
|
||||||
engine1.Descriptor(),
|
engine1.Descriptor(),
|
||||||
engine2.Descriptor(),
|
engine2.Descriptor(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectNames: []string{"b", "a"},
|
expectPDFEngineNames: []string{"b", "a"},
|
||||||
expectEnginesCount: 2,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "user select deprecated unoconv-pdfengine",
|
||||||
ctx: func() *gotenberg.Context {
|
ctx: func() *gotenberg.Context {
|
||||||
engine1 := struct{ ProtoPDFEngine }{}
|
provider := struct {
|
||||||
engine1.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }}
|
gotenberg.LoggerProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return zap.NewNop(), nil
|
||||||
}
|
}
|
||||||
engine1.validate = func() error { return nil }
|
|
||||||
|
|
||||||
engine2 := struct{ ProtoPDFEngine }{}
|
engine := struct {
|
||||||
engine2.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }}
|
gotenberg.ValidatorMock
|
||||||
|
gotenberg.PDFEngineMock
|
||||||
|
}{}
|
||||||
|
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "uno-pdfengine", New: func() gotenberg.Module { return engine }}
|
||||||
|
}
|
||||||
|
engine.ValidateMock = func() error {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
engine2.validate = func() error { return nil }
|
|
||||||
|
|
||||||
fs := new(PDFEngines).Descriptor().FlagSet
|
fs := new(PDFEngines).Descriptor().FlagSet
|
||||||
err := fs.Parse([]string{"--pdfengines-engines=b"})
|
err := fs.Parse([]string{"--pdfengines-engines=unoconv-pdfengine"})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected error but got: %v", err)
|
t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return gotenberg.NewContext(
|
return gotenberg.NewContext(
|
||||||
@@ -159,56 +168,136 @@ func TestPDFEngine_Provision(t *testing.T) {
|
|||||||
FlagSet: fs,
|
FlagSet: fs,
|
||||||
},
|
},
|
||||||
[]gotenberg.ModuleDescriptor{
|
[]gotenberg.ModuleDescriptor{
|
||||||
engine1.Descriptor(),
|
provider.Descriptor(),
|
||||||
engine2.Descriptor(),
|
engine.Descriptor(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}(),
|
}(),
|
||||||
expectNames: []string{"b"},
|
expectPDFEngineNames: []string{"uno-pdfengine"},
|
||||||
expectEnginesCount: 2,
|
|
||||||
},
|
},
|
||||||
} {
|
{
|
||||||
mod := new(PDFEngines)
|
name: "no logger provider",
|
||||||
err := mod.Provision(tc.ctx)
|
ctx: func() *gotenberg.Context {
|
||||||
|
return gotenberg.NewContext(
|
||||||
|
gotenberg.ParsedFlags{
|
||||||
|
FlagSet: new(PDFEngines).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectProvisionErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no logger from logger provider",
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
provider := struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.LoggerProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return nil, errors.New("foo")
|
||||||
|
}
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
return gotenberg.NewContext(
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
gotenberg.ParsedFlags{
|
||||||
}
|
FlagSet: new(PDFEngines).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
provider.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectProvisionErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no valid PDF engines",
|
||||||
|
ctx: func() *gotenberg.Context {
|
||||||
|
provider := struct {
|
||||||
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.LoggerProviderMock
|
||||||
|
}{}
|
||||||
|
provider.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module {
|
||||||
|
return provider
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
provider.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||||
|
return zap.NewNop(), nil
|
||||||
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
engine := struct {
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
gotenberg.ModuleMock
|
||||||
}
|
gotenberg.ValidatorMock
|
||||||
|
gotenberg.PDFEngineMock
|
||||||
|
}{}
|
||||||
|
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
|
||||||
|
}
|
||||||
|
engine.ValidateMock = func() error {
|
||||||
|
return errors.New("foo")
|
||||||
|
}
|
||||||
|
|
||||||
if len(tc.expectNames) != len(mod.names) {
|
return gotenberg.NewContext(
|
||||||
t.Errorf("test %d: expected %d names but got %d", i, len(tc.expectNames), len(mod.names))
|
gotenberg.ParsedFlags{
|
||||||
}
|
FlagSet: new(PDFEngines).Descriptor().FlagSet,
|
||||||
|
},
|
||||||
|
[]gotenberg.ModuleDescriptor{
|
||||||
|
provider.Descriptor(),
|
||||||
|
engine.Descriptor(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}(),
|
||||||
|
expectProvisionErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if tc.expectEnginesCount != len(mod.engines) {
|
for _, tc := range tests {
|
||||||
t.Errorf("test %d: expected %d engines but got %d", i, tc.expectEnginesCount, len(mod.engines))
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
}
|
mod := new(PDFEngines)
|
||||||
|
err := mod.Provision(tc.ctx)
|
||||||
|
|
||||||
for index, name := range mod.names {
|
if tc.expectProvisionErr && err == nil {
|
||||||
if name != tc.expectNames[index] {
|
t.Fatal("expected mod.Provision() error, but got none")
|
||||||
t.Errorf("test %d: expected name at index %d to be %s, but got: %s", i, index, name, tc.expectNames[index])
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if !tc.expectProvisionErr && err != nil {
|
||||||
|
t.Fatalf("expected no error from mod.Provision(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tc.expectPDFEngineNames) != len(mod.names) {
|
||||||
|
t.Errorf("expected %d names but got %d", len(tc.expectPDFEngineNames), len(mod.names))
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, name := range mod.names {
|
||||||
|
if name != tc.expectPDFEngineNames[index] {
|
||||||
|
t.Errorf("expected name at index %d to be %s, but got: %s", index, name, tc.expectPDFEngineNames[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFEngine_Validate(t *testing.T) {
|
func TestPDFEngines_Validate(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
names []string
|
name string
|
||||||
engines []gotenberg.PDFEngine
|
names []string
|
||||||
expectErr bool
|
engines []gotenberg.PDFEngine
|
||||||
|
expectValidateErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
expectErr: true,
|
name: "existing PDF engine",
|
||||||
},
|
|
||||||
{
|
|
||||||
names: []string{"foo"},
|
names: []string{"foo"},
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engines: func() []gotenberg.PDFEngine {
|
||||||
engine := struct{ ProtoPDFEngine }{}
|
engine := struct {
|
||||||
engine.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.PDFEngineMock
|
||||||
|
}{}
|
||||||
|
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }}
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,15 +307,22 @@ func TestPDFEngine_Validate(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "non-existing bar PDF engine",
|
||||||
names: []string{"foo", "bar", "baz"},
|
names: []string{"foo", "bar", "baz"},
|
||||||
engines: func() []gotenberg.PDFEngine {
|
engines: func() []gotenberg.PDFEngine {
|
||||||
engine1 := struct{ ProtoPDFEngine }{}
|
engine1 := struct {
|
||||||
engine1.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.PDFEngineMock
|
||||||
|
}{}
|
||||||
|
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
||||||
}
|
}
|
||||||
|
|
||||||
engine2 := struct{ ProtoPDFEngine }{}
|
engine2 := struct {
|
||||||
engine2.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
|
gotenberg.PDFEngineMock
|
||||||
|
}{}
|
||||||
|
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return engine2 }}
|
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return engine2 }}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,21 +331,31 @@ func TestPDFEngine_Validate(t *testing.T) {
|
|||||||
engine2,
|
engine2,
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
expectErr: true,
|
expectValidateErr: true,
|
||||||
},
|
},
|
||||||
} {
|
{
|
||||||
mod := new(PDFEngines)
|
name: "no PDF engine",
|
||||||
mod.names = tc.names
|
expectValidateErr: true,
|
||||||
mod.engines = tc.engines
|
},
|
||||||
err := mod.Validate()
|
}
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
for _, tc := range tests {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
}
|
mod := PDFEngines{
|
||||||
|
names: tc.names,
|
||||||
|
engines: tc.engines,
|
||||||
|
}
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
err := mod.Validate()
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
if tc.expectValidateErr && err == nil {
|
||||||
|
t.Errorf("expected mod.Validate() error, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectValidateErr && err != nil {
|
||||||
|
t.Errorf("expected no error from mod.Validate(), but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,76 +365,81 @@ func TestPDFEngines_SystemMessages(t *testing.T) {
|
|||||||
|
|
||||||
messages := mod.SystemMessages()
|
messages := mod.SystemMessages()
|
||||||
if len(messages) != 1 {
|
if len(messages) != 1 {
|
||||||
t.Errorf("expected one and only one message but got %d", len(messages))
|
t.Errorf("expected one and only one message from mod.SystemMessages(), but got %d", len(messages))
|
||||||
}
|
}
|
||||||
|
|
||||||
expect := strings.Join(mod.names[:], " ")
|
expect := strings.Join(mod.names[:], " ")
|
||||||
if messages[0] != expect {
|
if messages[0] != expect {
|
||||||
t.Errorf("expected message '%s' but got '%s'", expect, messages[0])
|
t.Errorf("expected message '%s' from mod.SystemMessages(), but got '%s'", expect, messages[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFEngine_PDFEngine(t *testing.T) {
|
func TestPDFEngines_PDFEngine(t *testing.T) {
|
||||||
mod := new(PDFEngines)
|
mod := PDFEngines{
|
||||||
mod.names = []string{"foo", "bar"}
|
names: []string{"foo", "bar"},
|
||||||
mod.engines = func() []gotenberg.PDFEngine {
|
engines: func() []gotenberg.PDFEngine {
|
||||||
engine1 := struct{ ProtoPDFEngine }{}
|
engine1 := struct {
|
||||||
engine1.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
gotenberg.PDFEngineMock
|
||||||
}
|
}{}
|
||||||
|
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
||||||
|
}
|
||||||
|
|
||||||
engine2 := struct{ ProtoPDFEngine }{}
|
engine2 := struct {
|
||||||
engine2.descriptor = func() gotenberg.ModuleDescriptor {
|
gotenberg.ModuleMock
|
||||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine2 }}
|
gotenberg.PDFEngineMock
|
||||||
}
|
}{}
|
||||||
|
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine2 }}
|
||||||
|
}
|
||||||
|
|
||||||
return []gotenberg.PDFEngine{
|
return []gotenberg.PDFEngine{
|
||||||
engine1,
|
engine1,
|
||||||
engine2,
|
engine2,
|
||||||
}
|
}
|
||||||
}()
|
}(),
|
||||||
|
}
|
||||||
|
|
||||||
_, err := mod.PDFEngine()
|
_, err := mod.PDFEngine()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error but got: %v", err)
|
t.Errorf("expected no error from mod.PDFEngine, but got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPDFEngine_Routes(t *testing.T) {
|
func TestPDFEngines_Routes(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
expectRoutes int
|
name string
|
||||||
disableRoutes bool
|
mod PDFEngines
|
||||||
|
expectRoutesCount int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
expectRoutes: 2,
|
name: "route not disabled",
|
||||||
|
mod: PDFEngines{
|
||||||
|
engines: []gotenberg.PDFEngine{
|
||||||
|
gotenberg.PDFEngineMock{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectRoutesCount: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
disableRoutes: true,
|
name: "route disabled",
|
||||||
|
mod: PDFEngines{
|
||||||
|
disableRoutes: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} {
|
}
|
||||||
mod := new(PDFEngines)
|
|
||||||
mod.engines = []gotenberg.PDFEngine{
|
|
||||||
struct{ ProtoPDFEngine }{},
|
|
||||||
}
|
|
||||||
mod.disableRoutes = tc.disableRoutes
|
|
||||||
|
|
||||||
routes, err := mod.Routes()
|
for _, tc := range tests {
|
||||||
if err != nil {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
routes, err := tc.mod.Routes()
|
||||||
}
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error from mod.Routes(), but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if tc.expectRoutes != len(routes) {
|
if tc.expectRoutesCount != len(routes) {
|
||||||
t.Errorf("test %d: expected %d routes but got %d", i, tc.expectRoutes, len(routes))
|
t.Errorf("expected %d routes from mod.Routes(), but got %d", tc.expectRoutesCount, len(routes))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
|
||||||
var (
|
|
||||||
_ gotenberg.Module = (*ProtoModule)(nil)
|
|
||||||
_ gotenberg.Validator = (*ProtoValidator)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoValidator)(nil)
|
|
||||||
_ gotenberg.PDFEngine = (*ProtoPDFEngine)(nil)
|
|
||||||
_ gotenberg.Module = (*ProtoPDFEngine)(nil)
|
|
||||||
_ gotenberg.Validator = (*ProtoPDFEngine)(nil)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestMergeHandler(t *testing.T) {
|
func TestMergeHandler(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
|
name string
|
||||||
ctx *api.MockContext
|
ctx *api.MockContext
|
||||||
engine gotenberg.PDFEngine
|
engine gotenberg.PDFEngine
|
||||||
expectErr bool
|
expectErr bool
|
||||||
@@ -22,34 +23,75 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
expectOutputPathsCount int
|
expectOutputPathsCount int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
name: "nominal behavior",
|
||||||
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"foo.pdf": "/foo/foo.pdf",
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: gotenberg.PDFEngineMock{
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectOutputPathsCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid form data: no PDF",
|
||||||
ctx: &api.MockContext{Context: &api.Context{}},
|
ctx: &api.MockContext{Context: &api.Context{}},
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
expectHTTPErr: true,
|
expectHTTPErr: true,
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "merge fail",
|
||||||
ctx: func() *api.MockContext {
|
ctx: func() *api.MockContext {
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
"foo.pdf": "/foo/foo.pdf",
|
||||||
})
|
})
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
engine: gotenberg.PDFEngineMock{
|
||||||
return &ProtoPDFEngine{
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
return errors.New("foo")
|
||||||
return errors.New("foo")
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "nominal behavior with a PDF format",
|
||||||
ctx: func() *api.MockContext {
|
ctx: func() *api.MockContext {
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"foo.pdf": "/foo/foo.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"pdfFormat": {
|
||||||
|
gotenberg.FormatPDFA1a,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: gotenberg.PDFEngineMock{
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectOutputPathsCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "convert to PDF format fail",
|
||||||
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
"foo.pdf": "/foo/foo.pdf",
|
||||||
})
|
})
|
||||||
@@ -61,153 +103,106 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
engine: gotenberg.PDFEngineMock{
|
||||||
return &ProtoPDFEngine{
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
return nil
|
||||||
return nil
|
},
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid PDF format",
|
||||||
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"foo.pdf": "/foo/foo.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"pdfFormat": {
|
||||||
|
"foo",
|
||||||
},
|
},
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
})
|
||||||
return gotenberg.ErrPDFFormatNotAvailable
|
|
||||||
},
|
return ctx
|
||||||
}
|
|
||||||
}(),
|
}(),
|
||||||
|
engine: gotenberg.PDFEngineMock{
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
|
return gotenberg.ErrPDFFormatNotAvailable
|
||||||
|
},
|
||||||
|
},
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
expectHTTPErr: true,
|
expectHTTPErr: true,
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "cannot add output paths",
|
||||||
ctx: func() *api.MockContext {
|
ctx: func() *api.MockContext {
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
"foo.pdf": "/foo/foo.pdf",
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfFormat": {
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
engine: func() gotenberg.PDFEngine {
|
|
||||||
return &ProtoPDFEngine{
|
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *api.MockContext {
|
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
|
|
||||||
ctx.SetCancelled(true)
|
ctx.SetCancelled(true)
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
engine: gotenberg.PDFEngineMock{
|
||||||
return &ProtoPDFEngine{
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
return nil
|
||||||
return nil
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
{
|
}
|
||||||
ctx: func() *api.MockContext {
|
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
|
|
||||||
ctx.SetFiles(map[string]string{
|
for _, tc := range tests {
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
})
|
c := echo.New().NewContext(nil, nil)
|
||||||
ctx.SetValues(map[string][]string{
|
c.Set("context", tc.ctx.Context)
|
||||||
"pdfFormat": {
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
err := mergeRoute(tc.engine).Handler(c)
|
||||||
}(),
|
|
||||||
engine: func() gotenberg.PDFEngine {
|
|
||||||
return &ProtoPDFEngine{
|
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectOutputPathsCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *api.MockContext {
|
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
|
|
||||||
ctx.SetFiles(map[string]string{
|
if tc.expectErr && err == nil {
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
t.Fatal("expected error from merge handler, but got none")
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
engine: func() gotenberg.PDFEngine {
|
|
||||||
return &ProtoPDFEngine{
|
|
||||||
merge: func(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectOutputPathsCount: 1,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
c := echo.New().NewContext(nil, nil)
|
|
||||||
c.Set("context", tc.ctx.Context)
|
|
||||||
|
|
||||||
err := mergeRoute(tc.engine).Handler(c)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
var httpErr api.HTTPError
|
|
||||||
isHTTPErr := errors.As(err, &httpErr)
|
|
||||||
|
|
||||||
if tc.expectHTTPErr && !isHTTPErr {
|
|
||||||
t.Errorf("test %d: expected HTTP error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectHTTPErr && isHTTPErr {
|
|
||||||
t.Errorf("test %d: expected no HTTP error but got one: %v", i, httpErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil && tc.expectHTTPErr && isHTTPErr {
|
|
||||||
status, _ := httpErr.HTTPError()
|
|
||||||
if status != tc.expectHTTPStatus {
|
|
||||||
t.Errorf("test %d: expected %d HTTP status code but got %d", i, tc.expectHTTPStatus, status)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
if !tc.expectErr && err != nil {
|
||||||
t.Errorf("test %d: expected %d output paths but got %d", i, tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
t.Fatalf("expected no error from merge handler, but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var httpErr api.HTTPError
|
||||||
|
isHTTPErr := errors.As(err, &httpErr)
|
||||||
|
|
||||||
|
if tc.expectHTTPErr && !isHTTPErr {
|
||||||
|
t.Errorf("expected HTTP error from merge handler, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectHTTPErr && isHTTPErr {
|
||||||
|
t.Errorf("expected no HTTP error from merge handler, but got one: %v", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && tc.expectHTTPErr && isHTTPErr {
|
||||||
|
status, _ := httpErr.HTTPError()
|
||||||
|
if status != tc.expectHTTPStatus {
|
||||||
|
t.Errorf("expected %d HTTP status code from merge handler, but got %d", tc.expectHTTPStatus, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
||||||
|
t.Errorf("expected %d output paths from merge handler, but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertHandler(t *testing.T) {
|
func TestConvertHandler(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
tests := []struct {
|
||||||
|
name string
|
||||||
ctx *api.MockContext
|
ctx *api.MockContext
|
||||||
engine gotenberg.PDFEngine
|
engine gotenberg.PDFEngine
|
||||||
expectErr bool
|
expectErr bool
|
||||||
@@ -216,132 +211,109 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
expectOutputPathsCount int
|
expectOutputPathsCount int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
ctx: &api.MockContext{Context: &api.Context{}},
|
name: "nominal behavior",
|
||||||
expectErr: true,
|
|
||||||
expectHTTPErr: true,
|
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *api.MockContext {
|
ctx: func() *api.MockContext {
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfFormat": {
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
expectHTTPErr: true,
|
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *api.MockContext {
|
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
|
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
"foo.pdf": "/foo/foo.pdf",
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfFormat": {
|
"pdfFormat": {
|
||||||
"foo",
|
gotenberg.FormatPDFA1a,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
engine: gotenberg.PDFEngineMock{
|
||||||
return &ProtoPDFEngine{
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
return nil
|
||||||
return gotenberg.ErrPDFFormatNotAvailable
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
expectHTTPErr: true,
|
|
||||||
expectHTTPStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *api.MockContext {
|
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfFormat": {
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
engine: func() gotenberg.PDFEngine {
|
|
||||||
return &ProtoPDFEngine{
|
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
|
||||||
return errors.New("foo")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *api.MockContext {
|
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
|
|
||||||
ctx.SetCancelled(true)
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfFormat": {
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
engine: func() gotenberg.PDFEngine {
|
|
||||||
return &ProtoPDFEngine{
|
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ctx: func() *api.MockContext {
|
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
|
||||||
|
|
||||||
ctx.SetFiles(map[string]string{
|
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
|
||||||
})
|
|
||||||
ctx.SetValues(map[string][]string{
|
|
||||||
"pdfFormat": {
|
|
||||||
"foo",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return ctx
|
|
||||||
}(),
|
|
||||||
engine: func() gotenberg.PDFEngine {
|
|
||||||
return &ProtoPDFEngine{
|
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}(),
|
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
name: "nominal behavior, but with 3 PDFs",
|
||||||
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"foo.pdf": "/foo/foo.pdf",
|
||||||
|
"bar.pdf": "/bar/bar.pdf",
|
||||||
|
"baz.pdf": "/baz/baz.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"pdfFormat": {
|
||||||
|
gotenberg.FormatPDFA1a,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: gotenberg.PDFEngineMock{
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectOutputPathsCount: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid form data: no PDF",
|
||||||
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"pdfFormat": {
|
||||||
|
gotenberg.FormatPDFA1a,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
expectHTTPErr: true,
|
||||||
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid form data: no PDF format",
|
||||||
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"foo.pdf": "/foo/foo.pdf",
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectErr: true,
|
||||||
|
expectHTTPErr: true,
|
||||||
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "convert to PDF format fail",
|
||||||
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"foo.pdf": "/foo/foo.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"pdfFormat": {
|
||||||
|
gotenberg.FormatPDFA1a,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: gotenberg.PDFEngineMock{
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PDF format not available",
|
||||||
ctx: func() *api.MockContext {
|
ctx: func() *api.MockContext {
|
||||||
ctx := &api.MockContext{Context: &api.Context{}}
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
"foo.pdf": "/foo/foo.pdf",
|
"foo.pdf": "/foo/foo.pdf",
|
||||||
"bar.pdf": "/foo/bar.pdf",
|
|
||||||
})
|
})
|
||||||
ctx.SetValues(map[string][]string{
|
ctx.SetValues(map[string][]string{
|
||||||
"pdfFormat": {
|
"pdfFormat": {
|
||||||
@@ -351,49 +323,76 @@ func TestConvertHandler(t *testing.T) {
|
|||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
engine: func() gotenberg.PDFEngine {
|
engine: gotenberg.PDFEngineMock{
|
||||||
return &ProtoPDFEngine{
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
convert: func(_ context.Context, _ *zap.Logger, _, _, _ string) error {
|
return gotenberg.ErrPDFFormatNotAvailable
|
||||||
return nil
|
},
|
||||||
},
|
},
|
||||||
}
|
expectErr: true,
|
||||||
}(),
|
expectHTTPErr: true,
|
||||||
expectOutputPathsCount: 2,
|
expectHTTPStatus: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
} {
|
{
|
||||||
c := echo.New().NewContext(nil, nil)
|
name: "cannot add output paths",
|
||||||
c.Set("context", tc.ctx.Context)
|
ctx: func() *api.MockContext {
|
||||||
|
ctx := &api.MockContext{Context: &api.Context{}}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"foo.pdf": "/foo/foo.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetValues(map[string][]string{
|
||||||
|
"pdfFormat": {
|
||||||
|
gotenberg.FormatPDFA1a,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.SetCancelled(true)
|
||||||
|
|
||||||
err := convertRoute(tc.engine).Handler(c)
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: gotenberg.PDFEngineMock{
|
||||||
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
for _, tc := range tests {
|
||||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
}
|
c := echo.New().NewContext(nil, nil)
|
||||||
|
c.Set("context", tc.ctx.Context)
|
||||||
|
|
||||||
if !tc.expectErr && err != nil {
|
err := convertRoute(tc.engine).Handler(c)
|
||||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var httpErr api.HTTPError
|
if tc.expectErr && err == nil {
|
||||||
isHTTPErr := errors.As(err, &httpErr)
|
t.Fatal("expected error from convert handler, but got none")
|
||||||
|
|
||||||
if tc.expectHTTPErr && !isHTTPErr {
|
|
||||||
t.Errorf("test %d: expected HTTP error but got: %v", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tc.expectHTTPErr && isHTTPErr {
|
|
||||||
t.Errorf("test %d: expected no HTTP error but got one: %v", i, httpErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil && tc.expectHTTPErr && isHTTPErr {
|
|
||||||
status, _ := httpErr.HTTPError()
|
|
||||||
if status != tc.expectHTTPStatus {
|
|
||||||
t.Errorf("test %d: expected %d HTTP status code but got %d", i, tc.expectHTTPStatus, status)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
if !tc.expectErr && err != nil {
|
||||||
t.Errorf("test %d: expected %d output paths but got %d", i, tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
t.Fatalf("expected no error from convert handler, but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var httpErr api.HTTPError
|
||||||
|
isHTTPErr := errors.As(err, &httpErr)
|
||||||
|
|
||||||
|
if tc.expectHTTPErr && !isHTTPErr {
|
||||||
|
t.Errorf("expected HTTP error from convert handler, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectHTTPErr && isHTTPErr {
|
||||||
|
t.Errorf("expected no HTTP error from convert handler, but got one: %v", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && tc.expectHTTPErr && isHTTPErr {
|
||||||
|
status, _ := httpErr.HTTPError()
|
||||||
|
if status != tc.expectHTTPStatus {
|
||||||
|
t.Errorf("expected %d HTTP status code from convert handler, but got %d", tc.expectHTTPStatus, status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
||||||
|
t.Errorf("expected %d output paths from convert handler, but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func (engine PDFtk) Merge(ctx context.Context, logger *zap.Logger, inputPaths []
|
|||||||
activeInstancesCount += 1
|
activeInstancesCount += 1
|
||||||
activeInstancesCountMu.Unlock()
|
activeInstancesCountMu.Unlock()
|
||||||
|
|
||||||
err = cmd.Exec()
|
_, err = cmd.Exec()
|
||||||
|
|
||||||
activeInstancesCountMu.Lock()
|
activeInstancesCountMu.Lock()
|
||||||
activeInstancesCount -= 1
|
activeInstancesCount -= 1
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func (engine QPDF) Merge(ctx context.Context, logger *zap.Logger, inputPaths []s
|
|||||||
activeInstancesCount += 1
|
activeInstancesCount += 1
|
||||||
activeInstancesCountMu.Unlock()
|
activeInstancesCountMu.Unlock()
|
||||||
|
|
||||||
err = cmd.Exec()
|
_, err = cmd.Exec()
|
||||||
|
|
||||||
activeInstancesCountMu.Lock()
|
activeInstancesCountMu.Lock()
|
||||||
activeInstancesCount -= 1
|
activeInstancesCount -= 1
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/gc"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/gc"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/pdfengine"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/pdfengine"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/unoconv"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/libreoffice/uno"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/logging"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/logging"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfcpu"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfcpu"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfengines"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfengines"
|
||||||
|
|||||||
@@ -42,4 +42,4 @@ RUN apt-get update -qq &&\
|
|||||||
# Pristine working directory.
|
# Pristine working directory.
|
||||||
WORKDIR /tests
|
WORKDIR /tests
|
||||||
|
|
||||||
ENTRYPOINT [ "docker-entrypoint.sh" ]
|
ENTRYPOINT [ "/usr/bin/tini", "--", "docker-entrypoint.sh" ]
|
||||||
Reference in New Issue
Block a user