From 70dfea5876f29329789d1a097460dc3867a54136 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Fri, 4 Oct 2019 17:49:26 +0200 Subject: [PATCH 01/18] adding pageRanges formField --- internal/app/xhttp/option.go | 10 +++++++ internal/app/xhttp/pkg/resource/arg.go | 4 +++ internal/pkg/printer/chrome.go | 39 ++++++++++++++++++-------- internal/pkg/printer/office.go | 27 +++++++++++++++--- 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/internal/app/xhttp/option.go b/internal/app/xhttp/option.go index d79841e8..a40d4ea4 100644 --- a/internal/app/xhttp/option.go +++ b/internal/app/xhttp/option.go @@ -48,6 +48,10 @@ func chromePrinterOptions(r resource.Resource, config conf.Config) (printer.Chro if err != nil { return printer.ChromePrinterOptions{}, err } + pageRanges, err := r.StringArg(resource.PageRangesArgKey, "") + if err != nil { + return printer.ChromePrinterOptions{}, err + } return printer.ChromePrinterOptions{ WaitTimeout: waitTimeout, WaitDelay: waitDelay, @@ -60,6 +64,7 @@ func chromePrinterOptions(r resource.Resource, config conf.Config) (printer.Chro MarginLeft: marginLeft, MarginRight: marginRight, Landscape: landscape, + PageRanges: pageRanges, }, nil } opts, err := resolver() @@ -80,9 +85,14 @@ func officePrinterOptions(r resource.Resource, config conf.Config) (printer.Offi if err != nil { return printer.OfficePrinterOptions{}, err } + pageRanges, err := r.StringArg(resource.PageRangesArgKey, "") + if err != nil { + return printer.OfficePrinterOptions{}, err + } return printer.OfficePrinterOptions{ WaitTimeout: waitTimeout, Landscape: landscape, + PageRanges: pageRanges, }, nil } opts, err := resolver() diff --git a/internal/app/xhttp/pkg/resource/arg.go b/internal/app/xhttp/pkg/resource/arg.go index 35c7800b..3372da46 100644 --- a/internal/app/xhttp/pkg/resource/arg.go +++ b/internal/app/xhttp/pkg/resource/arg.go @@ -51,6 +51,9 @@ const ( // LandscapeArgKey is the key // of the argument "landscape". LandscapeArgKey ArgKey = "landscape" + // PageRangesArgKey is the key + // of the argument "pageRanges". + PageRangesArgKey ArgKey = "pageRanges" ) /* @@ -73,6 +76,7 @@ func ArgKeys() []ArgKey { MarginLeftArgKey, MarginRightArgKey, LandscapeArgKey, + PageRangesArgKey, } } diff --git a/internal/pkg/printer/chrome.go b/internal/pkg/printer/chrome.go index aff21e75..f7cca718 100644 --- a/internal/pkg/printer/chrome.go +++ b/internal/pkg/printer/chrome.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io/ioutil" + "strings" "time" "github.com/mafredri/cdp" @@ -40,6 +41,7 @@ type ChromePrinterOptions struct { MarginLeft float64 MarginRight float64 Landscape bool + PageRanges string } // DefaultChromePrinterOptions returns the default @@ -58,6 +60,7 @@ func DefaultChromePrinterOptions(config conf.Config) ChromePrinterOptions { MarginLeft: 1.0, MarginRight: 1.0, Landscape: false, + PageRanges: "", } } @@ -142,23 +145,35 @@ func (p chromePrinter) Print(destination string) error { } else { p.logger.DebugOp(op, "no wait delay to apply, moving on...") } + printToPdfArgs := page.NewPrintToPDFArgs(). + SetPaperWidth(p.opts.PaperWidth). + SetPaperHeight(p.opts.PaperHeight). + SetMarginTop(p.opts.MarginTop). + SetMarginBottom(p.opts.MarginBottom). + SetMarginLeft(p.opts.MarginLeft). + SetMarginRight(p.opts.MarginRight). + SetLandscape(p.opts.Landscape). + SetDisplayHeaderFooter(true). + SetHeaderTemplate(p.opts.HeaderHTML). + SetFooterTemplate(p.opts.FooterHTML). + SetPrintBackground(true) + if p.opts.PageRanges != "" { + printToPdfArgs.SetPageRanges(p.opts.PageRanges) + } // print the page to PDF. + // TODO catch page range error? print, err := targetClient.Page.PrintToPDF( ctx, - page.NewPrintToPDFArgs(). - SetPaperWidth(p.opts.PaperWidth). - SetPaperHeight(p.opts.PaperHeight). - SetMarginTop(p.opts.MarginTop). - SetMarginBottom(p.opts.MarginBottom). - SetMarginLeft(p.opts.MarginLeft). - SetMarginRight(p.opts.MarginRight). - SetLandscape(p.opts.Landscape). - SetDisplayHeaderFooter(true). - SetHeaderTemplate(p.opts.HeaderHTML). - SetFooterTemplate(p.opts.FooterHTML). - SetPrintBackground(true), + printToPdfArgs, ) if err != nil { + if strings.Contains(err.Error(), "Page range syntax error") { + return xerror.Invalid( + "", + fmt.Sprintf("'%s' is not a valid Google Chrome page ranges", p.opts.PageRanges), + err, + ) + } return err } if err := ioutil.WriteFile(destination, print.Data, 0644); err != nil { diff --git a/internal/pkg/printer/office.go b/internal/pkg/printer/office.go index 8a21ff2e..7112acda 100644 --- a/internal/pkg/printer/office.go +++ b/internal/pkg/printer/office.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/phayes/freeport" "github.com/thecodingmachine/gotenberg/internal/pkg/conf" @@ -26,6 +27,7 @@ type officePrinter struct { type OfficePrinterOptions struct { WaitTimeout float64 Landscape bool + PageRanges string } // DefaultOfficePrinterOptions returns the default @@ -34,6 +36,7 @@ func DefaultOfficePrinterOptions(config conf.Config) OfficePrinterOptions { return OfficePrinterOptions{ WaitTimeout: config.DefaultWaitTimeout(), Landscape: false, + PageRanges: "", } } @@ -59,7 +62,7 @@ func (p officePrinter) Print(destination string) error { baseFilename := xrand.Get() tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename) p.logger.DebugfOp(op, "converting '%s' to PDF...", fpath) - if err := unoconv(ctx, p.logger, fpath, tmpDest, p.opts); err != nil { + if err := p.unoconv(ctx, fpath, tmpDest); err != nil { return err } p.logger.DebugfOp(op, "'%s.pdf' created", baseFilename) @@ -85,9 +88,12 @@ func (p officePrinter) Print(destination string) error { return nil } -func unoconv(ctx context.Context, logger xlog.Logger, fpath, destination string, opts OfficePrinterOptions) error { +func (p officePrinter) unoconv(ctx context.Context, fpath, destination string) error { const op string = "printer.unoconv" resolver := func() error { + hasPageRanges := func() bool { + return p.opts.PageRanges != "" && len(p.fpaths) == 1 + } port, err := freeport.GetFreePort() if err != nil { return err @@ -100,11 +106,24 @@ func unoconv(ctx context.Context, logger xlog.Logger, fpath, destination string, "--format", "pdf", } - if opts.Landscape { + if p.opts.Landscape { args = append(args, "--printer", "PaperOrientation=landscape") } + if hasPageRanges() { + args = append(args, "--export", fmt.Sprintf("PageRange=%s", p.opts.PageRanges)) + } args = append(args, "--output", destination, fpath) - return xexec.Run(ctx, logger, "unoconv", args...) + if err := xexec.Run(ctx, p.logger, "unoconv", args...); err != nil { + if hasPageRanges() && strings.Contains(err.Error(), "exit status 5") { + return xerror.Invalid( + "", + fmt.Sprintf("'%s' is not a valid LibreOffice page ranges", p.opts.PageRanges), + err, + ) + } + return err + } + return nil } if err := resolver(); err != nil { return xerror.New(op, err) From 60c069cb1ff6c276452e0f77a286be1e06bb43e1 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Fri, 4 Oct 2019 18:19:13 +0200 Subject: [PATCH 02/18] updating documention with pageRanges --- build/docs/content/04-html.md | 48 ++++++++++++++ build/docs/content/07-office.md | 52 +++++++++++++++ docs/index.html | 112 ++++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+) diff --git a/build/docs/content/04-html.md b/build/docs/content/04-html.md index 4eae9357..e008cda7 100644 --- a/build/docs/content/04-html.md +++ b/build/docs/content/04-html.md @@ -347,3 +347,51 @@ $request->setWaitDelay(5.5); $dest = "result.pdf"; $client->store($request, $dest); ``` + +## Page ranges + +You may specify the page ranges to convert. + +The format is the same as the one from the print options +of Google Chrome, e.g. `1-5,8,11-13`. + +### cURL + +```bash +$ curl --request POST \ + --url http://localhost:3000/convert/html \ + --header 'Content-Type: multipart/form-data' \ + --form files=@index.html \ + --form pageRanges='1-3,5' \ + -o result.pdf +``` + +### Go + +```golang +import "github.com/thecodingmachine/gotenberg-go-client/v6" + +func main() { + c := &gotenberg.Client{Hostname: "http://localhost:3000"} + req, _ := gotenberg.NewHTMLRequest("index.html") + req.PageRanges("1-3,5") + dest := "result.pdf" + c.Store(req, dest) +} +``` + +### PHP + +```php +use TheCodingMachine\Gotenberg\Client; +use TheCodingMachine\Gotenberg\DocumentFactory; +use TheCodingMachine\Gotenberg\HTMLRequest; +use TheCodingMachine\Gotenberg\Request; + +$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); +$index = DocumentFactory::makeFromPath('index.html', 'index.html'); +$request = new HTMLRequest($index); +$request->setPageRanges("1-3,5"); +$dest = "result.pdf"; +$client->store($request, $dest); +``` diff --git a/build/docs/content/07-office.md b/build/docs/content/07-office.md index 39e91e37..74745587 100644 --- a/build/docs/content/07-office.md +++ b/build/docs/content/07-office.md @@ -115,3 +115,55 @@ $request->setLandscape(true); $dest = "result.pdf"; $client->store($request, $dest); ``` + +## Page ranges + +You may specify the page ranges to convert. + +The format is the same as the one from the print options +of LibreOffice, e.g. `1-1` or `1-4`. + +> This feature does not work in there is more +> than one document to convert. + +### cURL + +```bash +$ curl --request POST \ + --url http://localhost:3000/convert/office \ + --header 'Content-Type: multipart/form-data' \ + --form files=@document.docx \ + --form pageRanges='1-3' \ + -o result.pdf +``` + +### Go + +```golang +import "github.com/thecodingmachine/gotenberg-go-client/v6" + +func main() { + c := &gotenberg.Client{Hostname: "http://localhost:3000"} + req, _ := gotenberg.NewOfficeRequest("document.docx") + req.PageRanges("1-3") + dest := "result.pdf" + c.Store(req, dest) +} +``` + +### PHP + +```php +use TheCodingMachine\Gotenberg\Client; +use TheCodingMachine\Gotenberg\DocumentFactory; +use TheCodingMachine\Gotenberg\OfficeRequest; + +$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); +$files = [ + DocumentFactory::makeFromPath('document.docx', 'document.docx'), +]; +$request = new OfficeRequest($files); +$request->setPageRanges("1-3"); +$dest = "result.pdf"; +$client->store($request, $dest); +``` diff --git a/docs/index.html b/docs/index.html index 9b004cb1..cbf92684 100755 --- a/docs/index.html +++ b/docs/index.html @@ -724,6 +724,59 @@ $request = new HTMLRequest($index); $request->setWaitDelay(5.5); $dest = "result.pdf"; $client->store($request, $dest); + + +

Page ranges

+ +

You may specify the page ranges to convert.

+ +

The format is the same as the one from the print options +of Google Chrome, e.g. 1-5,8,11-13.

+ +

cURL

+ +
$ curl --request POST \
+    --url http://localhost:3000/convert/html \
+    --header 'Content-Type: multipart/form-data' \
+    --form files=@index.html \
+    --form pageRanges='1-3,5' \
+    -o result.pdf
+
+ +

Go

+ +
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
+func main() {
+    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+    req, _ := gotenberg.NewHTMLRequest("index.html")
+    req.PageRanges("1-3,5")
+    dest := "result.pdf"
+    c.Store(req, dest)
+}
+
+ +

PHP

+ +
use TheCodingMachine\Gotenberg\Client;
+use TheCodingMachine\Gotenberg\DocumentFactory;
+use TheCodingMachine\Gotenberg\HTMLRequest;
+use TheCodingMachine\Gotenberg\Request;
+
+$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
+$index = DocumentFactory::makeFromPath('index.html', 'index.html');
+$request = new HTMLRequest($index);
+$request->setPageRanges("1-3,5");
+$dest = "result.pdf";
+$client->store($request, $dest);
 
@@ -1001,6 +1054,65 @@ $request = new OfficeRequest($files); $request->setLandscape(true); $dest = "result.pdf"; $client->store($request, $dest); + + +

Page ranges

+ +

You may specify the page ranges to convert.

+ +

The format is the same as the one from the print options +of LibreOffice, e.g. 1-1 or 1-4.

+ +
+

This feature does not work in there is more +than one document to convert.

+
+ +

cURL

+ +
$ curl --request POST \
+    --url http://localhost:3000/convert/office \
+    --header 'Content-Type: multipart/form-data' \
+    --form files=@document.docx \
+    --form pageRanges='1-3' \
+    -o result.pdf
+
+ +

Go

+ +
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
+func main() {
+    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+    req, _ := gotenberg.NewOfficeRequest("document.docx")
+    req.PageRanges("1-3")
+    dest := "result.pdf"
+    c.Store(req, dest)
+}
+
+ +

PHP

+ +
use TheCodingMachine\Gotenberg\Client;
+use TheCodingMachine\Gotenberg\DocumentFactory;
+use TheCodingMachine\Gotenberg\OfficeRequest;
+
+$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
+$files = [
+    DocumentFactory::makeFromPath('document.docx', 'document.docx'),
+];
+$request = new OfficeRequest($files);
+$request->setPageRanges("1-3");
+$dest = "result.pdf";
+$client->store($request, $dest);
 
From 827060b6cec5c5a290b88222b71cf8f65ff477f6 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Fri, 4 Oct 2019 18:22:46 +0200 Subject: [PATCH 03/18] emphazing that pageRanges does not work if more than one office document --- build/docs/content/07-office.md | 2 +- docs/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/docs/content/07-office.md b/build/docs/content/07-office.md index 74745587..5cded130 100644 --- a/build/docs/content/07-office.md +++ b/build/docs/content/07-office.md @@ -123,7 +123,7 @@ You may specify the page ranges to convert. The format is the same as the one from the print options of LibreOffice, e.g. `1-1` or `1-4`. -> This feature does not work in there is more +> **Attention:** this feature does not work in there is more > than one document to convert. ### cURL diff --git a/docs/index.html b/docs/index.html index cbf92684..fbc2a471 100755 --- a/docs/index.html +++ b/docs/index.html @@ -1066,7 +1066,7 @@ $client->store($request, $dest); of LibreOffice, e.g. 1-1 or 1-4.

-

This feature does not work in there is more +

Attention: this feature does not work in there is more than one document to convert.

From ca0784fa594f7883400a47edd5dc0ecef118a26f Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Sat, 5 Oct 2019 12:09:39 +0200 Subject: [PATCH 04/18] adding tests for page ranges --- internal/app/xhttp/pkg/resource/arg_test.go | 1 + internal/pkg/printer/chrome.go | 4 ++-- internal/pkg/printer/html_test.go | 20 +++++++++++++++++++ internal/pkg/printer/markdown_test.go | 22 +++++++++++++++++++++ internal/pkg/printer/office.go | 10 ++++------ internal/pkg/printer/office_test.go | 20 +++++++++++++++++++ internal/pkg/printer/url_test.go | 20 +++++++++++++++++++ 7 files changed, 89 insertions(+), 8 deletions(-) diff --git a/internal/app/xhttp/pkg/resource/arg_test.go b/internal/app/xhttp/pkg/resource/arg_test.go index 3c7f6b02..f42fd1d0 100644 --- a/internal/app/xhttp/pkg/resource/arg_test.go +++ b/internal/app/xhttp/pkg/resource/arg_test.go @@ -24,6 +24,7 @@ func TestArgKeys(t *testing.T) { MarginLeftArgKey, MarginRightArgKey, LandscapeArgKey, + PageRangesArgKey, } assert.Equal(t, expected, ArgKeys()) } diff --git a/internal/pkg/printer/chrome.go b/internal/pkg/printer/chrome.go index f7cca718..41b4c99d 100644 --- a/internal/pkg/printer/chrome.go +++ b/internal/pkg/printer/chrome.go @@ -161,15 +161,15 @@ func (p chromePrinter) Print(destination string) error { printToPdfArgs.SetPageRanges(p.opts.PageRanges) } // print the page to PDF. - // TODO catch page range error? print, err := targetClient.Page.PrintToPDF( ctx, printToPdfArgs, ) if err != nil { + // TODO: find a way to check it in the handlers. if strings.Contains(err.Error(), "Page range syntax error") { return xerror.Invalid( - "", + op, fmt.Sprintf("'%s' is not a valid Google Chrome page ranges", p.opts.PageRanges), err, ) diff --git a/internal/pkg/printer/html_test.go b/internal/pkg/printer/html_test.go index 4a233163..e69539fa 100644 --- a/internal/pkg/printer/html_test.go +++ b/internal/pkg/printer/html_test.go @@ -38,6 +38,26 @@ func TestHTMLPrinter(t *testing.T) { assert.Nil(t, err) err = os.RemoveAll(dest) assert.Nil(t, err) + // options with a page ranges. + opts = DefaultChromePrinterOptions(config) + opts.PageRanges = "1" + p = NewHTMLPrinter(logger, fpath, opts) + dest = test.GenerateDestination() + err = p.Print(dest) + assert.Nil(t, err) + err = os.RemoveAll(dest) + assert.Nil(t, err) + // should not be OK as options have + // a wrong page ranges. + opts = DefaultChromePrinterOptions(config) + opts.PageRanges = "foo" + p = NewHTMLPrinter(logger, fpath, opts) + dest = test.GenerateDestination() + err = p.Print(dest) + test.AssertError(t, err) + assert.Equal(t, xerror.InvalidCode, xerror.Code(err)) + err = os.RemoveAll(dest) + assert.Nil(t, err) // should not be OK as context.Context // should timeout. opts = DefaultChromePrinterOptions(config) diff --git a/internal/pkg/printer/markdown_test.go b/internal/pkg/printer/markdown_test.go index c5115a3a..9627cdea 100644 --- a/internal/pkg/printer/markdown_test.go +++ b/internal/pkg/printer/markdown_test.go @@ -40,6 +40,28 @@ func TestMarkdownPrinter(t *testing.T) { assert.Nil(t, err) err = os.RemoveAll(dest) assert.Nil(t, err) + // options with a page ranges. + opts = DefaultChromePrinterOptions(config) + opts.PageRanges = "1" + p, err = NewMarkdownPrinter(logger, fpath, opts) + assert.Nil(t, err) + dest = test.GenerateDestination() + err = p.Print(dest) + assert.Nil(t, err) + err = os.RemoveAll(dest) + assert.Nil(t, err) + // should not be OK as options have + // a wrong page ranges. + opts = DefaultChromePrinterOptions(config) + opts.PageRanges = "foo" + p, err = NewMarkdownPrinter(logger, fpath, opts) + assert.Nil(t, err) + dest = test.GenerateDestination() + err = p.Print(dest) + test.AssertError(t, err) + assert.Equal(t, xerror.InvalidCode, xerror.Code(err)) + err = os.RemoveAll(dest) + assert.Nil(t, err) // should not be OK as context.Context // should timeout. opts = DefaultChromePrinterOptions(config) diff --git a/internal/pkg/printer/office.go b/internal/pkg/printer/office.go index 7112acda..e0f4f20b 100644 --- a/internal/pkg/printer/office.go +++ b/internal/pkg/printer/office.go @@ -91,9 +91,6 @@ func (p officePrinter) Print(destination string) error { func (p officePrinter) unoconv(ctx context.Context, fpath, destination string) error { const op string = "printer.unoconv" resolver := func() error { - hasPageRanges := func() bool { - return p.opts.PageRanges != "" && len(p.fpaths) == 1 - } port, err := freeport.GetFreePort() if err != nil { return err @@ -109,14 +106,15 @@ func (p officePrinter) unoconv(ctx context.Context, fpath, destination string) e if p.opts.Landscape { args = append(args, "--printer", "PaperOrientation=landscape") } - if hasPageRanges() { + if p.opts.PageRanges != "" { args = append(args, "--export", fmt.Sprintf("PageRange=%s", p.opts.PageRanges)) } args = append(args, "--output", destination, fpath) if err := xexec.Run(ctx, p.logger, "unoconv", args...); err != nil { - if hasPageRanges() && strings.Contains(err.Error(), "exit status 5") { + // TODO: find a way to check it in the handlers. + if p.opts.PageRanges != "" && strings.Contains(err.Error(), "exit status 5") { return xerror.Invalid( - "", + op, fmt.Sprintf("'%s' is not a valid LibreOffice page ranges", p.opts.PageRanges), err, ) diff --git a/internal/pkg/printer/office_test.go b/internal/pkg/printer/office_test.go index b6a0235b..5eb8992e 100644 --- a/internal/pkg/printer/office_test.go +++ b/internal/pkg/printer/office_test.go @@ -46,6 +46,26 @@ func TestOfficePrinter(t *testing.T) { assert.Nil(t, err) err = os.RemoveAll(dest) assert.Nil(t, err) + // options with page ranges. + opts = DefaultOfficePrinterOptions(config) + opts.PageRanges = "1-1" + p = NewOfficePrinter(logger, []string{fpaths[0]}, opts) + dest = test.GenerateDestination() + err = p.Print(dest) + assert.Nil(t, err) + err = os.RemoveAll(dest) + assert.Nil(t, err) + // should not be OK as options have + // a wrong page ranges. + opts = DefaultOfficePrinterOptions(config) + opts.PageRanges = "foo" + p = NewOfficePrinter(logger, []string{fpaths[0]}, opts) + dest = test.GenerateDestination() + err = p.Print(dest) + test.AssertError(t, err) + assert.Equal(t, xerror.InvalidCode, xerror.Code(err)) + err = os.RemoveAll(dest) + assert.Nil(t, err) // should not be OK as context.Context // should timeout. opts = DefaultOfficePrinterOptions(config) diff --git a/internal/pkg/printer/url_test.go b/internal/pkg/printer/url_test.go index 2c0b99f7..ef477e75 100644 --- a/internal/pkg/printer/url_test.go +++ b/internal/pkg/printer/url_test.go @@ -38,6 +38,26 @@ func TestURLPrinter(t *testing.T) { assert.Nil(t, err) err = os.RemoveAll(dest) assert.Nil(t, err) + // options with a pages ranges. + opts = DefaultChromePrinterOptions(config) + opts.PageRanges = "1" + p = NewURLPrinter(logger, URL, opts) + dest = test.GenerateDestination() + err = p.Print(dest) + assert.Nil(t, err) + err = os.RemoveAll(dest) + assert.Nil(t, err) + // should not be OK as options have + // a wrong page ranges. + opts = DefaultChromePrinterOptions(config) + opts.PageRanges = "foo" + p = NewURLPrinter(logger, URL, opts) + dest = test.GenerateDestination() + err = p.Print(dest) + test.AssertError(t, err) + assert.Equal(t, xerror.InvalidCode, xerror.Code(err)) + err = os.RemoveAll(dest) + assert.Nil(t, err) // should not be OK as context.Context // should timeout. opts = DefaultChromePrinterOptions(config) From 3255f8af0c1f0efe6da9e545233b044dac3d9580 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Sat, 5 Oct 2019 12:13:06 +0200 Subject: [PATCH 05/18] updating documentation: if office and more than one document, page ranges will be applied for each of them --- build/docs/content/07-office.md | 4 ++-- docs/index.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/docs/content/07-office.md b/build/docs/content/07-office.md index 5cded130..546ee7de 100644 --- a/build/docs/content/07-office.md +++ b/build/docs/content/07-office.md @@ -123,8 +123,8 @@ You may specify the page ranges to convert. The format is the same as the one from the print options of LibreOffice, e.g. `1-1` or `1-4`. -> **Attention:** this feature does not work in there is more -> than one document to convert. +> **Attention:** if more than one document, the page ranges will be +> applied for each document. ### cURL diff --git a/docs/index.html b/docs/index.html index fbc2a471..b03e62e3 100755 --- a/docs/index.html +++ b/docs/index.html @@ -1066,8 +1066,8 @@ $client->store($request, $dest); of LibreOffice, e.g. 1-1 or 1-4.

-

Attention: this feature does not work in there is more -than one document to convert.

+

Attention: if more than one document, the page ranges will be +applied for each document.

Custom HTTP headers

+ +

You may send your own HTTP headers to the remoteURL.

+ +

For instance, by adding the HTTP header Gotenberg-Remoteurl-Your-Header to your request, +the API will send a request to the remoteURL with the HTTP header Your-Header.

+ +
+

Attention: the API uses a canonical format for the HTTP headers: +it transforms the first +letter and any letter following a hyphen to upper case; +the rest are converted to lowercase. For example, the +canonical key for accept-encoding is Accept-Encoding.

+
+ +

cURL

+ +
$ curl --request POST \
+    --url http://localhost:3000/convert/url \
+    --header 'Content-Type: multipart/form-data' \
+    --header 'Gotenberg-Remoteurl-Your-Header: Foo' \
+    --form remoteURL=https://google.com \
+    -o result.pdf
+
+ +

Go

+ +

// TODO

+ +

PHP

+ +

// TODO

+
@@ -1294,13 +1335,9 @@ $resp = $client->post($request);

You may also define this value globally: see the environment variables section.

-

Examples

- -

cURL

+cURL
$ curl --request POST \
     --url http://localhost:3000/convert/html \
@@ -1310,9 +1347,9 @@ $resp = $client->post($request);
     --form webhookURLTimeout=2.5
 
-

Go

+Go

import "github.com/thecodingmachine/gotenberg-go-client/v6"
 
@@ -1325,9 +1362,9 @@ $resp = $client->post($request);
 }
 
-

PHP

+PHP

use TheCodingMachine\Gotenberg\Client;
 use TheCodingMachine\Gotenberg\DocumentFactory;
@@ -1341,6 +1378,47 @@ $request->setWebhookURLTimeout(2.5);
 $resp = $client->post($request);
 
+

Custom HTTP headers

+ +

You may send your own HTTP headers to the webhookURL.

+ +

For instance, by adding the HTTP header Gotenberg-Webhookurl-Your-Header to your request, +the API will send a request to the webhookURL with the HTTP header Your-Header.

+ +
+

Attention: the API uses a canonical format for the HTTP headers: +it transforms the first +letter and any letter following a hyphen to upper case; +the rest are converted to lowercase. For example, the +canonical key for accept-encoding is Accept-Encoding.

+
+ +

cURL

+ +
$ curl --request POST \
+    --url http://localhost:3000/convert/html \
+    --header 'Content-Type: multipart/form-data' \
+    --header 'Gotenberg-Webhookurl-Your-Header: Foo' \
+    --form files=@index.html \
+    --form webhookURL='http://myapp.com/webhook/'
+
+ +

Go

+ +

// TODO

+ +

PHP

+ +

// TODO

+
From f0f48f4ddfd21e96181b4e357c6199f156095a47 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Fri, 6 Dec 2019 17:23:17 +0100 Subject: [PATCH 10/18] adding ROOT_PATH + tests --- Makefile | 5 +- build/lint/Dockerfile | 2 +- internal/app/xhttp/handler.go | 42 ++++++++++------ internal/app/xhttp/handler_test.go | 32 +++++++------ internal/app/xhttp/middleware.go | 6 +-- internal/app/xhttp/xhttp.go | 15 +++--- internal/app/xhttp/xhttp_test.go | 74 +++++++++++++++++++++-------- internal/pkg/conf/conf.go | 21 ++++++++ internal/pkg/conf/conf_test.go | 24 ++++++++++ internal/pkg/xassert/string.go | 61 ++++++++++++++++++++++++ internal/pkg/xassert/string_test.go | 24 ++++++++++ 11 files changed, 244 insertions(+), 62 deletions(-) diff --git a/Makefile b/Makefile index ece45530..8fece350 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ VERSION=snapshot DOCKER_USER= DOCKER_PASSWORD= DOCKER_REPOSITORY=thecodingmachine -GOLANGCI_LINT_VERSION=1.19.1 +GOLANGCI_LINT_VERSION=1.20.1 CODE_COVERAGE=0 TINI_VERSION=0.18.0 MAXIMUM_WAIT_TIMEOUT=30.0 @@ -15,6 +15,7 @@ DEFAULT_LISTEN_PORT=3000 DISABLE_GOOGLE_CHROME=0 DISABLE_UNOCONV=0 LOG_LEVEL=INFO +ROOT_PATH=/ DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=1048576 # build the base Docker image. @@ -55,7 +56,7 @@ image: # start the API using previously built Docker image. gotenberg: - docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -e DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=$(DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) + docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -e ROOT_PATH=$(ROOT_PATH) -e DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=$(DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) # publish Gotenberg images according to version. publish: diff --git a/build/lint/Dockerfile b/build/lint/Dockerfile index fd08c9c5..42a8c054 100644 --- a/build/lint/Dockerfile +++ b/build/lint/Dockerfile @@ -33,4 +33,4 @@ RUN go mod download &&\ # Copy our code source. COPY --chown=gotenberg:gotenberg . . -CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen" ] \ No newline at end of file +CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen", "--disable=wsl", "--disable=gocognit" ] \ No newline at end of file diff --git a/internal/app/xhttp/handler.go b/internal/app/xhttp/handler.go index 7c819bd2..0972a480 100644 --- a/internal/app/xhttp/handler.go +++ b/internal/app/xhttp/handler.go @@ -15,31 +15,45 @@ import ( "github.com/thecodingmachine/gotenberg/internal/pkg/xtime" ) -const ( - pingEndpoint string = "/ping" - mergeEndpoint string = "/merge" - convertGroupEndpoint string = "/convert" - htmlEndpoint string = "/html" - urlEndpoint string = "/url" - markdownEndpoint string = "/markdown" - officeEndpoint string = "/office" -) +func pingEndpoint(config conf.Config) string { + return fmt.Sprintf("%s%s", config.RootPath(), "ping") +} + +func mergeEndpoint(config conf.Config) string { + return fmt.Sprintf("%s%s", config.RootPath(), "merge") +} + +func htmlEndpoint(config conf.Config) string { + return fmt.Sprintf("%s%s", config.RootPath(), "convert/html") +} + +func urlEndpoint(config conf.Config) string { + return fmt.Sprintf("%s%s", config.RootPath(), "convert/url") +} + +func markdownEndpoint(config conf.Config) string { + return fmt.Sprintf("%s%s", config.RootPath(), "convert/markdown") +} + +func officeEndpoint(config conf.Config) string { + return fmt.Sprintf("%s%s", config.RootPath(), "convert/office") +} func isMultipartFormDataEndpoint(config conf.Config, path string) bool { var multipartFormDataEndpoints []string - multipartFormDataEndpoints = append(multipartFormDataEndpoints, mergeEndpoint) + multipartFormDataEndpoints = append(multipartFormDataEndpoints, mergeEndpoint(config)) if !config.DisableGoogleChrome() { multipartFormDataEndpoints = append( multipartFormDataEndpoints, - fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), - fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), - fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), + htmlEndpoint(config), + urlEndpoint(config), + markdownEndpoint(config), ) } if !config.DisableUnoconv() { multipartFormDataEndpoints = append( multipartFormDataEndpoints, - fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), + officeEndpoint(config), ) } for _, endpoint := range multipartFormDataEndpoints { diff --git a/internal/app/xhttp/handler_test.go b/internal/app/xhttp/handler_test.go index 5602d90b..8b1f5ee7 100644 --- a/internal/app/xhttp/handler_test.go +++ b/internal/app/xhttp/handler_test.go @@ -19,49 +19,51 @@ func TestPingHandler(t *testing.T) { // should return 200. config := conf.DefaultConfig() srv := New(config) - req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil) + endpoint := pingEndpoint(config) + req := httptest.NewRequest(http.MethodGet, endpoint, nil) test.AssertStatusCode(t, http.StatusOK, srv, req) // should return 405 as Method is wrong. - req = httptest.NewRequest(http.MethodPost, pingEndpoint, nil) + req = httptest.NewRequest(http.MethodPost, endpoint, nil) test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req) } func TestMergeHandler(t *testing.T) { config := conf.DefaultConfig() srv := New(config) + endpoint := mergeEndpoint(config) // should return 200. body, contentType := test.MergeMultipartForm(t, nil) - req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body) + req := httptest.NewRequest(http.MethodPost, endpoint, body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusOK, srv, req) // should return 405 as Method is wrong. - req = httptest.NewRequest(http.MethodGet, mergeEndpoint, nil) + req = httptest.NewRequest(http.MethodGet, endpoint, nil) test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req) // should return 415 as Content-Type is wrong. body, _ = test.MergeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body) + req = httptest.NewRequest(http.MethodPost, endpoint, body) test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req) // should return 400 as "waitTimeout" form field // value is < 0. body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body) + req = httptest.NewRequest(http.MethodPost, endpoint, body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusBadRequest, srv, req) // should return 400 as "waitTimeout" form field // value is is > config.MaximumWaitTimeout(). body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"}) - req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body) + req = httptest.NewRequest(http.MethodPost, endpoint, body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusBadRequest, srv, req) // should return 400 as "waitTimeout" form field // value is invalid. body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body) + req = httptest.NewRequest(http.MethodPost, endpoint, body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusBadRequest, srv, req) // should return 504. body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"}) - req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body) + req = httptest.NewRequest(http.MethodPost, endpoint, body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req) } @@ -69,7 +71,7 @@ func TestMergeHandler(t *testing.T) { func TestHTMLHandler(t *testing.T) { config := conf.DefaultConfig() srv := New(config) - endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint) + endpoint := htmlEndpoint(config) // should return 200. body, contentType := test.HTMLMultipartForm(t, nil) req := httptest.NewRequest(http.MethodPost, endpoint, body) @@ -224,7 +226,7 @@ func TestHTMLHandler(t *testing.T) { func TestURLHandler(t *testing.T) { config := conf.DefaultConfig() srv := New(config) - endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint) + endpoint := urlEndpoint(config) // should return 200. body, contentType := test.URLMultipartForm(t, nil) req := httptest.NewRequest(http.MethodPost, endpoint, body) @@ -379,7 +381,7 @@ func TestURLHandler(t *testing.T) { func TestMarkdownHandler(t *testing.T) { config := conf.DefaultConfig() srv := New(config) - endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint) + endpoint := markdownEndpoint(config) // should return 200. body, contentType := test.MarkdownMultipartForm(t, nil) req := httptest.NewRequest(http.MethodPost, endpoint, body) @@ -534,7 +536,7 @@ func TestMarkdownHandler(t *testing.T) { func TestOfficeHandler(t *testing.T) { config := conf.DefaultConfig() srv := New(config) - endpoint := fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint) + endpoint := officeEndpoint(config) // should return 200. body, contentType := test.OfficeMultipartForm(t, nil) req := httptest.NewRequest(http.MethodPost, endpoint, body) @@ -605,7 +607,7 @@ func TestWebhook(t *testing.T) { srv := New(config) // our custom server should receive the PDF. body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.WebhookURLArgKey): "http://localhost:3001/foo"}) - req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body) + req := httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusOK, srv, req) err := <-status @@ -616,7 +618,7 @@ func TestResultFilename(t *testing.T) { config := conf.DefaultConfig() srv := New(config) body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.ResultFilenameArgKey): "foo.pdf"}) - req := httptest.NewRequest(http.MethodPost, mergeEndpoint, body) + req := httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) rec := httptest.NewRecorder() srv.ServeHTTP(rec, req) diff --git a/internal/app/xhttp/middleware.go b/internal/app/xhttp/middleware.go index ff722c6a..4049b9ac 100644 --- a/internal/app/xhttp/middleware.go +++ b/internal/app/xhttp/middleware.go @@ -30,7 +30,7 @@ func contextMiddleware(config conf.Config) echo.MiddlewareFunc { // there is no need to create a Resource. if !isMultipartFormDataEndpoint(config, ctx.Path()) { // validate method for healthcheck endpoint. - if ctx.Path() == pingEndpoint && ctx.Request().Method != http.MethodGet { + if ctx.Path() == pingEndpoint(config) && ctx.Request().Method != http.MethodGet { err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed)) return ctx.LogRequestResult(err, false) } @@ -60,14 +60,14 @@ func contextMiddleware(config conf.Config) echo.MiddlewareFunc { } // loggerMiddleware logs the result of a request. -func loggerMiddleware() echo.MiddlewareFunc { +func loggerMiddleware(config conf.Config) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { ctx := context.MustCastFromEchoContext(c) err := next(ctx) // we do not want to log healthcheck requests if // log level is not set to DEBUG. - isDebug := ctx.Path() == pingEndpoint + isDebug := ctx.Path() == pingEndpoint(config) return ctx.LogRequestResult(err, isDebug) } } diff --git a/internal/app/xhttp/xhttp.go b/internal/app/xhttp/xhttp.go index 7324997e..02db10e1 100644 --- a/internal/app/xhttp/xhttp.go +++ b/internal/app/xhttp/xhttp.go @@ -11,22 +11,21 @@ func New(config conf.Config) *echo.Echo { srv.HideBanner = true srv.HidePort = true srv.Use(contextMiddleware(config)) - srv.Use(loggerMiddleware()) + srv.Use(loggerMiddleware(config)) srv.Use(cleanupMiddleware()) srv.Use(errorMiddleware()) - srv.GET(pingEndpoint, pingHandler) - srv.POST(mergeEndpoint, mergeHandler) + srv.GET(pingEndpoint(config), pingHandler) + srv.POST(mergeEndpoint(config), mergeHandler) if config.DisableGoogleChrome() && config.DisableUnoconv() { return srv } - g := srv.Group(convertGroupEndpoint) if !config.DisableGoogleChrome() { - g.POST(htmlEndpoint, htmlHandler) - g.POST(urlEndpoint, urlHandler) - g.POST(markdownEndpoint, markdownHandler) + srv.POST(htmlEndpoint(config), htmlHandler) + srv.POST(urlEndpoint(config), urlHandler) + srv.POST(markdownEndpoint(config), markdownHandler) } if !config.DisableUnoconv() { - g.POST(officeEndpoint, officeHandler) + srv.POST(officeEndpoint(config), officeHandler) } return srv } diff --git a/internal/app/xhttp/xhttp_test.go b/internal/app/xhttp/xhttp_test.go index 801407ec..f4fe3df0 100644 --- a/internal/app/xhttp/xhttp_test.go +++ b/internal/app/xhttp/xhttp_test.go @@ -1,7 +1,6 @@ package xhttp import ( - "fmt" "net/http" "net/http/httptest" "os" @@ -28,31 +27,31 @@ func TestDisableChromeEndpoints(t *testing.T) { assert.Nil(t, err) srv := New(config) // Ping endpoint should return 200. - req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil) + req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil) test.AssertStatusCode(t, http.StatusOK, srv, req) // Merge endpoint should return 200. body, contentType := test.MergeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body) + req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusOK, srv, req) // HTML endpoint should return 404. body, contentType = test.HTMLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body) + req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusNotFound, srv, req) // URL endpoint should return 404. body, contentType = test.URLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body) + req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusNotFound, srv, req) // Markdown endpoint should return 404. body, contentType = test.MarkdownMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body) + req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusNotFound, srv, req) // Office endpoint should return 200. body, contentType = test.OfficeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body) + req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusOK, srv, req) // finally... @@ -65,31 +64,31 @@ func TestDisableUnoconvEndpoints(t *testing.T) { assert.Nil(t, err) srv := New(config) // Ping endpoint should return 200. - req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil) + req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil) test.AssertStatusCode(t, http.StatusOK, srv, req) // Merge endpoint should return 200. body, contentType := test.MergeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body) + req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusOK, srv, req) // HTML endpoint should return 200. body, contentType = test.HTMLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body) + req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusOK, srv, req) // URL endpoint should return 200. body, contentType = test.URLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body) + req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusOK, srv, req) // Markdown endpoint should return 200. body, contentType = test.MarkdownMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body) + req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusOK, srv, req) // Office endpoint should return 404. body, contentType = test.OfficeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body) + req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusNotFound, srv, req) // finally... @@ -102,34 +101,71 @@ func TestDisableChromeAndUnoconvEndpoints(t *testing.T) { assert.Nil(t, err) srv := New(config) // Ping endpoint should return 200. - req := httptest.NewRequest(http.MethodGet, pingEndpoint, nil) + req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil) test.AssertStatusCode(t, http.StatusOK, srv, req) // Merge endpoint should return 200. body, contentType := test.MergeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, mergeEndpoint, body) + req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusOK, srv, req) // HTML endpoint should return 404. body, contentType = test.HTMLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, htmlEndpoint), body) + req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusNotFound, srv, req) // URL endpoint should return 404. body, contentType = test.URLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, urlEndpoint), body) + req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusNotFound, srv, req) // Markdown endpoint should return 404. body, contentType = test.MarkdownMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, markdownEndpoint), body) + req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusNotFound, srv, req) // Office endpoint should return 404. body, contentType = test.OfficeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", convertGroupEndpoint, officeEndpoint), body) + req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body) req.Header.Set(echo.HeaderContentType, contentType) test.AssertStatusCode(t, http.StatusNotFound, srv, req) // finally... os.Setenv(conf.DisableGoogleChromeEnvVar, "0") os.Setenv(conf.DisableUnoconvEnvVar, "0") } + +func TestCustomRootPath(t *testing.T) { + os.Setenv(conf.RootPathEnvVar, "/foo/") + config, err := conf.FromEnv() + assert.Nil(t, err) + srv := New(config) + // Ping endpoint should return 200. + req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil) + test.AssertStatusCode(t, http.StatusOK, srv, req) + // Merge endpoint should return 200. + body, contentType := test.MergeMultipartForm(t, nil) + req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body) + req.Header.Set(echo.HeaderContentType, contentType) + test.AssertStatusCode(t, http.StatusOK, srv, req) + // HTML endpoint should return 200. + body, contentType = test.HTMLMultipartForm(t, nil) + req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body) + req.Header.Set(echo.HeaderContentType, contentType) + test.AssertStatusCode(t, http.StatusOK, srv, req) + // URL endpoint should return 200. + body, contentType = test.URLMultipartForm(t, nil) + req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body) + req.Header.Set(echo.HeaderContentType, contentType) + test.AssertStatusCode(t, http.StatusOK, srv, req) + // Markdown endpoint should return 200. + body, contentType = test.MarkdownMultipartForm(t, nil) + req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body) + req.Header.Set(echo.HeaderContentType, contentType) + test.AssertStatusCode(t, http.StatusOK, srv, req) + // Office endpoint should return 200. + body, contentType = test.OfficeMultipartForm(t, nil) + req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body) + req.Header.Set(echo.HeaderContentType, contentType) + test.AssertStatusCode(t, http.StatusOK, srv, req) + // finally... + os.Setenv(conf.RootPathEnvVar, "/") +} diff --git a/internal/pkg/conf/conf.go b/internal/pkg/conf/conf.go index 89100c73..45d1bce1 100644 --- a/internal/pkg/conf/conf.go +++ b/internal/pkg/conf/conf.go @@ -34,6 +34,9 @@ const ( // LogLevelEnvVar contains the name // of the environment variable "LOG_LEVEL". LogLevelEnvVar string = "LOG_LEVEL" + // RootPathEnvVar contains the name + // of the environment variable "ROOT_PATH". + RootPathEnvVar string = "ROOT_PATH" // DefaultGoogleChromeRpccBufferSizeEnvVar contains the name // of the environment variable "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE". DefaultGoogleChromeRpccBufferSizeEnvVar string = "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE" @@ -51,6 +54,7 @@ type Config struct { disableGoogleChrome bool disableUnoconv bool logLevel xlog.Level + rootPath string maximumGoogleChromeRpccBufferSize int64 defaultGoogleChromeRpccBufferSize int64 } @@ -68,6 +72,7 @@ func DefaultConfig() Config { disableGoogleChrome: false, disableUnoconv: false, logLevel: xlog.InfoLevel, + rootPath: "/", maximumGoogleChromeRpccBufferSize: 104857600, // ~100 MB defaultGoogleChromeRpccBufferSize: 1048576, // 1 MB } @@ -163,6 +168,16 @@ func FromEnv() (Config, error) { if err != nil { return c, err } + rootPath, err := xassert.StringFromEnv( + RootPathEnvVar, + c.rootPath, + xassert.StringStartWith("/"), + xassert.StringEndWith("/"), + ) + c.rootPath = rootPath + if err != nil { + return c, err + } defaultGoogleChromeRpccBufferSize, err := xassert.Int64FromEnv( DefaultGoogleChromeRpccBufferSizeEnvVar, c.defaultGoogleChromeRpccBufferSize, @@ -242,6 +257,12 @@ func (c Config) LogLevel() xlog.Level { return c.logLevel } +// RootPath returns the rooth path from +// the configuration. +func (c Config) RootPath() string { + return c.rootPath +} + // MaximumGoogleChromeRpccBufferSize returns the maximum // Google Chrome rpcc buffer size from the configuration. func (c Config) MaximumGoogleChromeRpccBufferSize() int64 { diff --git a/internal/pkg/conf/conf_test.go b/internal/pkg/conf/conf_test.go index 45ed01da..678ac2aa 100644 --- a/internal/pkg/conf/conf_test.go +++ b/internal/pkg/conf/conf_test.go @@ -320,6 +320,29 @@ func TestLogLevelFromEnv(t *testing.T) { os.Unsetenv(LogLevelEnvVar) } +func TestRootPathFromEnv(t *testing.T) { + var ( + expected Config + result Config + err error + ) + // ROOT_PATH correctly set. + os.Setenv(RootPathEnvVar, "/foo/") + expected = DefaultConfig() + expected.rootPath = "/foo/" + result, err = FromEnv() + assert.Nil(t, err) + assert.Equal(t, expected, result) + os.Unsetenv(RootPathEnvVar) + // ROOT_PATH wrongly set. + os.Setenv(RootPathEnvVar, "foo") + expected = DefaultConfig() + result, err = FromEnv() + test.AssertError(t, err) + assert.Equal(t, expected, result) + os.Unsetenv(RootPathEnvVar) +} + func TestDefaultGoogleChromeRpccBufferSizeFromEnv(t *testing.T) { var ( expected Config @@ -368,6 +391,7 @@ func TestGetters(t *testing.T) { assert.Equal(t, result.disableGoogleChrome, result.DisableGoogleChrome()) assert.Equal(t, result.disableUnoconv, result.DisableUnoconv()) assert.Equal(t, result.logLevel, result.LogLevel()) + assert.Equal(t, result.rootPath, result.RootPath()) assert.Equal(t, result.maximumGoogleChromeRpccBufferSize, result.MaximumGoogleChromeRpccBufferSize()) assert.Equal(t, result.defaultGoogleChromeRpccBufferSize, result.DefaultGoogleChromeRpccBufferSize()) } diff --git a/internal/pkg/xassert/string.go b/internal/pkg/xassert/string.go index 540eee9b..d59bea8a 100644 --- a/internal/pkg/xassert/string.go +++ b/internal/pkg/xassert/string.go @@ -2,6 +2,7 @@ package xassert import ( "fmt" + "strings" "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" ) @@ -54,7 +55,67 @@ func StringOneOf(values []string) RuleString { } } +type ruleStringStartWith struct { + *baseRuleString + startWith string +} + +func (r ruleStringStartWith) validate() error { + const op string = "xassert.ruleStringStartWith.validate" + if strings.HasPrefix(r.value, r.startWith) { + return nil + } + return xerror.Invalid( + op, + fmt.Sprintf("'%s' should start with '%s', got '%s'", r.key, r.startWith, r.value), + nil, + ) +} + +/* +StringStartWith returns a RuleString for +validating that a string starts with +given string. +*/ +func StringStartWith(startWith string) RuleString { + return ruleStringStartWith{ + &baseRuleString{}, + startWith, + } +} + +type ruleStringEndWith struct { + *baseRuleString + endWith string +} + +func (r ruleStringEndWith) validate() error { + const op string = "xassert.ruleStringEndWith.validate" + if strings.HasSuffix(r.value, r.endWith) { + return nil + } + return xerror.Invalid( + op, + fmt.Sprintf("'%s' should end with '%s', got '%s'", r.key, r.endWith, r.value), + nil, + ) +} + +/* +StringEndWith returns a RuleString for +validating that a string ends with +given string. +*/ +func StringEndWith(endWith string) RuleString { + return ruleStringEndWith{ + &baseRuleString{}, + endWith, + } +} + // Compile-time checks to ensure type implements desired interfaces. var ( _ = RuleString(new(ruleStringOneOf)) + _ = RuleString(new(ruleStringStartWith)) + _ = RuleString(new(ruleStringEndWith)) ) diff --git a/internal/pkg/xassert/string_test.go b/internal/pkg/xassert/string_test.go index 87ad8065..fb1c0d4e 100644 --- a/internal/pkg/xassert/string_test.go +++ b/internal/pkg/xassert/string_test.go @@ -18,3 +18,27 @@ func TestStringOfOne(t *testing.T) { err = rule.validate() test.AssertError(t, err) } + +func TestStringStartWith(t *testing.T) { + rule := StringStartWith("foo") + // should be OK. + rule.with("FOO", "foobarfoo") + err := rule.validate() + assert.Nil(t, err) + // should not be OK. + rule.with("FOO", "qux") + err = rule.validate() + test.AssertError(t, err) +} + +func TestStringEndWith(t *testing.T) { + rule := StringEndWith("foo") + // should be OK. + rule.with("FOO", "foobarfoo") + err := rule.validate() + assert.Nil(t, err) + // should not be OK. + rule.with("FOO", "qux") + err = rule.validate() + test.AssertError(t, err) +} From 84a37f124b771bf1f2b4c3103a618b1b5a3b3a74 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Fri, 6 Dec 2019 17:30:36 +0100 Subject: [PATCH 11/18] adding documentation for ROOT_PATH --- build/docs/content/03-environment-variables.md | 12 ++++++++++++ docs/index.html | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/build/docs/content/03-environment-variables.md b/build/docs/content/03-environment-variables.md index e1f18731..b1522740 100644 --- a/build/docs/content/03-environment-variables.md +++ b/build/docs/content/03-environment-variables.md @@ -23,6 +23,18 @@ You may customize this value with the environment variable `DEFAULT_LISTEN_PORT` This environment variable accepts any string that can be turned into a port number. +## Root path + +By default, the API root path is `/`. + +You may customize this value with the environment variable `ROOT_PATH`. + +This environment variable accepts a string starting and ending with `/`. + +For instance, `/gotenberg/` is a valid value while `gotenberg` is not. + +> This is useful if you wish to do service discovery via URL paths. + ## Disable Google Chrome In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME` diff --git a/docs/index.html b/docs/index.html index 85c3b75a..8aca8fff 100755 --- a/docs/index.html +++ b/docs/index.html @@ -264,6 +264,22 @@ about what’s going on.

This environment variable accepts any string that can be turned into a port number.

+

Root path

+ +

By default, the API root path is /.

+ +

You may customize this value with the environment variable ROOT_PATH.

+ +

This environment variable accepts a string starting and ending with /.

+ +

For instance, /gotenberg/ is a valid value while gotenberg is not.

+ +
+

This is useful if you wish to do service discovery via URL paths.

+
+

Disable Google Chrome

From ebfe99be795154144166507d95053371de5d4220 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Mon, 9 Dec 2019 14:36:01 +0100 Subject: [PATCH 12/18] adding documentation on how to change gid/uid + renamming DOCKER_REPOSITORY to DOCKER_REGISTRY --- Makefile | 24 +++++++------- build/base/Dockerfile | 7 +++-- build/docs/content/01-install.md | 21 +++++++++++-- docs/index.html | 54 +++++++++++++++++++++----------- scripts/publish.sh | 18 +++++------ scripts/tests.sh | 8 ++--- 6 files changed, 84 insertions(+), 48 deletions(-) diff --git a/Makefile b/Makefile index ece45530..2430b5c0 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,9 @@ GOLANG_VERSION=1.13 VERSION=snapshot DOCKER_USER= DOCKER_PASSWORD= -DOCKER_REPOSITORY=thecodingmachine +DOCKER_REGISTRY=thecodingmachine +GOTENBERG_USER_GID=1001 +GOTENBERG_USER_UID=1001 GOLANGCI_LINT_VERSION=1.19.1 CODE_COVERAGE=0 TINI_VERSION=0.18.0 @@ -19,12 +21,12 @@ DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=1048576 # build the base Docker image. base: - docker build -t $(DOCKER_REPOSITORY)/gotenberg:base -f build/base/Dockerfile . + docker build --build-arg GOTENBERG_USER_GID=$(GOTENBERG_USER_GID) --build-arg GOTENBERG_USER_UID=$(GOTENBERG_USER_UID) -t $(DOCKER_REGISTRY)/gotenberg:base -f build/base/Dockerfile . # build the workspace Docker image. workspace: make base - docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:workspace -f build/workspace/Dockerfile . + docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:workspace -f build/workspace/Dockerfile . # gofmt and goimports all go files. fmt: @@ -34,30 +36,30 @@ fmt: # run all linters. lint: make workspace - docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:lint -f build/lint/Dockerfile . - docker run --rm $(DOCKER_REPOSITORY)/gotenberg:lint + docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:lint -f build/lint/Dockerfile . + docker run --rm $(DOCKER_REGISTRY)/gotenberg:lint # run all tests. tests: make workspace - ./scripts/tests.sh $(DOCKER_REPOSITORY) $(CODE_COVERAGE) + ./scripts/tests.sh $(DOCKER_REGISTRY) $(CODE_COVERAGE) # generate documentation. doc: make workspace - docker build -t $(DOCKER_REPOSITORY)/gotenberg:docs -f build/docs/Dockerfile . - docker run --rm -it -v "$(PWD):/gotenberg/docs" $(DOCKER_REPOSITORY)/gotenberg:docs + docker build -t $(DOCKER_REGISTRY)/gotenberg:docs -f build/docs/Dockerfile . + docker run --rm -it -v "$(PWD):/gotenberg/docs" $(DOCKER_REGISTRY)/gotenberg:docs # build Gotenberg Docker image. image: make workspace - docker build --build-arg VERSION=$(VERSION) --build-arg TINI_VERSION=$(TINI_VERSION) -t $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) -f build/package/Dockerfile . + docker build --build-arg VERSION=$(VERSION) --build-arg TINI_VERSION=$(TINI_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:$(VERSION) -f build/package/Dockerfile . # start the API using previously built Docker image. gotenberg: - docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -e DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=$(DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REPOSITORY)/gotenberg:$(VERSION) + docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -e DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=$(DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REGISTRY)/gotenberg:$(VERSION) # publish Gotenberg images according to version. publish: make workspace - ./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REPOSITORY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD) \ No newline at end of file + ./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REGISTRY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD) \ No newline at end of file diff --git a/build/base/Dockerfile b/build/base/Dockerfile index c40df230..31f78d84 100644 --- a/build/base/Dockerfile +++ b/build/base/Dockerfile @@ -104,7 +104,10 @@ COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf # | non-root user. # | -RUN groupadd --gid 1001 gotenberg \ - && useradd --uid 1001 --gid gotenberg --shell /bin/bash --home /gotenberg --no-create-home gotenberg \ +ARG GOTENBERG_USER_GID=1001 +ARG GOTENBERG_USER_UID=1001 + +RUN groupadd --gid ${GOTENBERG_USER_GID} gotenberg \ + && useradd --uid ${GOTENBERG_USER_UID} --gid gotenberg --shell /bin/bash --home /gotenberg --no-create-home gotenberg \ && mkdir /gotenberg \ && chown gotenberg: /gotenberg \ No newline at end of file diff --git a/build/docs/content/01-install.md b/build/docs/content/01-install.md index ec4cdca5..7e2c4d0e 100644 --- a/build/docs/content/01-install.md +++ b/build/docs/content/01-install.md @@ -4,8 +4,6 @@ title: Install Gotenberg is shipped within a Docker image. -> It uses a dedicated non-root user called `gotenberg` with uid and gid `1001`. - You may start it with: ```bash @@ -14,6 +12,23 @@ $ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6 > The API will be available at [http://localhost:3000](http://localhost:3000). +The image uses a dedicated non-root user called `gotenberg` with uid and gid `1001`. + +If you wish to change those uid and gid, you will have to: + +* clone the project +* re-build the image +* publish the image in your own Docker registry + +For instance: + +```bash +$ git clone https://github.com/thecodingmachine/gotenberg.git +$ make publish GOTENBERG_USER_GID=your_custom_gid GOTENBERG_USER_UID=your_custom_uid DOCKER_REGISTRY=your_registry DOCKER_USER=registry_user DOCKER_PASSWORD=registry_password VERSION=6.1.0 +``` + +> `master` branch is always up-to-date with the latest version of the API. + ## Docker Compose You may also add it in your Docker Compose stack: @@ -39,7 +54,7 @@ Make sure to provide enough memory and CPU requests (for instance `512Mi` and `0 > The more resources are granted, the quicker will be the conversions. -In the deployment specification of the pod, also specify the uid `1001` of the user `gotenberg`: +In the deployment specification of the pod, also specify the uid of the user `gotenberg`: ``` securityContext: diff --git a/docs/index.html b/docs/index.html index 85c3b75a..215116e8 100755 --- a/docs/index.html +++ b/docs/index.html @@ -132,33 +132,49 @@ Install

Gotenberg is shipped within a Docker image.

-
-

It uses a dedicated non-root user called gotenberg with uid and gid 1001.

-
-

You may start it with:

-
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
+
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
 

The API will be available at http://localhost:3000.

+

The image uses a dedicated non-root user called gotenberg with uid and gid 1001.

+ +

If you wish to change those uid and gid, you will have to:

+ +
    +
  • clone the project
  • +
  • re-build the image
  • +
  • publish the image in your own Docker registry
  • +
+ +

For instance:

+ +
$ git clone https://github.com/thecodingmachine/gotenberg.git
+$ make publish GOTENBERG_USER_GID=your_custom_gid GOTENBERG_USER_UID=your_custom_uid DOCKER_REGISTRY=your_registry DOCKER_USER=registry_user DOCKER_PASSWORD=registry_password VERSION=6.1.0 
+
+ +
+

master branch is always up-to-date with the latest version of the API.

+
+

Docker Compose

You may also add it in your Docker Compose stack:

-
version: '3'
+
version: '3'
 
-services:
+services:
 
   # your others services
 
-  gotenberg:
-    image: thecodingmachine/gotenberg:6
+  gotenberg:
+    image: thecodingmachine/gotenberg:6
 
@@ -177,7 +193,7 @@

The more resources are granted, the quicker will be the conversions.

-

In the deployment specification of the pod, also specify the uid 1001 of the user gotenberg:

+

In the deployment specification of the pod, also specify the uid of the user gotenberg:

securityContext:
   privileged: false
@@ -657,8 +673,8 @@ $client->store($request, $dest);
     --url http://localhost:3000/convert/html \
     --header 'Content-Type: multipart/form-data' \
     --form files=@index.html \
-    --form paperWidth=8.27 \
-    --form paperHeight=11.69 \
+    --form paperWidth=8.27 \
+    --form paperHeight=11.69 \
     --form marginTop=0 \
     --form marginBottom=0 \
     --form marginLeft=0 \
@@ -723,7 +739,7 @@ a lot on JavaScript for rendering.

--url http://localhost:3000/convert/html \ --header 'Content-Type: multipart/form-data' \ --form files=@index.html \ - --form waitDelay=5.5 \ + --form waitDelay=5.5 \ -o result.pdf
@@ -1189,7 +1205,7 @@ If unsucessful, it returns a 504 HTTP code.

--url http://localhost:3000/convert/html \ --header 'Content-Type: multipart/form-data' \ --form files=@index.html \ - --form waitTimeout=2.5 + --form waitTimeout=2.5

http://localhost:3
 
$ composer require php-http/guzzle6-adapter
 
-

Then the PHP client:

+

Then the PHP client:

$ composer require thecodingmachine/gotenberg-php-client
 
@@ -474,7 +474,7 @@ use TheCodingMachine\Gotenberg\HTMLRequest; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); $index = DocumentFactory::makeFromPath('index.html', 'index.html'); $request = new HTMLRequest($index); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest);
@@ -569,7 +569,7 @@ $footer = DocumentFactory::makeFromPath('footer.html', 'footer.html& $request = new HTMLRequest($index); $request->setHeader($header); $request->setFooter($footer); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -665,7 +665,7 @@ $assets = [ ]; $request = new HTMLRequest($index); $request->setAssets($assets); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -731,7 +731,7 @@ $request = new HTMLRequest($index); $request->setPaperSize(Request::A4); $request->setMargins(Request::NO_MARGINS); $request->setLandscape(true); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -787,7 +787,7 @@ $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\ $index = DocumentFactory::makeFromPath('index.html', 'index.html'); $request = new HTMLRequest($index); $request->setWaitDelay(5.5); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -846,7 +846,7 @@ $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\ $index = DocumentFactory::makeFromPath('index.html', 'index.html'); $request = new HTMLRequest($index); $request->setGoogleChromeRpccBufferSize(1048576); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -912,7 +912,7 @@ use TheCodingMachine\Gotenberg\URLRequest; $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); $request = new URLRequest('https://google.com'); $request->setMargins(Request::NO_MARGINS); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -955,7 +955,15 @@ canonical key for accept-encoding is Accept-Encoding.< PHP

-

// TODO

+
use TheCodingMachine\Gotenberg\Client;
+use TheCodingMachine\Gotenberg\URLRequest;
+
+$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
+$request = new URLRequest('https://google.com');
+$request->addRemoteURLHTTPHeader('Your-Header', 'Foo')
+$dest = 'result.pdf';
+$client->store($request, $dest);
+
@@ -1030,7 +1038,7 @@ $markdowns = [ DocumentFactory::makeFromPath('file.md', 'file.md'), ]; $request = new MarkdownRequest($index, $markdowns); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -1111,7 +1119,7 @@ $files = [ DocumentFactory::makeFromPath('document2.docx', 'document2.docx'), ]; $request = new OfficeRequest($files); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -1164,7 +1172,7 @@ $files = [ ]; $request = new OfficeRequest($files); $request->setLandscape(true); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -1229,7 +1237,7 @@ $files = [ DocumentFactory::makeFromPath('file2.pdf', 'file2.pdf'), ]; $request = new MergeRequest($files); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -1292,7 +1300,7 @@ $client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\ $index = DocumentFactory::makeFromPath('index.html', 'index.html'); $request = new HTMLRequest($index); $request->setWaitTimeout(2.5); -$dest = "result.pdf"; +$dest = 'result.pdf'; $client->store($request, $dest); @@ -1449,7 +1457,17 @@ canonical key for accept-encoding is Accept-Encoding.< PHP -

// TODO

+
use TheCodingMachine\Gotenberg\Client;
+use TheCodingMachine\Gotenberg\DocumentFactory;
+use TheCodingMachine\Gotenberg\HTMLRequest;
+
+$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
+$index = DocumentFactory::makeFromPath('index.html', 'index.html');
+$request = new HTMLRequest($index);
+$request->setWebhookURL('http://myapp.com/webhook/');
+$request->addWebhookURLHTTPHeader('Your-Header', 'Foo');
+$resp = $client->post($request);
+
diff --git a/internal/app/xhttp/handler.go b/internal/app/xhttp/handler.go index 07959870..e15f6443 100644 --- a/internal/app/xhttp/handler.go +++ b/internal/app/xhttp/handler.go @@ -138,7 +138,7 @@ func urlHandler(c echo.Context) error { if err != nil { return err } - opts.CustomHeaders = resource.RemoteURLCustomHeaders(r) + opts.CustomHTTPHeaders = resource.RemoteURLCustomHTTPHeaders(r) if !r.HasArg(resource.RemoteURLArgKey) { return xerror.Invalid( op, @@ -338,14 +338,14 @@ func convertAsync(ctx context.Context, p printer.Printer, filename, fpath string } req.Header.Set(echo.HeaderContentType, "application/pdf") // set custom headers (if any). - customHeaders := resource.WebhookURLCustomHeaders(r) + customHeaders := resource.WebhookURLCustomHTTPHeaders(r) if len(customHeaders) > 0 { for key, value := range customHeaders { req.Header.Set(key, value) - logger.DebugfOp(op, "set '%s' to custom header '%s'", value, key) + logger.DebugfOp(op, "set '%s' to custom HTTP header '%s'", value, key) } } else { - logger.DebugOp(op, "skipping custom headers as none have been provided...") + logger.DebugOp(op, "skipping custom HTTP headers as none have been provided...") } // send the result file. logger.DebugfOp( diff --git a/internal/app/xhttp/handler_test.go b/internal/app/xhttp/handler_test.go index 792569ad..7e141d53 100644 --- a/internal/app/xhttp/handler_test.go +++ b/internal/app/xhttp/handler_test.go @@ -582,7 +582,7 @@ func TestOfficeHandler(t *testing.T) { func TestWebhook(t *testing.T) { customHeaderRealKey := http.CanonicalHeaderKey("MyCustomHeader") - customHeaderKey := fmt.Sprintf("%s%s", resource.WebhookURLCustomHeaderCanonicalBaseKey, customHeaderRealKey) + customHeaderKey := fmt.Sprintf("%s%s", resource.WebhookURLCustomHTTPHeaderCanonicalBaseKey, customHeaderRealKey) customHeaderValue := "foo" status := make(chan error, 2) rcv := echo.New() diff --git a/internal/app/xhttp/pkg/context/context.go b/internal/app/xhttp/pkg/context/context.go index 41f430fb..b8e0b892 100644 --- a/internal/app/xhttp/pkg/context/context.go +++ b/internal/app/xhttp/pkg/context/context.go @@ -80,7 +80,7 @@ func (ctx *Context) WithResource(directoryName string) error { } // retrieve custom headers from request. for key, value := range ctx.Request().Header { - r.WithCustomHeader(key, value[0]) + r.WithCustomHTTPHeader(key, value[0]) } // retrieve form values from request. for _, key := range resource.ArgKeys() { diff --git a/internal/app/xhttp/pkg/resource/header.go b/internal/app/xhttp/pkg/resource/header.go index 9a9745d7..fa4c8814 100644 --- a/internal/app/xhttp/pkg/resource/header.go +++ b/internal/app/xhttp/pkg/resource/header.go @@ -5,15 +5,15 @@ import ( ) const ( - // RemoteURLCustomHeaderCanonicalBaseKey is the base key + // RemoteURLCustomHTTPHeaderCanonicalBaseKey is the base key // of custom headers send to the remote URL. - RemoteURLCustomHeaderCanonicalBaseKey string = "Gotenberg-Remoteurl-" - // WebhookURLCustomHeaderCanonicalBaseKey is the base key + RemoteURLCustomHTTPHeaderCanonicalBaseKey string = "Gotenberg-Remoteurl-" + // WebhookURLCustomHTTPHeaderCanonicalBaseKey is the base key // of custom headers send to the webhook URL. - WebhookURLCustomHeaderCanonicalBaseKey string = "Gotenberg-Webhookurl-" + WebhookURLCustomHTTPHeaderCanonicalBaseKey string = "Gotenberg-Webhookurl-" ) -func fetchCustomHeaders(r Resource, baseKey string) map[string]string { +func fetchCustomHTTPHeaders(r Resource, baseKey string) map[string]string { customHeaders := make(map[string]string) for key, value := range r.customHeaders { if strings.Contains(key, baseKey) { @@ -24,14 +24,14 @@ func fetchCustomHeaders(r Resource, baseKey string) map[string]string { return customHeaders } -// RemoteURLCustomHeaders is a helper for retrieving +// RemoteURLCustomHTTPHeaders is a helper for retrieving // the custom headers for the URL conversion. -func RemoteURLCustomHeaders(r Resource) map[string]string { - return fetchCustomHeaders(r, RemoteURLCustomHeaderCanonicalBaseKey) +func RemoteURLCustomHTTPHeaders(r Resource) map[string]string { + return fetchCustomHTTPHeaders(r, RemoteURLCustomHTTPHeaderCanonicalBaseKey) } -// WebhookURLCustomHeaders is a helper for retrieving +// WebhookURLCustomHTTPHeaders is a helper for retrieving // the custom headers for the webhook URL. -func WebhookURLCustomHeaders(r Resource) map[string]string { - return fetchCustomHeaders(r, WebhookURLCustomHeaderCanonicalBaseKey) +func WebhookURLCustomHTTPHeaders(r Resource) map[string]string { + return fetchCustomHTTPHeaders(r, WebhookURLCustomHTTPHeaderCanonicalBaseKey) } diff --git a/internal/app/xhttp/pkg/resource/header_test.go b/internal/app/xhttp/pkg/resource/header_test.go index 5f9856ee..35a944f8 100644 --- a/internal/app/xhttp/pkg/resource/header_test.go +++ b/internal/app/xhttp/pkg/resource/header_test.go @@ -17,16 +17,16 @@ func TestRemoteURLCustomHeaders(t *testing.T) { // should find the custom header. customHeaderValue := "bar" customHeaderCanonicalRealKey := "Foo" - customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", RemoteURLCustomHeaderCanonicalBaseKey, customHeaderCanonicalRealKey)) - r.WithCustomHeader(customHeaderCanonicalKey, customHeaderValue) - r.WithCustomHeader("Bar", "Bar") + customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", RemoteURLCustomHTTPHeaderCanonicalBaseKey, customHeaderCanonicalRealKey)) + r.WithCustomHTTPHeader(customHeaderCanonicalKey, customHeaderValue) + r.WithCustomHTTPHeader("Bar", "Bar") expected := map[string]string{ customHeaderCanonicalRealKey: customHeaderValue, } notExpected := map[string]string{ customHeaderCanonicalKey: customHeaderValue, } - v := RemoteURLCustomHeaders(r) + v := RemoteURLCustomHTTPHeaders(r) assert.Equal(t, expected, v) assert.NotEqual(t, notExpected, v) } @@ -39,16 +39,16 @@ func TestWebhookURLCustomHeaders(t *testing.T) { // should find the custom header. customHeaderValue := "bar" customHeaderCanonicalRealKey := "Foo" - customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", WebhookURLCustomHeaderCanonicalBaseKey, customHeaderCanonicalRealKey)) - r.WithCustomHeader(customHeaderCanonicalKey, customHeaderValue) - r.WithCustomHeader("Bar", "Bar") + customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", WebhookURLCustomHTTPHeaderCanonicalBaseKey, customHeaderCanonicalRealKey)) + r.WithCustomHTTPHeader(customHeaderCanonicalKey, customHeaderValue) + r.WithCustomHTTPHeader("Bar", "Bar") expected := map[string]string{ customHeaderCanonicalRealKey: customHeaderValue, } notExpected := map[string]string{ customHeaderCanonicalKey: customHeaderValue, } - v := WebhookURLCustomHeaders(r) + v := WebhookURLCustomHTTPHeaders(r) assert.Equal(t, expected, v) assert.NotEqual(t, notExpected, v) } diff --git a/internal/app/xhttp/pkg/resource/resource.go b/internal/app/xhttp/pkg/resource/resource.go index 29d1241d..3a983f18 100644 --- a/internal/app/xhttp/pkg/resource/resource.go +++ b/internal/app/xhttp/pkg/resource/resource.go @@ -75,19 +75,19 @@ func (r Resource) Close() error { return nil } -// WithCustomHeader add a new custom header to the Resource. +// WithCustomHTTPHeader add a new custom header to the Resource. // Given key should be in canonical format. -func (r *Resource) WithCustomHeader(key string, value string) { - const op string = "resource.Resource.WithCustomHeader" +func (r *Resource) WithCustomHTTPHeader(key string, value string) { + const op string = "resource.Resource.WithCustomHTTPHeader" // should already be in canonical format. canonicalKey := http.CanonicalHeaderKey(key) - if strings.Contains(canonicalKey, RemoteURLCustomHeaderCanonicalBaseKey) || - strings.Contains(canonicalKey, WebhookURLCustomHeaderCanonicalBaseKey) { + if strings.Contains(canonicalKey, RemoteURLCustomHTTPHeaderCanonicalBaseKey) || + strings.Contains(canonicalKey, WebhookURLCustomHTTPHeaderCanonicalBaseKey) { r.customHeaders[canonicalKey] = value - r.logger.DebugfOp(op, "added '%s' with value '%s' to resource custom headers", canonicalKey, value) + r.logger.DebugfOp(op, "added '%s' with value '%s' to resource custom HTTP headers", canonicalKey, value) return } - r.logger.DebugfOp(op, "skipping '%s' as it is not a custom header...", canonicalKey) + r.logger.DebugfOp(op, "skipping '%s' as it is not a custom HTTP header...", canonicalKey) } // WithArg add a new argument to the Resource. diff --git a/internal/pkg/printer/chrome.go b/internal/pkg/printer/chrome.go index dce45a05..71e16b32 100644 --- a/internal/pkg/printer/chrome.go +++ b/internal/pkg/printer/chrome.go @@ -31,19 +31,19 @@ type chromePrinter struct { // ChromePrinterOptions helps customizing the // Google Chrome Printer behaviour. type ChromePrinterOptions struct { - WaitTimeout float64 - WaitDelay float64 - HeaderHTML string - FooterHTML string - PaperWidth float64 - PaperHeight float64 - MarginTop float64 - MarginBottom float64 - MarginLeft float64 - MarginRight float64 - Landscape bool - RpccBufferSize int64 - CustomHeaders map[string]string + WaitTimeout float64 + WaitDelay float64 + HeaderHTML string + FooterHTML string + PaperWidth float64 + PaperHeight float64 + MarginTop float64 + MarginBottom float64 + MarginLeft float64 + MarginRight float64 + Landscape bool + RpccBufferSize int64 + CustomHTTPHeaders map[string]string } // DefaultChromePrinterOptions returns the default @@ -51,19 +51,19 @@ type ChromePrinterOptions struct { func DefaultChromePrinterOptions(config conf.Config) ChromePrinterOptions { const defaultHeaderFooterHTML string = "" return ChromePrinterOptions{ - WaitTimeout: config.DefaultWaitTimeout(), - WaitDelay: 0.0, - HeaderHTML: defaultHeaderFooterHTML, - FooterHTML: defaultHeaderFooterHTML, - PaperWidth: 8.27, - PaperHeight: 11.7, - MarginTop: 1.0, - MarginBottom: 1.0, - MarginLeft: 1.0, - MarginRight: 1.0, - Landscape: false, - RpccBufferSize: config.DefaultGoogleChromeRpccBufferSize(), - CustomHeaders: make(map[string]string), + WaitTimeout: config.DefaultWaitTimeout(), + WaitDelay: 0.0, + HeaderHTML: defaultHeaderFooterHTML, + FooterHTML: defaultHeaderFooterHTML, + PaperWidth: 8.27, + PaperHeight: 11.7, + MarginTop: 1.0, + MarginBottom: 1.0, + MarginLeft: 1.0, + MarginRight: 1.0, + Landscape: false, + RpccBufferSize: config.DefaultGoogleChromeRpccBufferSize(), + CustomHTTPHeaders: make(map[string]string), } } @@ -148,7 +148,7 @@ func (p chromePrinter) Print(destination string) error { return err } // add custom headers (if any). - if err := p.setCustomHeaders(ctx, targetClient); err != nil { + if err := p.setCustomHTTPHeaders(ctx, targetClient); err != nil { return err } // listen for all events. @@ -254,18 +254,18 @@ func (p chromePrinter) enableEvents(ctx context.Context, client *cdp.Client) err return nil } -func (p chromePrinter) setCustomHeaders(ctx context.Context, client *cdp.Client) error { - const op string = "printer.chromePrinter.setCustomHeaders" +func (p chromePrinter) setCustomHTTPHeaders(ctx context.Context, client *cdp.Client) error { + const op string = "printer.chromePrinter.setCustomHTTPHeaders" resolver := func() error { - if len(p.opts.CustomHeaders) == 0 { - p.logger.DebugOp(op, "skipping custom headers as none have been provided...") + if len(p.opts.CustomHTTPHeaders) == 0 { + p.logger.DebugOp(op, "skipping custom HTTP headers as none have been provided...") return nil } customHeaders := make(map[string]string) // useless but for the logs. - for key, value := range p.opts.CustomHeaders { + for key, value := range p.opts.CustomHTTPHeaders { customHeaders[key] = value - p.logger.DebugfOp(op, "set '%s' to custom header '%s'", value, key) + p.logger.DebugfOp(op, "set '%s' to custom HTTP header '%s'", value, key) } b, err := json.Marshal(customHeaders) if err != nil { From 1eeb5fc2b0c0aec6281d4d4bf6a21f2a5e649cc5 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Mon, 9 Dec 2019 17:20:56 +0100 Subject: [PATCH 14/18] typo in variable --- internal/app/xhttp/handler.go | 6 +++--- internal/pkg/printer/chrome.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/app/xhttp/handler.go b/internal/app/xhttp/handler.go index e15f6443..92cf22ea 100644 --- a/internal/app/xhttp/handler.go +++ b/internal/app/xhttp/handler.go @@ -338,9 +338,9 @@ func convertAsync(ctx context.Context, p printer.Printer, filename, fpath string } req.Header.Set(echo.HeaderContentType, "application/pdf") // set custom headers (if any). - customHeaders := resource.WebhookURLCustomHTTPHeaders(r) - if len(customHeaders) > 0 { - for key, value := range customHeaders { + customHTTPHeaders := resource.WebhookURLCustomHTTPHeaders(r) + if len(customHTTPHeaders) > 0 { + for key, value := range customHTTPHeaders { req.Header.Set(key, value) logger.DebugfOp(op, "set '%s' to custom HTTP header '%s'", value, key) } diff --git a/internal/pkg/printer/chrome.go b/internal/pkg/printer/chrome.go index 71e16b32..10337fab 100644 --- a/internal/pkg/printer/chrome.go +++ b/internal/pkg/printer/chrome.go @@ -261,13 +261,13 @@ func (p chromePrinter) setCustomHTTPHeaders(ctx context.Context, client *cdp.Cli p.logger.DebugOp(op, "skipping custom HTTP headers as none have been provided...") return nil } - customHeaders := make(map[string]string) + customHTTPHeaders := make(map[string]string) // useless but for the logs. for key, value := range p.opts.CustomHTTPHeaders { - customHeaders[key] = value + customHTTPHeaders[key] = value p.logger.DebugfOp(op, "set '%s' to custom HTTP header '%s'", value, key) } - b, err := json.Marshal(customHeaders) + b, err := json.Marshal(customHTTPHeaders) if err != nil { return err } From 81e7bab22a0602c04915c72059f613eecd958688 Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Mon, 9 Dec 2019 17:26:00 +0100 Subject: [PATCH 15/18] updating documentation with Golang examples for custom HTTP headers --- build/docs/content/05-url.md | 12 +++++++++++- build/docs/content/10-webhook.md | 12 +++++++++++- docs/index.html | 22 ++++++++++++++++++++-- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/build/docs/content/05-url.md b/build/docs/content/05-url.md index 900f91f5..16c42b83 100644 --- a/build/docs/content/05-url.md +++ b/build/docs/content/05-url.md @@ -81,7 +81,17 @@ $ curl --request POST \ ### Go -// TODO +```golang +import "github.com/thecodingmachine/gotenberg-go-client/v6" + +func main() { + c := &gotenberg.Client{Hostname: "http://localhost:3000"} + req := gotenberg.NewURLRequest("https://google.com") + req.AddRemoteURLHTTPHeader("Your-Header", "Foo") + dest := "result.pdf" + c.Store(req, dest) +} +``` ### PHP diff --git a/build/docs/content/10-webhook.md b/build/docs/content/10-webhook.md index 2824e192..06c97f24 100644 --- a/build/docs/content/10-webhook.md +++ b/build/docs/content/10-webhook.md @@ -124,7 +124,17 @@ $ curl --request POST \ ### Go -// TODO +```golang +import "github.com/thecodingmachine/gotenberg-go-client/v6" + +func main() { + c := &gotenberg.Client{Hostname: "http://localhost:3000"} + req, _ := gotenberg.NewHTMLRequest("index.html") + req.WebhookURL("http://myapp.com/webhook/") + req.AddWebhookURLHTTPHeader("Your-Header", "Foo") + resp, _ := c.Post(req) +} +``` ### PHP diff --git a/docs/index.html b/docs/index.html index 63fe13fe..95af3d47 100755 --- a/docs/index.html +++ b/docs/index.html @@ -949,7 +949,16 @@ canonical key for accept-encoding is Accept-Encoding.< Go -

// TODO

+
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
+func main() {
+    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+    req := gotenberg.NewURLRequest("https://google.com")
+    req.AddRemoteURLHTTPHeader("Your-Header", "Foo")
+    dest := "result.pdf"
+    c.Store(req, dest)
+}
+

Go

-

// TODO

+
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
+func main() {
+    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+    req, _ := gotenberg.NewHTMLRequest("index.html")
+    req.WebhookURL("http://myapp.com/webhook/")
+    req.AddWebhookURLHTTPHeader("Your-Header", "Foo")
+    resp, _ := c.Post(req)
+}
+

http://localhost:3 Go client

-
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v6
+
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v7
 
+

See also the example from the README.

+

PHP client

@@ -238,6 +240,8 @@ Gotenberg API is available at http://localhost:3
$ composer require thecodingmachine/gotenberg-php-client
 
+

See also the example from the README.

+

Community clients

@@ -453,14 +457,13 @@ which will be converted to PDF.

Go -
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewHTMLRequest("index.html")
-    dest := "result.pdf"
-    c.Store(req, dest)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
+req := gotenberg.NewHTMLRequest(index)
+dest := "result.pdf"
+c.Store(req, dest)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewHTMLRequest("index.html")
-    req.Header("header.html")
-    req.Footer("footer.html")
-    dest := "result.pdf"
-    c.Store(req, dest)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
+header, _ := gotenberg.NewDocumentFromPath("header.html", "/path/to/file")
+footer, _ := gotenberg.NewDocumentFromPath("footer.html", "/path/to/file")
+req := gotenberg.NewHTMLRequest(index)
+req.Header(header)
+req.Footer(footer)
+dest := "result.pdf"
+c.Store(req, dest)
 

fonts section.

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewHTMLRequest("index.html")
-    req.Assets("font.woff", "img.gif", "style.css")
-    dest := "result.pdf"
-    c.Store(req, dest)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
+style, _ := gotenberg.NewDocumentFromPath("style.css", "/path/to/file")
+img, _ := gotenberg.NewDocumentFromPath("img.png", "/path/to/file")
+font, _ := gotenberg.NewDocumentFromPath("font.woff", "/path/to/file")
+req := gotenberg.NewHTMLRequest(index)
+req.Assets(style, img, font)
+dest := "result.pdf"
+c.Store(req, dest)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewHTMLRequest("index.html")
-    req.PaperSize(gotenberg.A4)
-    req.Margins(gotenberg.NoMargins)
-    req.Landscape(true)
-    dest := "result.pdf"
-    c.Store(req, dest)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
+req := gotenberg.NewHTMLRequest(index)
+req.PaperSize(gotenberg.A4)
+req.Margins(gotenberg.NoMargins)
+req.Landscape(true)
+dest := "result.pdf"
+c.Store(req, dest)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewHTMLRequest("index.html")
-    req.WaitDelay(5.5)
-    dest := "result.pdf"
-    c.Store(req, dest)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
+req := gotenberg.NewHTMLRequest(index)
+req.WaitDelay(5.5)
+dest := "result.pdf"
+c.Store(req, dest)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewHTMLRequest("index.html")
-    req.GoogleChromeRpccBufferSize(1048576)
-    dest := "result.pdf"
-    c.Store(req, dest)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
+req := gotenberg.NewHTMLRequest(index)
+req.GoogleChromeRpccBufferSize(1048576)
+dest := "result.pdf"
+c.Store(req, dest)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req := gotenberg.NewURLRequest("https://google.com")
-    req.Margins(gotenberg.NoMargins)
-    dest := "result.pdf"
-    c.Store(req, dest)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+req := gotenberg.NewURLRequest("https://google.com")
+req.Margins(gotenberg.NoMargins)
+dest := "result.pdf"
+c.Store(req, dest)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req := gotenberg.NewURLRequest("https://google.com")
-    req.AddRemoteURLHTTPHeader("Your-Header", "Foo")
-    dest := "result.pdf"
-    c.Store(req, dest)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+req := gotenberg.NewURLRequest("https://google.com")
+req.AddRemoteURLHTTPHeader("Your-Header", "Foo")
+dest := "result.pdf"
+c.Store(req, dest)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewMarkdownRequest("index.html", "file.md")
-    dest := "result.pdf"
-    c.Store(req, dest)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
+markdown, _ := gotenberg.NewDocumentFromPath("file.md", "/path/to/file")
+req := gotenberg.NewMarkdownRequest(index, markdown)
+dest := "result.pdf"
+c.Store(req, dest)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewOfficeRequest("document.docx", "document2.docx")
-    dest := "result.pdf"
-    c.Store(req, dest)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
+doc2, _ := gotenberg.NewDocumentFromPath("document2.docx", "/path/to/file")
+req := gotenberg.NewOfficeRequest(doc, doc2)
+dest := "result.pdf"
+c.Store(req, dest)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewOfficeRequest("document.docx")
-    req.Landscape(true)
-    dest := "result.pdf"
-    c.Store(req, dest)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
+req := gotenberg.NewOfficeRequest(doc)
+req.Landscape(true)
+dest := "result.pdf"
+c.Store(req, dest)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewMergeRequest("file.pdf", "file2.pdf")
-    dest := "result.pdf"
-    c.Store(req, dest)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+pdf, _ := gotenberg.NewDocumentFromPath("file.pdf", "/path/to/file")
+pdf2, _ := gotenberg.NewDocumentFromPath("file2.pdf", "/path/to/file")
+req := gotenberg.NewMergeRequest(pdf, pdf2)
+dest := "result.pdf"
+c.Store(req, dest)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewHTMLRequest("index.html")
-    req.WaitTimeout(2.5)
-    resp, _ := c.Post(req)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
+req := gotenberg.NewHTMLRequest(index)
+req.WaitTimeout(2.5)
+resp, _ := c.Post(req)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewHTMLRequest("index.html")
-    req.WebhookURL("http://myapp.com/webhook/")
-    resp, _ := c.Post(req)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
+req := gotenberg.NewHTMLRequest(index)
+req.WebhookURL("http://myapp.com/webhook/")
+resp, _ := c.Post(req)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewHTMLRequest("index.html")
-    req.WebhookURL("http://myapp.com/webhook/")
-    req.WebhookURLTimeout(2.5)
-    resp, _ := c.Post(req)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
+req := gotenberg.NewHTMLRequest(index)
+req.WebhookURL("http://myapp.com/webhook/")
+req.WebhookURLTimeout(2.5)
+resp, _ := c.Post(req)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewHTMLRequest("index.html")
-    req.WebhookURL("http://myapp.com/webhook/")
-    req.AddWebhookURLHTTPHeader("Your-Header", "Foo")
-    resp, _ := c.Post(req)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
+req := gotenberg.NewHTMLRequest(index)
+req.WebhookURL("http://myapp.com/webhook/")
+req.AddWebhookURLHTTPHeader("Your-Header", "Foo")
+resp, _ := c.Post(req)
 

Go

-
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
-func main() {
-    c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-    req, _ := gotenberg.NewHTMLRequest("index.html")
-    req.ResultFilename("foo.pdf")
-    resp, _ := c.Post(req)
-}
+c := &gotenberg.Client{Hostname: "http://localhost:3000"}
+index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
+req := gotenberg.NewHTMLRequest(index)
+req.ResultFilename("foo.pdf")
+resp, _ := c.Post(req)
 

Go -
import "github.com/thecodingmachine/gotenberg-go-client/v6"
+
import "github.com/thecodingmachine/gotenberg-go-client/v7"
 
 c := &gotenberg.Client{Hostname: "http://localhost:3000"}
 doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")