reworking errors + now using a working dir for our converter

This commit is contained in:
Julien Neuhart
2018-03-30 09:19:37 +02:00
parent e847c86baa
commit a882b7e5c5
16 changed files with 203 additions and 241 deletions

View File

@@ -22,8 +22,7 @@ type App struct {
func NewApp(version string) (*App, error) {
c, err := config.NewAppConfig()
if err != nil {
logger.Error(err)
return nil, &appConfigError{}
return nil, err
}
a := &App{}
@@ -52,8 +51,8 @@ func NewApp(version string) (*App, error) {
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)
logger.Infof("Starting Gotenberg version %s", a.version)
logger.Infof("Listening on port %s", a.config.Port)
return a.Server.ListenAndServe()
}

View File

@@ -4,9 +4,6 @@ import (
"io/ioutil"
"text/template"
"github.com/gulien/gotenberg/app/logger"
"github.com/satori/go.uuid"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
@@ -36,23 +33,25 @@ type (
func NewAppConfig() (*AppConfig, error) {
fileConfig, err := loadFileConfig()
if err != nil {
logger.Error(err)
return nil, &fileConfigError{}
return nil, err
}
c := &AppConfig{}
c.Port = fileConfig.Port
c.Logs.Level = getLoggingLevelFromFileConfig(fileConfig)
c.Logs.Formatter = getLoggingFormatterFromFileConfig(fileConfig)
if c.Logs.Level == 999 {
return nil, &wrongLoggingLevelError{}
lvl, err := getLoggingLevelFromFileConfig(fileConfig)
if err != nil {
return nil, err
}
if c.Logs.Formatter == nil {
return nil, &wrongLoggingFormatError{}
formatter, err := getLoggingFormatterFromFileConfig(fileConfig)
if err != nil {
return nil, err
}
c.Logs.Level = lvl
c.Logs.Formatter = formatter
c.CommandsConfig = &CommandsConfig{}
c.CommandsConfig.HTML = &CommandConfig{}
c.CommandsConfig.Office = &CommandConfig{}
@@ -61,22 +60,19 @@ func NewAppConfig() (*AppConfig, error) {
c.CommandsConfig.Office.Timeout = fileConfig.Commands.Office.Timeout
c.CommandsConfig.Merge.Timeout = fileConfig.Commands.Merge.Timeout
tmplHTML, err := getCommandTemplate(fileConfig.Commands.HTML.Template)
tmplHTML, err := getCommandTemplate(fileConfig.Commands.HTML.Template, "HTML")
if err != nil {
logger.Error(err)
return nil, &wrongHTMLCommandTemplate{}
return nil, err
}
tmplOffice, err := getCommandTemplate(fileConfig.Commands.Office.Template)
tmplOffice, err := getCommandTemplate(fileConfig.Commands.Office.Template, "Office")
if err != nil {
logger.Error(err)
return nil, &wrongOfficeCommandTemplate{}
return nil, err
}
tmplMerge, err := getCommandTemplate(fileConfig.Commands.Merge.Template)
tmplMerge, err := getCommandTemplate(fileConfig.Commands.Merge.Template, "Merge")
if err != nil {
logger.Error(err)
return nil, &wrongMergeCommandTemplate{}
return nil, err
}
c.CommandsConfig.HTML.Template = tmplHTML
@@ -116,13 +112,11 @@ func loadFileConfig() (*fileConfig, error) {
data, err := ioutil.ReadFile(configurationFilePath)
if err != nil {
logger.Error(err)
return nil, &readFileError{}
return nil, err
}
if err := yaml.Unmarshal(data, &c); err != nil {
logger.Error(err)
return nil, &unmarshalError{}
return nil, err
}
return c, nil
@@ -137,13 +131,19 @@ var levels = map[string]logrus.Level{
"PANIC": logrus.PanicLevel,
}
func getLoggingLevelFromFileConfig(c *fileConfig) logrus.Level {
l, ok := levels[c.Logs.Level]
if !ok {
return 999
type wrongLoggingLevelError struct{}
func (e *wrongLoggingLevelError) Error() string {
return "Accepted values for logging level: DEBUG, INFO, WARN, ERROR, FATAL, PANIC"
}
return l
func getLoggingLevelFromFileConfig(c *fileConfig) (logrus.Level, error) {
l, ok := levels[c.Logs.Level]
if !ok {
return 999, &wrongLoggingLevelError{}
}
return l, nil
}
var formatters = map[string]logrus.Formatter{
@@ -151,17 +151,23 @@ var formatters = map[string]logrus.Formatter{
"json": &logrus.JSONFormatter{},
}
func getLoggingFormatterFromFileConfig(c *fileConfig) logrus.Formatter {
type wrongLoggingFormatError struct{}
func (e *wrongLoggingFormatError) Error() string {
return "Accepted value for logging format: text, json"
}
func getLoggingFormatterFromFileConfig(c *fileConfig) (logrus.Formatter, error) {
f, ok := formatters[c.Logs.Format]
if !ok {
return nil
return nil, &wrongLoggingFormatError{}
}
return f
return f, nil
}
func getCommandTemplate(command string) (*template.Template, error) {
t, err := template.New(uuid.NewV4().String()).Parse(command)
func getCommandTemplate(command string, commandName string) (*template.Template, error) {
t, err := template.New(commandName).Parse(command)
if err != nil {
return nil, err
}

View File

@@ -1,49 +0,0 @@
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"
}

View File

@@ -1,7 +0,0 @@
package app
type appConfigError struct{}
func (e *appConfigError) Error() string {
return "A fatal error occured while setting up the application"
}

View File

@@ -14,6 +14,7 @@ type key uint32
const (
contentTypeKey key = iota
converterKey
resultFilePathKey
)
func WithContentType(r *http.Request, contentType ghttp.ContentType) *http.Request {
@@ -24,6 +25,12 @@ func WithContentType(r *http.Request, contentType ghttp.ContentType) *http.Reque
return r
}
type contentTypeNotFoundError struct{}
func (e *contentTypeNotFoundError) Error() string {
return "The 'Content-Type' was not found in request context"
}
func GetContentType(r *http.Request) (ghttp.ContentType, error) {
ct, ok := r.Context().Value(contentTypeKey).(ghttp.ContentType)
if !ok {
@@ -41,6 +48,12 @@ func WithConverter(r *http.Request, converter *converter.Converter) *http.Reques
return r
}
type converterNotFoundError struct{}
func (e *converterNotFoundError) Error() string {
return "The converter was not found in request context"
}
func GetConverter(r *http.Request) (*converter.Converter, error) {
c, ok := r.Context().Value(converterKey).(*converter.Converter)
if !ok {
@@ -49,3 +62,26 @@ func GetConverter(r *http.Request) (*converter.Converter, error) {
return c, nil
}
func WithResultFilePath(r *http.Request, resultFilePath string) *http.Request {
ctx := r.Context()
ctx = context.WithValue(ctx, resultFilePathKey, resultFilePath)
r = r.WithContext(ctx)
return r
}
type resultFilePathNotFoundError struct{}
func (e *resultFilePathNotFoundError) Error() string {
return "The resultFilePath was not found in request context"
}
func GetResultFilePath(r *http.Request) (string, error) {
path, ok := r.Context().Value(resultFilePathKey).(string)
if !ok {
return "", &resultFilePathNotFoundError{}
}
return path, nil
}

View File

@@ -1,13 +0,0 @@
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"
}

View File

@@ -1,22 +1,36 @@
package converter
import (
"fmt"
"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"
"github.com/satori/go.uuid"
)
type Converter struct {
files []*gfile.File
resultFilesPaths []string
FinalFilePath string
workingDir string
}
type NoFileToConvertError struct{}
func (e *NoFileToConvertError) Error() string {
return "There is no file to convert"
}
func NewConverter(r *http.Request, contentType ghttp.ContentType) (*Converter, error) {
c := &Converter{}
c := &Converter{
workingDir: fmt.Sprintf("./%s/", uuid.NewV4().String()),
}
if err := os.Mkdir(c.workingDir, 0666); err != nil {
return nil, err
}
switch contentType {
case ghttp.MultipartFormDataContentType:
@@ -28,7 +42,6 @@ func NewConverter(r *http.Request, contentType ghttp.ContentType) (*Converter, e
formData := r.MultipartForm
files, ok := formData.File["files"]
if !ok {
// TODO
return nil, nil
}
@@ -40,9 +53,8 @@ func NewConverter(r *http.Request, contentType ghttp.ContentType) (*Converter, e
defer file.Close()
f, err := gfile.NewFile(file)
f, err := gfile.NewFile(c.workingDir, file)
if err != nil {
// todo err
return nil, err
}
@@ -50,29 +62,28 @@ func NewConverter(r *http.Request, contentType ghttp.ContentType) (*Converter, e
}
break
default:
f, err := gfile.NewFile(r.Body)
f, err := gfile.NewFile(c.workingDir, r.Body)
if err != nil {
// todo err
return nil, err
}
c.files = append(c.files, f)
}
if len(c.files) == 0 {
return nil, &NoFileToConvertError{}
}
return c, nil
}
func (c *Converter) Convert() error {
if len(c.files) == 0 {
return &noFileToConvertError{}
}
func (c *Converter) Convert() (string, error) {
var filesPaths []string
for _, f := range c.files {
if f.Type != gfile.PDFType {
path, err := process.ExecConversion(f)
path, err := process.ExecConversion(c.workingDir, f)
if err != nil {
return err
return "", err
}
filesPaths = append(filesPaths, path)
@@ -81,38 +92,22 @@ func (c *Converter) Convert() error {
}
}
path, err := process.ExecMerge(filesPaths)
if err != nil {
return err
if len(filesPaths) == 0 {
return filesPaths[0], nil
}
c.resultFilesPaths = filesPaths
c.FinalFilePath = path
path, err := process.ExecMerge(c.workingDir, filesPaths)
if err != nil {
return "", err
}
return nil
return path, nil
}
func (c *Converter) Clear() error {
for _, f := range c.files {
err := os.Remove(f.Path)
if err != nil {
if err := os.RemoveAll(c.workingDir); 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
}

View File

@@ -1,7 +0,0 @@
package converter
type noFileToConvertError struct{}
func (e *noFileToConvertError) Error() string {
return "There is no file to convert"
}

View File

@@ -10,30 +10,22 @@ import (
"github.com/satori/go.uuid"
)
type (
File struct {
type File struct {
Type FileType
Path string
}
FileType string
FileExt string
)
type FileType uint32
const (
PDFType FileType = "PDF"
HTMLType FileType = "HTML"
OfficeType FileType = "Office"
PDFExt FileExt = ".pdf"
HTMLExt FileExt = ".html"
OfficeExt FileExt = ""
PDFType FileType = iota
HTMLType
OfficeType
)
func NewFile(r io.Reader) (*File, error) {
func NewFile(workingDir string, r io.Reader) (*File, error) {
f := &File{
Path: MakeFilePath(),
Path: MakeFilePath(workingDir),
}
file, err := os.Create(f.Path)
@@ -58,7 +50,7 @@ func NewFile(r io.Reader) (*File, error) {
f.Type = t
f, err = reworkFilePath(f)
f, err = reworkFilePath(workingDir, f)
if err != nil {
return nil, err
}
@@ -66,8 +58,8 @@ func NewFile(r io.Reader) (*File, error) {
return f, nil
}
func MakeFilePath() string {
return fmt.Sprintf("./%s", uuid.NewV4().String())
func MakeFilePath(workingDir string) string {
return fmt.Sprintf("%s%s", workingDir, uuid.NewV4().String())
}
var filesTypes = map[ghttp.ContentType]FileType{
@@ -77,36 +69,54 @@ var filesTypes = map[ghttp.ContentType]FileType{
ghttp.ZipContentType: OfficeType,
}
type fileTypeNotFound struct{}
func (e *fileTypeNotFound) Error() string {
return "The file type was not found for the given 'Content-Type'"
}
func findFileType(f *os.File) (FileType, error) {
ct, err := ghttp.SniffContentType(f)
if err != nil {
return "", err
return 999, err
}
t, ok := filesTypes[ct]
if !ok {
// TODO error
return "", nil
return 999, &fileTypeNotFound{}
}
return t, nil
}
type FileExt string
const (
PDFExt FileExt = ".pdf"
HTMLExt FileExt = ".html"
OfficeExt FileExt = ""
)
var filesExtensions = map[FileType]FileExt{
PDFType: PDFExt,
HTMLType: HTMLExt,
OfficeType: OfficeExt,
}
func reworkFilePath(f *File) (*File, error) {
type fileExtNotFound struct{}
func (e *fileExtNotFound) Error() string {
return "The file extension was not found for the given file type"
}
func reworkFilePath(workingDir string, f *File) (*File, error) {
ext, ok := filesExtensions[f.Type]
if !ok {
// TODO error
return nil, nil
return nil, &fileExtNotFound{}
}
if ext != OfficeExt {
newPath := fmt.Sprintf("./%s%s", MakeFilePath(), ext)
newPath := fmt.Sprintf("%s%s", MakeFilePath(workingDir), ext)
err := os.Rename(f.Path, newPath)
if err != nil {

View File

@@ -1,13 +0,0 @@
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"
}

View File

@@ -17,19 +17,21 @@ 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) {
type impossibleConversionError struct{}
func (e *impossibleConversionError) Error() string {
return "Impossible conversion"
}
func ExecConversion(workingDir string, file *gfile.File) (string, error) {
cmdData := &conversionData{
FilePath: file.Path,
ResultFilePath: fmt.Sprintf("%s%s", gfile.MakeFilePath(), gfile.PDFExt),
ResultFilePath: fmt.Sprintf("%s%s", gfile.MakeFilePath(workingDir), gfile.PDFExt),
}
var (
@@ -68,10 +70,10 @@ type mergeData struct {
ResultFilePath string
}
func ExecMerge(filesPaths []string) (string, error) {
func ExecMerge(workingDir string, filesPaths []string) (string, error) {
cmdData := &mergeData{
FilesPaths: filesPaths,
ResultFilePath: fmt.Sprintf("%s%s", gfile.MakeFilePath(), gfile.PDFExt),
ResultFilePath: fmt.Sprintf("%s%s", gfile.MakeFilePath(workingDir), gfile.PDFExt),
}
cmdTemplate := commandsConfig.Merge.Template
@@ -90,8 +92,13 @@ func ExecMerge(filesPaths []string) (string, error) {
return cmdData.ResultFilePath, nil
}
type commandTimeoutError struct{}
func (e *commandTimeoutError) Error() string {
return "The command has reached timeout"
}
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
@@ -102,6 +109,7 @@ func execCommand(command string, timeout int) error {
done <- cmd.Wait()
}()
// wait for the process to finish or kill it after a timeout.
select {
case <-time.After(time.Duration(timeout) * time.Second):
if err := cmd.Process.Kill(); err != nil {

View File

@@ -1,7 +0,0 @@
package handlers
type requestHasNoContentError struct{}
func (e *requestHasNoContentError) Error() string {
return "Request has not content"
}

View File

@@ -18,6 +18,12 @@ func GetHandlersChain() http.Handler {
return alice.New(enforceContentLengthHandler, enforceContentTypeHandler, convertHandler, serveHandler).ThenFunc(clearHandler)
}
type requestHasNoContentError struct{}
func (e *requestHasNoContentError) Error() string {
return "Request has not content"
}
// enforeContentLengthHandler checks if the request has content.
func enforceContentLengthHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -61,12 +67,17 @@ func convertHandler(next http.Handler) http.Handler {
c, err := converter.NewConverter(r, ct)
if err != nil {
if noFileToConvertError, ok := err.(*converter.NoFileToConvertError); ok {
http.Error(w, noFileToConvertError.Error(), http.StatusBadRequest)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
logger.Error(err)
return
}
err = c.Convert()
path, err := c.Convert()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error(err)
@@ -74,6 +85,7 @@ func convertHandler(next http.Handler) http.Handler {
}
r = context.WithConverter(r, c)
r = context.WithResultFilePath(r, path)
next.ServeHTTP(w, r)
})
@@ -82,13 +94,13 @@ func convertHandler(next http.Handler) http.Handler {
// 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)
path, err := context.GetResultFilePath(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error(err)
}
reader, err := os.Open(c.FinalFilePath)
reader, err := os.Open(path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error(err)

View File

@@ -1,15 +0,0 @@
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'")
}

View File

@@ -1,6 +1,7 @@
package http
import (
"fmt"
"net/http"
"os"
"strings"
@@ -16,6 +17,12 @@ const (
MultipartFormDataContentType ContentType = "multipart/form-data"
)
type notAuthorizedContentTypeError struct{}
func (e *notAuthorizedContentTypeError) Error() string {
return fmt.Sprintf("Accepted values for 'Content-Type': %s, %s, %s, %s", PDFContentType, HTMLContentType, OctetStreamContentType, MultipartFormDataContentType)
}
func FindAuthorizedContentType(h http.Header) (ContentType, error) {
ct := findContentType(h.Get("Content-Type"), HTMLContentType, OctetStreamContentType, MultipartFormDataContentType)
if ct == "" {
@@ -25,6 +32,12 @@ func FindAuthorizedContentType(h http.Header) (ContentType, error) {
return ct, nil
}
type notAuthorizedFileContentTypeError struct{}
func (e *notAuthorizedFileContentTypeError) Error() string {
return fmt.Sprintf("Unable to detect a file 'Content-Type'")
}
func SniffContentType(f *os.File) (ContentType, error) {
// only the first 512 bytes are used to sniff the content type.
buffer := make([]byte, 512)

12
main.go
View File

@@ -15,7 +15,6 @@ import (
"time"
"github.com/gulien/gotenberg/app"
"github.com/gulien/gotenberg/app/handlers/converter/process"
"github.com/gulien/gotenberg/app/logger"
"github.com/sirupsen/logrus"
@@ -29,7 +28,7 @@ var version = "master"
func main() {
a, err := app.NewApp(version)
if err != nil {
resetState()
logger.SetLevel(logrus.InfoLevel)
logger.Fatal(err)
os.Exit(1)
}
@@ -37,7 +36,7 @@ func main() {
// runs our server in a goroutine so that it doesn't block.
go func() {
if err = a.Run(); err != nil {
resetState()
logger.SetLevel(logrus.InfoLevel)
logger.Panic(err)
os.Exit(1)
}
@@ -59,13 +58,8 @@ func main() {
// doesn't block if no connections, but will otherwise wait
// until the timeout deadline.
a.Server.Shutdown(ctx)
resetState()
logger.SetLevel(logrus.InfoLevel)
logger.Info("Bye!")
os.Exit(0)
}
func resetState() {
logger.SetLevel(logrus.InfoLevel)
process.Reset()
}