mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-17 20:52:14 +01:00
huge refactoring
This commit is contained in:
59
app/app.go
Normal file
59
app/app.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gulien/gotenberg/app/config"
|
||||||
|
"github.com/gulien/gotenberg/app/handlers"
|
||||||
|
"github.com/gulien/gotenberg/app/handlers/converter/process"
|
||||||
|
"github.com/gulien/gotenberg/app/logger"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
version string
|
||||||
|
config *config.AppConfig
|
||||||
|
Server *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApp(version string) (*App, error) {
|
||||||
|
c, err := config.NewAppConfig()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
return nil, &appConfigError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
a := &App{}
|
||||||
|
a.version = version
|
||||||
|
a.config = c
|
||||||
|
|
||||||
|
// defines our application logging.
|
||||||
|
logger.SetLevel(a.config.Logs.Level)
|
||||||
|
logger.SetFormatter(a.config.Logs.Formatter)
|
||||||
|
|
||||||
|
// defines our application router.
|
||||||
|
r := mux.NewRouter()
|
||||||
|
r.Handle("/", handlers.GetHandlersChain())
|
||||||
|
|
||||||
|
a.Server = &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%s", a.config.Port),
|
||||||
|
// good practice to set timeouts to avoid Slowloris attacks.
|
||||||
|
WriteTimeout: time.Second * 15,
|
||||||
|
ReadTimeout: time.Second * 15,
|
||||||
|
IdleTimeout: time.Second * 60,
|
||||||
|
Handler: r,
|
||||||
|
}
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Run() error {
|
||||||
|
process.Load(a.config.CommandsConfig)
|
||||||
|
logger.Infof("Gotenberg %s", a.version)
|
||||||
|
logger.Infof("Application is starting on %s", a.Server.Addr)
|
||||||
|
|
||||||
|
return a.Server.ListenAndServe()
|
||||||
|
}
|
||||||
170
app/config/config.go
Normal file
170
app/config/config.go
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/gulien/gotenberg/app/logger"
|
||||||
|
|
||||||
|
"github.com/satori/go.uuid"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
AppConfig struct {
|
||||||
|
Port string
|
||||||
|
Logs struct {
|
||||||
|
Level logrus.Level
|
||||||
|
Formatter logrus.Formatter
|
||||||
|
}
|
||||||
|
CommandsConfig *CommandsConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandsConfig struct {
|
||||||
|
HTML *CommandConfig
|
||||||
|
Office *CommandConfig
|
||||||
|
Merge *CommandConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandConfig struct {
|
||||||
|
Timeout int
|
||||||
|
Template *template.Template
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewAppConfig() (*AppConfig, error) {
|
||||||
|
fileConfig, err := loadFileConfig()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
return nil, &fileConfigError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &AppConfig{}
|
||||||
|
c.Port = fileConfig.Port
|
||||||
|
c.Logs.Level = getLoggingLevelFromFileConfig(fileConfig)
|
||||||
|
c.Logs.Formatter = getLoggingFormatterFromFileConfig(fileConfig)
|
||||||
|
|
||||||
|
if c.Logs.Level == 999 {
|
||||||
|
return nil, &wrongLoggingLevelError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Logs.Formatter == nil {
|
||||||
|
return nil, &wrongLoggingFormatError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.CommandsConfig = &CommandsConfig{}
|
||||||
|
c.CommandsConfig.HTML = &CommandConfig{}
|
||||||
|
c.CommandsConfig.Office = &CommandConfig{}
|
||||||
|
c.CommandsConfig.Merge = &CommandConfig{}
|
||||||
|
c.CommandsConfig.HTML.Timeout = fileConfig.Commands.HTML.Timeout
|
||||||
|
c.CommandsConfig.Office.Timeout = fileConfig.Commands.Office.Timeout
|
||||||
|
c.CommandsConfig.Merge.Timeout = fileConfig.Commands.Merge.Timeout
|
||||||
|
|
||||||
|
tmplHTML, err := getCommandTemplate(fileConfig.Commands.HTML.Template)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
return nil, &wrongHTMLCommandTemplate{}
|
||||||
|
}
|
||||||
|
|
||||||
|
tmplOffice, err := getCommandTemplate(fileConfig.Commands.Office.Template)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
return nil, &wrongOfficeCommandTemplate{}
|
||||||
|
}
|
||||||
|
|
||||||
|
tmplMerge, err := getCommandTemplate(fileConfig.Commands.Merge.Template)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
return nil, &wrongMergeCommandTemplate{}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.CommandsConfig.HTML.Template = tmplHTML
|
||||||
|
c.CommandsConfig.Office.Template = tmplOffice
|
||||||
|
c.CommandsConfig.Merge.Template = tmplMerge
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileConfig struct {
|
||||||
|
Port string `yaml:"port"`
|
||||||
|
Logs struct {
|
||||||
|
Level string `yaml:"level"`
|
||||||
|
Format string `yaml:"format"`
|
||||||
|
} `yaml:"logs"`
|
||||||
|
Commands struct {
|
||||||
|
HTML struct {
|
||||||
|
Timeout int
|
||||||
|
Template string
|
||||||
|
} `yaml:"html"`
|
||||||
|
Office struct {
|
||||||
|
Timeout int `yaml:"timeout"`
|
||||||
|
Template string `yaml:"template"`
|
||||||
|
} `yaml:"office"`
|
||||||
|
Merge struct {
|
||||||
|
Timeout int `yaml:"timeout"`
|
||||||
|
Template string `yaml:"template"`
|
||||||
|
} `yaml:"merge"`
|
||||||
|
} `yaml:"commands"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// configurationFilePath is our default configuration file to parse.
|
||||||
|
const configurationFilePath = "gotenberg.yml"
|
||||||
|
|
||||||
|
func loadFileConfig() (*fileConfig, error) {
|
||||||
|
c := &fileConfig{}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(configurationFilePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
return nil, &readFileError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(data, &c); err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
return nil, &unmarshalError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var levels = map[string]logrus.Level{
|
||||||
|
"DEBUG": logrus.DebugLevel,
|
||||||
|
"INFO": logrus.InfoLevel,
|
||||||
|
"WARN": logrus.WarnLevel,
|
||||||
|
"ERROR": logrus.ErrorLevel,
|
||||||
|
"FATAL": logrus.FatalLevel,
|
||||||
|
"PANIC": logrus.PanicLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLoggingLevelFromFileConfig(c *fileConfig) logrus.Level {
|
||||||
|
l, ok := levels[c.Logs.Level]
|
||||||
|
if !ok {
|
||||||
|
return 999
|
||||||
|
}
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatters = map[string]logrus.Formatter{
|
||||||
|
"text": &logrus.TextFormatter{},
|
||||||
|
"json": &logrus.JSONFormatter{},
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLoggingFormatterFromFileConfig(c *fileConfig) logrus.Formatter {
|
||||||
|
f, ok := formatters[c.Logs.Format]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommandTemplate(command string) (*template.Template, error) {
|
||||||
|
t, err := template.New(uuid.NewV4().String()).Parse(command)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
49
app/config/errors.go
Normal file
49
app/config/errors.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type fileConfigError struct{}
|
||||||
|
|
||||||
|
func (e *fileConfigError) Error() string {
|
||||||
|
return "An error occured while trying to load the configuration file"
|
||||||
|
}
|
||||||
|
|
||||||
|
type wrongLoggingLevelError struct{}
|
||||||
|
|
||||||
|
func (e *wrongLoggingLevelError) Error() string {
|
||||||
|
return "Accepted values for logging level: DEBUG, INFO, WARN, ERROR, FATAL, PANIC"
|
||||||
|
}
|
||||||
|
|
||||||
|
type wrongLoggingFormatError struct{}
|
||||||
|
|
||||||
|
func (e *wrongLoggingFormatError) Error() string {
|
||||||
|
return "Accepted value for logging format: text, json"
|
||||||
|
}
|
||||||
|
|
||||||
|
type wrongHTMLCommandTemplate struct{}
|
||||||
|
|
||||||
|
func (e *wrongHTMLCommandTemplate) Error() string {
|
||||||
|
return "An error occured while trying to parse the HTML command's template"
|
||||||
|
}
|
||||||
|
|
||||||
|
type wrongOfficeCommandTemplate struct{}
|
||||||
|
|
||||||
|
func (e *wrongOfficeCommandTemplate) Error() string {
|
||||||
|
return "An error occured while trying to parse the Office command's template"
|
||||||
|
}
|
||||||
|
|
||||||
|
type wrongMergeCommandTemplate struct{}
|
||||||
|
|
||||||
|
func (e *wrongMergeCommandTemplate) Error() string {
|
||||||
|
return "An error occured while trying to parse the merge command's template"
|
||||||
|
}
|
||||||
|
|
||||||
|
type readFileError struct{}
|
||||||
|
|
||||||
|
func (e *readFileError) Error() string {
|
||||||
|
return "An error occured while trying to read the configuration file"
|
||||||
|
}
|
||||||
|
|
||||||
|
type unmarshalError struct{}
|
||||||
|
|
||||||
|
func (e *unmarshalError) Error() string {
|
||||||
|
return "An error occured while trying to decode the configuration file as YAML"
|
||||||
|
}
|
||||||
7
app/errors.go
Normal file
7
app/errors.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
type appConfigError struct{}
|
||||||
|
|
||||||
|
func (e *appConfigError) Error() string {
|
||||||
|
return "A fatal error occured while setting up the application"
|
||||||
|
}
|
||||||
51
app/handlers/context/context.go
Normal file
51
app/handlers/context/context.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gulien/gotenberg/app/handlers/converter"
|
||||||
|
|
||||||
|
ghttp "github.com/gulien/gotenberg/app/handlers/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type key uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
contentTypeKey key = iota
|
||||||
|
converterKey
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithContentType(r *http.Request, contentType ghttp.ContentType) *http.Request {
|
||||||
|
ctx := r.Context()
|
||||||
|
ctx = context.WithValue(ctx, contentTypeKey, contentType)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetContentType(r *http.Request) (ghttp.ContentType, error) {
|
||||||
|
ct, ok := r.Context().Value(contentTypeKey).(ghttp.ContentType)
|
||||||
|
if !ok {
|
||||||
|
return "", &contentTypeNotFoundError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ct, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithConverter(r *http.Request, converter *converter.Converter) *http.Request {
|
||||||
|
ctx := r.Context()
|
||||||
|
ctx = context.WithValue(ctx, converterKey, converter)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConverter(r *http.Request) (*converter.Converter, error) {
|
||||||
|
c, ok := r.Context().Value(converterKey).(*converter.Converter)
|
||||||
|
if !ok {
|
||||||
|
return nil, &converterNotFoundError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
13
app/handlers/context/errors.go
Normal file
13
app/handlers/context/errors.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
type contentTypeNotFoundError struct{}
|
||||||
|
|
||||||
|
func (e *contentTypeNotFoundError) Error() string {
|
||||||
|
return "The 'Content-Type' was not found in request context"
|
||||||
|
}
|
||||||
|
|
||||||
|
type converterNotFoundError struct{}
|
||||||
|
|
||||||
|
func (e *converterNotFoundError) Error() string {
|
||||||
|
return "The converter was not found in request context"
|
||||||
|
}
|
||||||
118
app/handlers/converter/converter.go
Normal file
118
app/handlers/converter/converter.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package converter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
gfile "github.com/gulien/gotenberg/app/handlers/converter/file"
|
||||||
|
"github.com/gulien/gotenberg/app/handlers/converter/process"
|
||||||
|
ghttp "github.com/gulien/gotenberg/app/handlers/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Converter struct {
|
||||||
|
files []*gfile.File
|
||||||
|
resultFilesPaths []string
|
||||||
|
FinalFilePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConverter(r *http.Request, contentType ghttp.ContentType) (*Converter, error) {
|
||||||
|
c := &Converter{}
|
||||||
|
|
||||||
|
switch contentType {
|
||||||
|
case ghttp.MultipartFormDataContentType:
|
||||||
|
err := r.ParseMultipartForm(32 << 20)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
formData := r.MultipartForm
|
||||||
|
files, ok := formData.File["files"]
|
||||||
|
if !ok {
|
||||||
|
// TODO
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range files {
|
||||||
|
file, err := files[i].Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
f, err := gfile.NewFile(file)
|
||||||
|
if err != nil {
|
||||||
|
// todo err
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.files = append(c.files, f)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
f, err := gfile.NewFile(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
// todo err
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.files = append(c.files, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Converter) Convert() error {
|
||||||
|
if len(c.files) == 0 {
|
||||||
|
return &noFileToConvertError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var filesPaths []string
|
||||||
|
for _, f := range c.files {
|
||||||
|
if f.Type != gfile.PDFType {
|
||||||
|
path, err := process.ExecConversion(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filesPaths = append(filesPaths, path)
|
||||||
|
} else {
|
||||||
|
filesPaths = append(filesPaths, f.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := process.ExecMerge(filesPaths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.resultFilesPaths = filesPaths
|
||||||
|
c.FinalFilePath = path
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Converter) Clear() error {
|
||||||
|
for _, f := range c.files {
|
||||||
|
err := os.Remove(f.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range c.resultFilesPaths {
|
||||||
|
err := os.Remove(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.FinalFilePath != "" {
|
||||||
|
err := os.Remove(c.FinalFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
7
app/handlers/converter/errors.go
Normal file
7
app/handlers/converter/errors.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package converter
|
||||||
|
|
||||||
|
type noFileToConvertError struct{}
|
||||||
|
|
||||||
|
func (e *noFileToConvertError) Error() string {
|
||||||
|
return "There is no file to convert"
|
||||||
|
}
|
||||||
120
app/handlers/converter/file/file.go
Normal file
120
app/handlers/converter/file/file.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
ghttp "github.com/gulien/gotenberg/app/handlers/http"
|
||||||
|
|
||||||
|
"github.com/satori/go.uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
File struct {
|
||||||
|
Type FileType
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
FileType string
|
||||||
|
|
||||||
|
FileExt string
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PDFType FileType = "PDF"
|
||||||
|
HTMLType FileType = "HTML"
|
||||||
|
OfficeType FileType = "Office"
|
||||||
|
|
||||||
|
PDFExt FileExt = ".pdf"
|
||||||
|
HTMLExt FileExt = ".html"
|
||||||
|
OfficeExt FileExt = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewFile(r io.Reader) (*File, error) {
|
||||||
|
f := &File{
|
||||||
|
Path: MakeFilePath(),
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(f.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// resets the read pointer.
|
||||||
|
file.Seek(0, 0)
|
||||||
|
|
||||||
|
t, err := findFileType(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Type = t
|
||||||
|
|
||||||
|
f, err = reworkFilePath(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeFilePath() string {
|
||||||
|
return fmt.Sprintf("./%s", uuid.NewV4().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var filesTypes = map[ghttp.ContentType]FileType{
|
||||||
|
ghttp.PDFContentType: PDFType,
|
||||||
|
ghttp.HTMLContentType: HTMLType,
|
||||||
|
ghttp.OctetStreamContentType: OfficeType,
|
||||||
|
ghttp.ZipContentType: OfficeType,
|
||||||
|
}
|
||||||
|
|
||||||
|
func findFileType(f *os.File) (FileType, error) {
|
||||||
|
ct, err := ghttp.SniffContentType(f)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
t, ok := filesTypes[ct]
|
||||||
|
if !ok {
|
||||||
|
// TODO error
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var filesExtensions = map[FileType]FileExt{
|
||||||
|
PDFType: PDFExt,
|
||||||
|
HTMLType: HTMLExt,
|
||||||
|
OfficeType: OfficeExt,
|
||||||
|
}
|
||||||
|
|
||||||
|
func reworkFilePath(f *File) (*File, error) {
|
||||||
|
ext, ok := filesExtensions[f.Type]
|
||||||
|
if !ok {
|
||||||
|
// TODO error
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ext != OfficeExt {
|
||||||
|
newPath := fmt.Sprintf("./%s%s", MakeFilePath(), ext)
|
||||||
|
|
||||||
|
err := os.Rename(f.Path, newPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Path = newPath
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
13
app/handlers/converter/process/errors.go
Normal file
13
app/handlers/converter/process/errors.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package process
|
||||||
|
|
||||||
|
type impossibleConversionError struct{}
|
||||||
|
|
||||||
|
func (e *impossibleConversionError) Error() string {
|
||||||
|
return "Impossible conversion"
|
||||||
|
}
|
||||||
|
|
||||||
|
type commandTimeoutError struct{}
|
||||||
|
|
||||||
|
func (e *commandTimeoutError) Error() string {
|
||||||
|
return "The command has reached timeout"
|
||||||
|
}
|
||||||
119
app/handlers/converter/process/process.go
Normal file
119
app/handlers/converter/process/process.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gulien/gotenberg/app/config"
|
||||||
|
gfile "github.com/gulien/gotenberg/app/handlers/converter/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandsConfig *config.CommandsConfig
|
||||||
|
|
||||||
|
func Load(config *config.CommandsConfig) {
|
||||||
|
commandsConfig = config
|
||||||
|
}
|
||||||
|
|
||||||
|
func Reset() {
|
||||||
|
commandsConfig = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type conversionData struct {
|
||||||
|
FilePath string
|
||||||
|
ResultFilePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecConversion(file *gfile.File) (string, error) {
|
||||||
|
cmdData := &conversionData{
|
||||||
|
FilePath: file.Path,
|
||||||
|
ResultFilePath: fmt.Sprintf("%s%s", gfile.MakeFilePath(), gfile.PDFExt),
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
cmdTemplate *template.Template
|
||||||
|
cmdTimeout int
|
||||||
|
)
|
||||||
|
|
||||||
|
switch file.Type {
|
||||||
|
case gfile.HTMLType:
|
||||||
|
cmdTemplate = commandsConfig.HTML.Template
|
||||||
|
cmdTimeout = commandsConfig.HTML.Timeout
|
||||||
|
break
|
||||||
|
case gfile.OfficeType:
|
||||||
|
cmdTemplate = commandsConfig.Office.Template
|
||||||
|
cmdTimeout = commandsConfig.Office.Timeout
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return "", &impossibleConversionError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var data bytes.Buffer
|
||||||
|
if err := cmdTemplate.Execute(&data, cmdData); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := execCommand(data.String(), cmdTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmdData.ResultFilePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mergeData struct {
|
||||||
|
FilesPaths []string
|
||||||
|
ResultFilePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecMerge(filesPaths []string) (string, error) {
|
||||||
|
cmdData := &mergeData{
|
||||||
|
FilesPaths: filesPaths,
|
||||||
|
ResultFilePath: fmt.Sprintf("%s%s", gfile.MakeFilePath(), gfile.PDFExt),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdTemplate := commandsConfig.Merge.Template
|
||||||
|
cmdTimeout := commandsConfig.Merge.Timeout
|
||||||
|
|
||||||
|
var data bytes.Buffer
|
||||||
|
if err := cmdTemplate.Execute(&data, cmdData); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := execCommand(data.String(), cmdTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmdData.ResultFilePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func execCommand(command string, timeout int) error {
|
||||||
|
// Wait for the process to finish or kill it after a timeout.
|
||||||
|
cmd := exec.Command("/bin/sh", "-c", command)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- cmd.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Duration(timeout) * time.Second):
|
||||||
|
if err := cmd.Process.Kill(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &commandTimeoutError{}
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
7
app/handlers/errors.go
Normal file
7
app/handlers/errors.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
type requestHasNoContentError struct{}
|
||||||
|
|
||||||
|
func (e *requestHasNoContentError) Error() string {
|
||||||
|
return "Request has not content"
|
||||||
|
}
|
||||||
126
app/handlers/handlers.go
Normal file
126
app/handlers/handlers.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gulien/gotenberg/app/handlers/context"
|
||||||
|
"github.com/gulien/gotenberg/app/handlers/converter"
|
||||||
|
ghttp "github.com/gulien/gotenberg/app/handlers/http"
|
||||||
|
"github.com/gulien/gotenberg/app/logger"
|
||||||
|
|
||||||
|
"github.com/justinas/alice"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetHandlersChain() http.Handler {
|
||||||
|
return alice.New(enforceContentLengthHandler, enforceContentTypeHandler, convertHandler, serveHandler).ThenFunc(clearHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// enforeContentLengthHandler checks if the request has content.
|
||||||
|
func enforceContentLengthHandler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.ContentLength == 0 {
|
||||||
|
e := &requestHasNoContentError{}
|
||||||
|
http.Error(w, e.Error(), http.StatusBadRequest)
|
||||||
|
logger.Error(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// enforceContentTypeHandler checks if the "Content-Type" entry
|
||||||
|
// from the request's header matches one of the allowed content types.
|
||||||
|
func enforceContentTypeHandler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ct, err := ghttp.FindAuthorizedContentType(r.Header)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusUnsupportedMediaType)
|
||||||
|
logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r = context.WithContentType(r, ct)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertHandler is in charge of converting the file(s) from the request to PDF.
|
||||||
|
func convertHandler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ct, err := context.GetContentType(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := converter.NewConverter(r, ct)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.Convert()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r = context.WithConverter(r, c)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveHandler simply serves the created PDF.
|
||||||
|
func serveHandler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
c, err := context.GetConverter(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := os.Open(c.FinalFilePath)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
resultFileInfo, err := reader.Stat()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", resultFileInfo.Name()))
|
||||||
|
w.Header().Set("Content-Type", "application/pdf")
|
||||||
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", resultFileInfo.Size()))
|
||||||
|
io.Copy(w, reader)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearHandler removes all files created during the conversion.
|
||||||
|
func clearHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
c, err := context.GetConverter(r)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Clear(); err != nil {
|
||||||
|
logger.Warn(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/handlers/http/errors.go
Normal file
15
app/handlers/http/errors.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type notAuthorizedContentTypeError struct{}
|
||||||
|
|
||||||
|
func (e *notAuthorizedContentTypeError) Error() string {
|
||||||
|
return fmt.Sprintf("Accepted values for 'Content-Type': %s, %s, %s, %s", PDFContentType, HTMLContentType, OctetStreamContentType, MultipartFormDataContentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
type notAuthorizedFileContentTypeError struct{}
|
||||||
|
|
||||||
|
func (e *notAuthorizedFileContentTypeError) Error() string {
|
||||||
|
return fmt.Sprintf("Unable to detect a file 'Content-Type'")
|
||||||
|
}
|
||||||
60
app/handlers/http/http.go
Normal file
60
app/handlers/http/http.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContentType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PDFContentType ContentType = "application/pdf"
|
||||||
|
HTMLContentType ContentType = "text/html"
|
||||||
|
OctetStreamContentType ContentType = "application/octet-stream"
|
||||||
|
ZipContentType ContentType = "application/zip"
|
||||||
|
MultipartFormDataContentType ContentType = "multipart/form-data"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FindAuthorizedContentType(h http.Header) (ContentType, error) {
|
||||||
|
ct := findContentType(h.Get("Content-Type"), HTMLContentType, OctetStreamContentType, MultipartFormDataContentType)
|
||||||
|
if ct == "" {
|
||||||
|
return "", ¬AuthorizedContentTypeError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ct, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SniffContentType(f *os.File) (ContentType, error) {
|
||||||
|
// only the first 512 bytes are used to sniff the content type.
|
||||||
|
buffer := make([]byte, 512)
|
||||||
|
n, err := f.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// resets the read pointer.
|
||||||
|
f.Seek(0, 0)
|
||||||
|
|
||||||
|
// using n if size of buffer < 512 bytes.
|
||||||
|
ct := findContentType(http.DetectContentType(buffer[:n]), PDFContentType, HTMLContentType, OctetStreamContentType, ZipContentType)
|
||||||
|
if ct == "" {
|
||||||
|
return "", ¬AuthorizedFileContentTypeError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ct, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findContentType(contentType string, contentTypes ...ContentType) ContentType {
|
||||||
|
for _, ct := range contentTypes {
|
||||||
|
if i := strings.IndexRune(contentType, ';'); i != -1 {
|
||||||
|
contentType = contentType[0:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentType == string(ct) {
|
||||||
|
return ct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
65
app/logger/logger.go
Normal file
65
app/logger/logger.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Package logger
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logger struct {
|
||||||
|
logger *logrus.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var log *logger = newLogger()
|
||||||
|
|
||||||
|
func newLogger() *logger {
|
||||||
|
l := &logger{
|
||||||
|
logger: logrus.New(),
|
||||||
|
}
|
||||||
|
|
||||||
|
l.logger.Out = os.Stdout
|
||||||
|
l.logger.Level = logrus.InfoLevel
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetLevel(level logrus.Level) {
|
||||||
|
log.logger.SetLevel(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetFormatter(formatter logrus.Formatter) {
|
||||||
|
log.logger.Formatter = formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debug(message string) {
|
||||||
|
log.logger.Debug(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debugf(format string, args ...interface{}) {
|
||||||
|
log.logger.Debugf(format, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Info(message string) {
|
||||||
|
log.logger.Info(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Infof(format string, args ...interface{}) {
|
||||||
|
log.logger.Infof(format, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warn(message string) {
|
||||||
|
log.logger.Warn(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(err error) {
|
||||||
|
log.logger.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fatal(err error) {
|
||||||
|
log.logger.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Panic(err error) {
|
||||||
|
log.logger.Panic(err.Error())
|
||||||
|
}
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
// Package config implements a solution for parsing a configuration file ("gotenberg.yml")
|
|
||||||
// and populating an instance of Config which will be used accross the application.
|
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config represents the data provided by a YAML file.
|
|
||||||
type Config struct {
|
|
||||||
// Port the port the application will listen to.
|
|
||||||
Port string `yaml:"port"`
|
|
||||||
// LogLevel the log level used by the logger of the application.
|
|
||||||
LogLevel string `yaml:"logLevel"`
|
|
||||||
// Commands the commands' templates from the configuration file.
|
|
||||||
Commands struct {
|
|
||||||
// HTMLtoPDF the command's template to convert an HTML file to a PDF file.
|
|
||||||
HTMLtoPDF string `yaml:"HTMLtoPDF"`
|
|
||||||
// WordToPDF the command's template to convert a Word file to a PDF file.
|
|
||||||
WordToPDF string `yaml:"WordToPDF"`
|
|
||||||
// MergePDF the command's template to merge many PDF files into one final PDF file.
|
|
||||||
MergePDF string `yaml:"MergePDF"`
|
|
||||||
} `yaml:"commands"`
|
|
||||||
// Templates gathers all instances of Template which will be created from previous
|
|
||||||
// Commands block.
|
|
||||||
Templates struct {
|
|
||||||
// HTMLtoPDF the instance of template created from Commands.HTMLtoPDF.
|
|
||||||
HTMLtoPDF *template.Template
|
|
||||||
// WordToPDF the instance of template created from Commands.HTMLtoPDF.
|
|
||||||
WordToPDF *template.Template
|
|
||||||
// MergePDF the instance of template created from Commands.HTMLtoPDF.
|
|
||||||
MergePDF *template.Template
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppConfig is the configuration instance used accross the application.
|
|
||||||
var AppConfig *Config
|
|
||||||
|
|
||||||
// configurationFilePath is our default configuration file to parse.
|
|
||||||
const configurationFilePath = "gotenberg.yml"
|
|
||||||
|
|
||||||
// MakeConfig instantiates our configuration by parsing a YAML file.
|
|
||||||
func MakeConfig() error {
|
|
||||||
AppConfig = &Config{}
|
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(configurationFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := yaml.Unmarshal(data, &AppConfig); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl, err := template.New("HTMLToPDF").Parse(AppConfig.Commands.HTMLtoPDF)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Unable to parse the HTML to PDF command: %s", err)
|
|
||||||
}
|
|
||||||
AppConfig.Templates.HTMLtoPDF = tmpl
|
|
||||||
|
|
||||||
tmpl, err = template.New("WordToPDF").Parse(AppConfig.Commands.WordToPDF)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Unable to parse the Word to PDF command: %s", err)
|
|
||||||
}
|
|
||||||
AppConfig.Templates.WordToPDF = tmpl
|
|
||||||
|
|
||||||
tmpl, err = template.New("MergePDF").Parse(AppConfig.Commands.MergePDF)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Unable to parse the merge PDF command: %s", err)
|
|
||||||
}
|
|
||||||
AppConfig.Templates.MergePDF = tmpl
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
// Package context implements a solution for accessing and setting a request's context values.
|
|
||||||
package context
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/gulien/gotenberg/converters"
|
|
||||||
"github.com/gulien/gotenberg/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// transactionIDCtxKeyType is a basic type for transactionIDCtxKey.
|
|
||||||
type transactionIDCtxKeyType string
|
|
||||||
|
|
||||||
// transactionIDCtxKey is the transactionID accessing key.
|
|
||||||
const transactionIDCtxKey transactionIDCtxKeyType = "transactionID"
|
|
||||||
|
|
||||||
// WithTransactionID populates a context ctx with a transaction ID v.
|
|
||||||
func WithTransactionID(ctx context.Context, v string) context.Context {
|
|
||||||
return context.WithValue(ctx, transactionIDCtxKey, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTransactionID returns the transaction ID from the context ctx.
|
|
||||||
func GetTransactionID(ctx context.Context) string {
|
|
||||||
v, ok := ctx.Value(transactionIDCtxKey).(string)
|
|
||||||
if !ok {
|
|
||||||
logger.Log.Warn("Unable to retrieve the transaction ID from request context")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// contentTypeCtxKeyType is a basic type for contentTypeCtxKey.
|
|
||||||
type contentTypeCtxKeyType string
|
|
||||||
|
|
||||||
// contentTypeCtxKey is the contentType accessing key.
|
|
||||||
const contentTypeCtxKey contentTypeCtxKeyType = "contentType"
|
|
||||||
|
|
||||||
// WithContentType populates a context ctx with a content type v.
|
|
||||||
func WithContentType(ctx context.Context, v string) context.Context {
|
|
||||||
return context.WithValue(ctx, contentTypeCtxKey, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContentType returns the content type from the context ctx.
|
|
||||||
func GetContentType(ctx context.Context) string {
|
|
||||||
v, ok := ctx.Value(contentTypeCtxKey).(string)
|
|
||||||
if !ok {
|
|
||||||
logger.Log.Error("Unable to retrieve the content type from request context")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// resultFilePathCtxKeyType is a basic type for resultFilePathCtxKey.
|
|
||||||
type resultFilePathCtxKeyType string
|
|
||||||
|
|
||||||
// resultFilePathCtxKey is the resultFilePath accessing key.
|
|
||||||
const resultFilePathCtxKey resultFilePathCtxKeyType = "resultFilePath"
|
|
||||||
|
|
||||||
// WithResultFilePath populates a context ctx with a result file path v.
|
|
||||||
func WithResultFilePath(ctx context.Context, v string) context.Context {
|
|
||||||
return context.WithValue(ctx, resultFilePathCtxKey, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetResultFilePath returns the result file path from the context ctx.
|
|
||||||
func GetResultFilePath(ctx context.Context) string {
|
|
||||||
v, ok := ctx.Value(resultFilePathCtxKey).(string)
|
|
||||||
if !ok {
|
|
||||||
logger.Log.Error("Unable to retrieve the result file path from request context")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// converterCtxKeyType is a basic type for converterCtxKey.
|
|
||||||
type converterCtxKeyType string
|
|
||||||
|
|
||||||
// converterCtxKey is the converter accessing key.
|
|
||||||
const converterCtxKey contentTypeCtxKeyType = "converter"
|
|
||||||
|
|
||||||
// WithConverter populates a context ctx with a converter v.
|
|
||||||
func WithConverter(ctx context.Context, v converters.Converter) context.Context {
|
|
||||||
return context.WithValue(ctx, converterCtxKey, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConverter returns the converter from the context ctx.
|
|
||||||
func GetConverter(ctx context.Context) converters.Converter {
|
|
||||||
v, ok := ctx.Value(converterCtxKey).(converters.Converter)
|
|
||||||
if !ok {
|
|
||||||
logger.Log.Warn("Unable to retrieve the converter from request context")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
// Package converters implements a solution for reading files from a request
|
|
||||||
// and converting them to PDF.
|
|
||||||
package converters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/gulien/gotenberg/config"
|
|
||||||
"github.com/gulien/gotenberg/helpers"
|
|
||||||
|
|
||||||
"github.com/satori/go.uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// Converter is the interface used in our convertHandler middleware.
|
|
||||||
// Indeed, we don't want to know in this middleware which converter is actually used.
|
|
||||||
Converter interface {
|
|
||||||
// Convert should returns the file path of the PDF created by the converter.
|
|
||||||
Convert() (string, error)
|
|
||||||
// Clear should removes all files used by the converter.
|
|
||||||
Clear() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// DirectConverter allows us to convert a file to PDF.
|
|
||||||
DirectConverter struct {
|
|
||||||
// contentType is the content type of the file to convert.
|
|
||||||
contentType string
|
|
||||||
// filePath is the path of the file to convert.
|
|
||||||
filePath string
|
|
||||||
// resultFilePath is the path of the PDF created by
|
|
||||||
// the conversion.
|
|
||||||
resultFilePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// MultipartFormDataConverter contains an array of
|
|
||||||
// DirectConverter instances, each one having to convert a file to PDF.
|
|
||||||
MultipartFormDataConverter struct {
|
|
||||||
// converters contains all DirectConverter instances which will be used
|
|
||||||
// to convert files to PDF.
|
|
||||||
converters []*DirectConverter
|
|
||||||
// resultFilePath is the path of the PDF file created by the merge
|
|
||||||
// of all PDF files created by the DirectConverter instances.
|
|
||||||
resultFilePath string
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewConverter instantiates a converter according to the content type of the request.
|
|
||||||
func NewConverter(contentType string, r *http.Request) (Converter, error) {
|
|
||||||
switch contentType {
|
|
||||||
case "multipart/form-data":
|
|
||||||
return newMultipartFromDataConverter(contentType, r)
|
|
||||||
default:
|
|
||||||
return newDirectConverter(contentType, r.Body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConverterUnprocessableEntityError is a custom error which is throwed when
|
|
||||||
// a file's content type does not match with one of the allowed content types.
|
|
||||||
type ConverterUnprocessableEntityError struct {
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error is the implementation of the Error function from the error interface.
|
|
||||||
func (e *ConverterUnprocessableEntityError) Error() string {
|
|
||||||
return e.message
|
|
||||||
}
|
|
||||||
|
|
||||||
// filesExtensions associates all allowed content types with their file extension.
|
|
||||||
var filesExtensions = map[string]string{
|
|
||||||
"application/pdf": ".pdf",
|
|
||||||
"text/html": ".html",
|
|
||||||
"application/octet-stream": ".doc",
|
|
||||||
"application/msword": ".doc",
|
|
||||||
"application/zip": ".docx",
|
|
||||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
|
||||||
}
|
|
||||||
|
|
||||||
// newDirectConverter instantiates a DirectConverter.
|
|
||||||
func newDirectConverter(contentType string, r io.Reader) (*DirectConverter, error) {
|
|
||||||
fileExtension, ok := filesExtensions[contentType]
|
|
||||||
if !ok {
|
|
||||||
return nil, &ConverterUnprocessableEntityError{message: "No file extension found"}
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath := fmt.Sprintf("./%s%s", uuid.NewV4().String(), fileExtension)
|
|
||||||
|
|
||||||
file, err := os.Create(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(file, r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// resets the read pointer.
|
|
||||||
file.Seek(0, 0)
|
|
||||||
|
|
||||||
contentType, err = helpers.DetectFileContentType(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &ConverterUnprocessableEntityError{message: fmt.Sprintf("An error occured while trying to detect the content type of a file: %s", err.Error())}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &DirectConverter{contentType: contentType, filePath: filePath}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConversionCommandData will be applied to the data-driven command's template
|
|
||||||
// which will convert a file to PDF.
|
|
||||||
type ConversionCommandData struct {
|
|
||||||
// FilePath is the path of the file to convert to PDF.
|
|
||||||
FilePath string
|
|
||||||
// The path of the PDF file created by the considered command.
|
|
||||||
ResultFilePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert converts a file to PDF.
|
|
||||||
func (c *DirectConverter) Convert() (string, error) {
|
|
||||||
var cmdTemplate *template.Template
|
|
||||||
|
|
||||||
switch c.contentType {
|
|
||||||
case "text/html":
|
|
||||||
cmdTemplate = config.AppConfig.Templates.HTMLtoPDF
|
|
||||||
break
|
|
||||||
case "application/octet-stream", "application/msword", "application/zip", "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
||||||
cmdTemplate = config.AppConfig.Templates.WordToPDF
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
// case "application/pdf".
|
|
||||||
cmdTemplate = nil
|
|
||||||
c.resultFilePath = c.filePath
|
|
||||||
|
|
||||||
return c.resultFilePath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c.resultFilePath = fmt.Sprintf("./%s.pdf", uuid.NewV4().String())
|
|
||||||
|
|
||||||
cmdData := &ConversionCommandData{
|
|
||||||
FilePath: c.filePath,
|
|
||||||
ResultFilePath: c.resultFilePath,
|
|
||||||
}
|
|
||||||
|
|
||||||
var data bytes.Buffer
|
|
||||||
if err := cmdTemplate.Execute(&data, cmdData); err != nil {
|
|
||||||
return "", fmt.Errorf("An error occured while executing a template: %s", err)
|
|
||||||
}
|
|
||||||
cmd := data.String()
|
|
||||||
|
|
||||||
e := exec.Command("/bin/sh", "-c", cmd)
|
|
||||||
if err := e.Run(); err != nil {
|
|
||||||
return "", fmt.Errorf("An error occured while executing the command %s: %s", cmd, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.resultFilePath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear removes all files used by an instance of DirectConverter.
|
|
||||||
func (c *DirectConverter) Clear() error {
|
|
||||||
if err := os.Remove(c.filePath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if "application/pdf" content type, the file path is the same
|
|
||||||
// as the result file path.
|
|
||||||
if c.contentType != "application/pdf" {
|
|
||||||
if err := os.Remove(c.resultFilePath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newMultipartFromDataConverter instantiates a MultipartFormDataConverter.
|
|
||||||
func newMultipartFromDataConverter(contentType string, r *http.Request) (*MultipartFormDataConverter, error) {
|
|
||||||
err := r.ParseMultipartForm(32 << 20)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
formData := r.MultipartForm
|
|
||||||
files := formData.File["files"]
|
|
||||||
c := &MultipartFormDataConverter{}
|
|
||||||
|
|
||||||
for i := range files {
|
|
||||||
file, err := files[i].Open()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
contentType, err := helpers.DetectMultipartFileContentType(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
d, err := newDirectConverter(contentType, file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.converters = append(c.converters, d)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeCommandData will be applied to the data-driven command's template
|
|
||||||
// which will merge PDF files.
|
|
||||||
type MergeCommandData struct {
|
|
||||||
// FilesPaths are the paths of the PDF files to merge.
|
|
||||||
FilesPaths []string
|
|
||||||
// The path of the PDF file created by the considered command.
|
|
||||||
ResultFilePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert converts all files from form data to PDF
|
|
||||||
// and then merges those resulting PDF into one final PDF.
|
|
||||||
func (c *MultipartFormDataConverter) Convert() (string, error) {
|
|
||||||
var filesPaths []string
|
|
||||||
|
|
||||||
for _, d := range c.converters {
|
|
||||||
filePath, err := d.Convert()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
filesPaths = append(filesPaths, filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.resultFilePath = fmt.Sprintf("./%s.pdf", uuid.NewV4().String())
|
|
||||||
cmdTemplate := config.AppConfig.Templates.MergePDF
|
|
||||||
cmdData := &MergeCommandData{
|
|
||||||
FilesPaths: filesPaths,
|
|
||||||
ResultFilePath: c.resultFilePath,
|
|
||||||
}
|
|
||||||
|
|
||||||
var data bytes.Buffer
|
|
||||||
if err := cmdTemplate.Execute(&data, cmdData); err != nil {
|
|
||||||
return "", fmt.Errorf("An error occured while executing a template: %s", err)
|
|
||||||
}
|
|
||||||
cmd := data.String()
|
|
||||||
|
|
||||||
e := exec.Command("/bin/sh", "-c", cmd)
|
|
||||||
if err := e.Run(); err != nil {
|
|
||||||
return "", fmt.Errorf("An error occured while executing the command %s: %s", cmd, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.resultFilePath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear removes all files used by an instance of MultipartFormDataConverter.
|
|
||||||
func (c *MultipartFormDataConverter) Clear() error {
|
|
||||||
for _, d := range c.converters {
|
|
||||||
if err := d.Clear(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Remove(c.resultFilePath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
# The port the application will listen to.
|
# The port the application will listen to.
|
||||||
port: 3000
|
port: 3000
|
||||||
|
|
||||||
# Defines the log level.
|
logs:
|
||||||
# Accepted values, in order of severity: DEBUG, INFO, WARN, ERROR, FATAL, PANIC.
|
# Accepted values, in order of severity: DEBUG, INFO, WARN, ERROR, FATAL, PANIC.
|
||||||
# Messages at and above the selected level will be logged.
|
# Messages at and above the selected level will be logged.
|
||||||
logLevel: "DEBUG"
|
level: "DEBUG"
|
||||||
|
|
||||||
|
# Accepted values: text, json.
|
||||||
|
# When a TTY is not attached, the output will be in the definied format.
|
||||||
|
format: "text"
|
||||||
|
|
||||||
# The commands' templates.
|
|
||||||
commands:
|
commands:
|
||||||
# Available data:
|
|
||||||
# - FilePath: the path of the file to convert to PDF.
|
html:
|
||||||
# - ResultFilePath: path of the PDF file created by the command.
|
timeout: 10
|
||||||
HTMLtoPDF: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
|
||||||
# Available data:
|
|
||||||
# - FilePath: the path of the file to convert to PDF.
|
office:
|
||||||
# - ResultFilePath: path of the PDF file created by the command.
|
timeout: 10
|
||||||
WordToPDF: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
|
||||||
# Available data:
|
|
||||||
# - FilePath: the paths of the PDF files to merge.
|
merge:
|
||||||
# - ResultFilePath: path of the PDF file created by the considered command.
|
timeout: 10
|
||||||
MergePDF: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
// Package helpers implements simple functions used across the application.
|
|
||||||
package helpers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// allowedContentTypes contains all allowed content types
|
|
||||||
// by the application.
|
|
||||||
var allowedContentTypes = []string{
|
|
||||||
"application/pdf",
|
|
||||||
"text/html",
|
|
||||||
"application/octet-stream",
|
|
||||||
"application/msword",
|
|
||||||
"application/zip",
|
|
||||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
||||||
"multipart/form-data",
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMatchingContentType parses a content type and tries to find a match
|
|
||||||
// with our allowed content types. If no match found, returns an empty string.
|
|
||||||
func GetMatchingContentType(contentType string) string {
|
|
||||||
for _, allowedContentType := range allowedContentTypes {
|
|
||||||
if i := strings.IndexRune(contentType, ';'); i != -1 {
|
|
||||||
contentType = contentType[0:i]
|
|
||||||
}
|
|
||||||
|
|
||||||
if contentType == allowedContentType {
|
|
||||||
return allowedContentType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// getMatchingContentTypeForFile is a simple wrapper of GetMatchingContentType
|
|
||||||
// function. It adds a condition for "multipart/form-data" content type, which
|
|
||||||
// is not an allowed content type for a file.
|
|
||||||
func getMatchingContentTypeForFile(contentType string) string {
|
|
||||||
matchingContentType := GetMatchingContentType(contentType)
|
|
||||||
if matchingContentType == "multipart/form-data" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchingContentType
|
|
||||||
}
|
|
||||||
|
|
||||||
// DetectMultipartFileContentType sniffs the content type of a file from
|
|
||||||
// form data.
|
|
||||||
func DetectMultipartFileContentType(file multipart.File) (string, error) {
|
|
||||||
// only the first 512 bytes are used to sniff the content type.
|
|
||||||
buffer := make([]byte, 512)
|
|
||||||
n, err := file.Read(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// resets the read pointer.
|
|
||||||
file.Seek(0, 0)
|
|
||||||
|
|
||||||
// using n if size of buffer < 512 bytes.
|
|
||||||
return getMatchingContentTypeForFile(http.DetectContentType(buffer[:n])), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DetectFileContentType sniffs the content type of a file.
|
|
||||||
func DetectFileContentType(file *os.File) (string, error) {
|
|
||||||
// only the first 512 bytes are used to sniff the content type.
|
|
||||||
buffer := make([]byte, 512)
|
|
||||||
n, err := file.Read(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// resets the read pointer.
|
|
||||||
file.Seek(0, 0)
|
|
||||||
|
|
||||||
// using n if size of buffer < 512 bytes.
|
|
||||||
return getMatchingContentTypeForFile(http.DetectContentType(buffer[:n])), nil
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
// Package logger implements a simple helper for displaying outputs to the user.
|
|
||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// newLogger instantiates a logrus logger.
|
|
||||||
func newLogger() *logrus.Logger {
|
|
||||||
l := logrus.New()
|
|
||||||
l.Out = os.Stdout
|
|
||||||
l.Level = logrus.InfoLevel
|
|
||||||
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log is the logger instance used accross the application.
|
|
||||||
var Log = newLogger()
|
|
||||||
|
|
||||||
// logLevels associates log levels from the configuration file with its equivalent
|
|
||||||
// in the logrus library.
|
|
||||||
var logLevels = map[string]logrus.Level{
|
|
||||||
"DEBUG": logrus.DebugLevel,
|
|
||||||
"INFO": logrus.InfoLevel,
|
|
||||||
"WARN": logrus.WarnLevel,
|
|
||||||
"ERROR": logrus.ErrorLevel,
|
|
||||||
"FATAL": logrus.FatalLevel,
|
|
||||||
"PANIC": logrus.PanicLevel,
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLevel changes our logger's log level according to
|
|
||||||
// the log level defined in the configuration file.
|
|
||||||
func SetLevel(logLevel string) {
|
|
||||||
lvl, ok := logLevels[logLevel]
|
|
||||||
if ok {
|
|
||||||
Log.Level = lvl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InfoR logs an information with a transaction field.
|
|
||||||
func InfoR(transactionID string, msg string) {
|
|
||||||
Log.WithFields(logrus.Fields{
|
|
||||||
"transaction": transactionID,
|
|
||||||
}).Info(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WarnR logs a warning with a transaction field.
|
|
||||||
func WarnR(transactionID string, msg string) {
|
|
||||||
Log.WithFields(logrus.Fields{
|
|
||||||
"transaction": transactionID,
|
|
||||||
}).Warn(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorR logs an error with transaction, err and code fields.
|
|
||||||
func ErrorR(transactionID string, err error, code int, msg string) {
|
|
||||||
Log.WithFields(logrus.Fields{
|
|
||||||
"transaction": transactionID,
|
|
||||||
"code": code,
|
|
||||||
"err": err.Error(),
|
|
||||||
}).Error(msg)
|
|
||||||
}
|
|
||||||
@@ -10,56 +10,35 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gulien/gotenberg/config"
|
"github.com/gulien/gotenberg/app"
|
||||||
"github.com/gulien/gotenberg/logger"
|
"github.com/gulien/gotenberg/app/handlers/converter/process"
|
||||||
"github.com/gulien/gotenberg/middlewares"
|
"github.com/gulien/gotenberg/app/logger"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// version will be set on build time.
|
// version will be set on build time.
|
||||||
var version = "master"
|
var version = "master"
|
||||||
|
|
||||||
// init sets up the application configuration.
|
|
||||||
func init() {
|
|
||||||
err := config.MakeConfig()
|
|
||||||
if err != nil {
|
|
||||||
logger.Log.Fatal(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.SetLevel(config.AppConfig.LogLevel)
|
|
||||||
logger.Log.Infof("Gotenberg %s", version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// main initializes the application and handles
|
// main initializes the application and handles
|
||||||
// graceful shutdown.
|
// graceful shutdown.
|
||||||
func main() {
|
func main() {
|
||||||
r := mux.NewRouter()
|
a, err := app.NewApp(version)
|
||||||
|
if err != nil {
|
||||||
// defines our entry point.
|
resetState()
|
||||||
r.Handle("/", middlewares.GetMiddlewaresChain()).Methods("POST")
|
logger.Fatal(err)
|
||||||
|
os.Exit(1)
|
||||||
srv := &http.Server{
|
|
||||||
Addr: fmt.Sprintf(":%s", config.AppConfig.Port),
|
|
||||||
// good practice to set timeouts to avoid Slowloris attacks.
|
|
||||||
WriteTimeout: time.Second * 15,
|
|
||||||
ReadTimeout: time.Second * 15,
|
|
||||||
IdleTimeout: time.Second * 60,
|
|
||||||
Handler: r,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// runs our server in a goroutine so that it doesn't block.
|
// runs our server in a goroutine so that it doesn't block.
|
||||||
go func() {
|
go func() {
|
||||||
logger.Log.Infof("Listening to port %s", config.AppConfig.Port)
|
if err = a.Run(); err != nil {
|
||||||
if err := srv.ListenAndServe(); err != nil {
|
resetState()
|
||||||
logger.Log.Panicf("Unrecoverable error: %s", err)
|
logger.Panic(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -79,8 +58,14 @@ func main() {
|
|||||||
|
|
||||||
// doesn't block if no connections, but will otherwise wait
|
// doesn't block if no connections, but will otherwise wait
|
||||||
// until the timeout deadline.
|
// until the timeout deadline.
|
||||||
srv.Shutdown(ctx)
|
a.Server.Shutdown(ctx)
|
||||||
|
resetState()
|
||||||
|
|
||||||
logger.Log.Info("Bye!")
|
logger.Info("Bye!")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resetState() {
|
||||||
|
logger.SetLevel(logrus.InfoLevel)
|
||||||
|
process.Reset()
|
||||||
|
}
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
/*
|
|
||||||
Package middlewares implements all handlers of the application.
|
|
||||||
|
|
||||||
Those handlers manage requests coming from the unique entry point (aka "/").
|
|
||||||
*/
|
|
||||||
package middlewares
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/gulien/gotenberg/context"
|
|
||||||
"github.com/gulien/gotenberg/converters"
|
|
||||||
"github.com/gulien/gotenberg/helpers"
|
|
||||||
"github.com/gulien/gotenberg/logger"
|
|
||||||
|
|
||||||
"github.com/justinas/alice"
|
|
||||||
"github.com/satori/go.uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetMiddlewaresChain builds and returns the chaining of handlers
|
|
||||||
// using the alice library.
|
|
||||||
func GetMiddlewaresChain() http.Handler {
|
|
||||||
return alice.New(loggingHandler, enforceContentLengthHandler, enforceContentTypeHandler, convertHandler, serveHandler).ThenFunc(clearHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// loggingHandler identifies the request.
|
|
||||||
func loggingHandler(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
transactionID := uuid.NewV4().String()
|
|
||||||
r = r.WithContext(context.WithTransactionID(r.Context(), transactionID))
|
|
||||||
logger.InfoR(context.GetTransactionID(r.Context()), fmt.Sprintf("Hello %s", r.RemoteAddr))
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// enforeContentLengthHandler checks if the request has content.
|
|
||||||
func enforceContentLengthHandler(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.ContentLength == 0 {
|
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
||||||
logger.ErrorR(context.GetTransactionID(r.Context()), fmt.Errorf("%s", http.StatusText(http.StatusBadRequest)), http.StatusBadRequest, "Request has no content")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// enforceContentTypeHandler checks if the "Content-Type" entry
|
|
||||||
// from the request's header matches one of the allowed content types.
|
|
||||||
func enforceContentTypeHandler(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
contentType := helpers.GetMatchingContentType(r.Header.Get("Content-Type"))
|
|
||||||
if contentType == "" {
|
|
||||||
http.Error(w, http.StatusText(http.StatusUnsupportedMediaType), http.StatusUnsupportedMediaType)
|
|
||||||
logger.ErrorR(context.GetTransactionID(r.Context()), fmt.Errorf("%s", http.StatusText(http.StatusUnsupportedMediaType)), http.StatusUnsupportedMediaType, "No matching content type found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r = r.WithContext(context.WithContentType(r.Context(), contentType))
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertHandler is in charge of converting the file(s) from the request to PDF.
|
|
||||||
func convertHandler(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
contentType := context.GetContentType(r.Context())
|
|
||||||
|
|
||||||
c, err := converters.NewConverter(contentType, r)
|
|
||||||
if err != nil {
|
|
||||||
if converterUnprocessableEntityError, ok := err.(*converters.ConverterUnprocessableEntityError); ok {
|
|
||||||
// a file from the request has a content type which does not match with one of the allowed
|
|
||||||
// content types.
|
|
||||||
http.Error(w, converterUnprocessableEntityError.Error(), http.StatusUnprocessableEntity)
|
|
||||||
logger.ErrorR(context.GetTransactionID(r.Context()), converterUnprocessableEntityError, http.StatusUnprocessableEntity, "A file has a content type which does not match with one of the allowed content types")
|
|
||||||
} else {
|
|
||||||
// something bad happened during the creation of the converter...
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
logger.ErrorR(context.GetTransactionID(r.Context()), err, http.StatusInternalServerError, "Something bad happened during the creation of the converter")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resultFilePath, err := c.Convert()
|
|
||||||
if err != nil {
|
|
||||||
// an error occured during conversion...
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
logger.ErrorR(context.GetTransactionID(r.Context()), err, http.StatusInternalServerError, "An error occured during conversion")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r = r.WithContext(context.WithConverter(r.Context(), c))
|
|
||||||
r = r.WithContext(context.WithResultFilePath(r.Context(), resultFilePath))
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// serveHandler simply serves the created PDF.
|
|
||||||
func serveHandler(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
resultFilePath := context.GetResultFilePath(r.Context())
|
|
||||||
|
|
||||||
reader, err := os.Open(resultFilePath)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
logger.ErrorR(context.GetTransactionID(r.Context()), err, http.StatusInternalServerError, fmt.Sprintf("An error occured while opening result file \"%s\"", resultFilePath))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
resultFileInfo, err := reader.Stat()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
logger.ErrorR(context.GetTransactionID(r.Context()), err, http.StatusInternalServerError, fmt.Sprintf("An error occured while retrieving info from result file \"%s\"", resultFilePath))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", resultFileInfo.Name()))
|
|
||||||
w.Header().Set("Content-Type", "application/pdf")
|
|
||||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", resultFileInfo.Size()))
|
|
||||||
io.Copy(w, reader)
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// clearHandler removes all files created during the conversion.
|
|
||||||
func clearHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
c := context.GetConverter(r.Context())
|
|
||||||
|
|
||||||
if err := c.Clear(); err != nil {
|
|
||||||
logger.WarnR(context.GetTransactionID(r.Context()), err.Error())
|
|
||||||
}
|
|
||||||
logger.InfoR(context.GetTransactionID(r.Context()), fmt.Sprintf("Bye %s", r.RemoteAddr))
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user