diff --git a/Makefile b/Makefile
index 2430b5c0..5a0d93fb 100644
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@ DOCKER_PASSWORD=
DOCKER_REGISTRY=thecodingmachine
GOTENBERG_USER_GID=1001
GOTENBERG_USER_UID=1001
-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
@@ -17,6 +17,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.
@@ -57,7 +58,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_REGISTRY)/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/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/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/docs/index.html b/docs/index.html
index 215116e8..4d130685 100755
--- a/docs/index.html
+++ b/docs/index.html
@@ -280,6 +280,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
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)
+}