Skip to content

Commit

Permalink
Added JS upload checks
Browse files Browse the repository at this point in the history
  • Loading branch information
svera committed Mar 1, 2024
1 parent b4b9f80 commit 8e1337b
Show file tree
Hide file tree
Showing 30 changed files with 192 additions and 93 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,5 @@ On first run, Coreander creates an admin user with the following credentials:
* `MIN_PASSWORD_LENGTH`: Minimum length acceptable for passwords. Defaults to 5.
* `WORDS_PER_MINUTE`: Defines a default words per minute reading speed that will be used for not logged-in users. Defaults to 250.
* `SESSION_TIMEOUT`: Specifies the maximum time a user session may last, in hours. Floating-point values are allowed. Defaults to 24 hours.
* `UPLOAD_MAX_SIZE`: Maximum document size allowed to be uploaded to the library, in megabytes. Set this to 0 to unlimit upload size. Defaults to 20 megabytes.
* `UPLOAD_DOCUMENT_MAX_SIZE`: Maximum document size allowed to be uploaded to the library, in megabytes. Set this to 0 to unlimit upload size. Defaults to 20 megabytes.

4 changes: 2 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type Config struct {
WordsPerMinute float64 `env:"WORDS_PER_MINUTE" env-default:"250"`
// SessionTimeout specifies the maximum time a user session may last in hours
SessionTimeout float64 `env:"SESSION_TIMEOUT" env-default:"24"`
// UploadMaxSize is the maximum document size allowed to be uploaded to the library, in megabytes.
// UploadDocumentMaxSize is the maximum document size allowed to be uploaded to the library, in megabytes.
// Set this to 0 to unlimit upload size. Defaults to 20 megabytes.
UploadMaxSize int `env:"UPLOAD_MAX_SIZE" env-default:"20"`
UploadDocumentMaxSize int `env:"UPLOAD_DOCUMENT_MAX_SIZE" env-default:"20"`
}
14 changes: 7 additions & 7 deletions internal/webserver/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ func SetupControllers(cfg Config, db *gorm.DB, metadataReaders map[string]metada
}

documentsCfg := document.Config{
WordsPerMinute: cfg.WordsPerMinute,
LibraryPath: cfg.LibraryPath,
HomeDir: cfg.HomeDir,
CoverMaxWidth: cfg.CoverMaxWidth,
Hostname: cfg.Hostname,
Port: cfg.Port,
WordsPerMinute: cfg.WordsPerMinute,
LibraryPath: cfg.LibraryPath,
HomeDir: cfg.HomeDir,
CoverMaxWidth: cfg.CoverMaxWidth,
Hostname: cfg.Hostname,
Port: cfg.Port,
UploadDocumentMaxSize: cfg.UploadDocumentMaxSize,
}

authController := auth.NewController(usersRepository, sender, authCfg, printers)
Expand Down Expand Up @@ -126,7 +127,6 @@ func SetupControllers(cfg Config, db *gorm.DB, metadataReaders map[string]metada
ErrorHandler: func(c *fiber.Ctx, err error) error {
// Status code defaults to 500
code := fiber.StatusInternalServerError

// Retrieve the custom status code if it's a *fiber.Error
var e *fiber.Error
if errors.As(err, &e) {
Expand Down
16 changes: 0 additions & 16 deletions internal/webserver/controller/auth/controller.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package auth

import (
"fmt"
"time"

"github.com/gofiber/fiber/v2"
"github.com/svera/coreander/v3/internal/webserver/model"
"golang.org/x/text/message"
)
Expand Down Expand Up @@ -34,11 +32,6 @@ type Config struct {
SessionTimeout time.Duration
}

const (
defaultHttpPort = 80
defaultHttpsPort = 443
)

func NewController(repository authRepository, sender recoveryEmail, cfg Config, printers map[string]*message.Printer) *Controller {
return &Controller{
repository: repository,
Expand All @@ -47,12 +40,3 @@ func NewController(repository authRepository, sender recoveryEmail, cfg Config,
config: cfg,
}
}

func (a *Controller) urlPort(c *fiber.Ctx) string {
port := fmt.Sprintf(":%d", a.config.Port)
if (a.config.Port == defaultHttpPort && c.Protocol() == "http") ||
(a.config.Port == defaultHttpsPort && c.Protocol() == "https") {
port = ""
}
return port
}
3 changes: 2 additions & 1 deletion internal/webserver/controller/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strings"

"github.com/gofiber/fiber/v2"
"github.com/svera/coreander/v3/internal/webserver/controller"
"github.com/svera/coreander/v3/internal/webserver/infrastructure"
)

Expand All @@ -13,7 +14,7 @@ func (a *Controller) Login(c *fiber.Ctx) error {
"%s://%s%s/%s/reset-password",
c.Protocol(),
a.config.Hostname,
a.urlPort(c),
controller.UrlPort(c.Protocol(), a.config.Port),
c.Params("lang"),
)

Expand Down
3 changes: 2 additions & 1 deletion internal/webserver/controller/auth/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/svera/coreander/v3/internal/webserver/controller"
"github.com/svera/coreander/v3/internal/webserver/infrastructure"
)

Expand All @@ -33,7 +34,7 @@ func (a *Controller) Request(c *fiber.Ctx) error {
"%s://%s%s/%s/reset-password?id=%s",
c.Protocol(),
a.config.Hostname,
a.urlPort(c),
controller.UrlPort(c.Protocol(), a.config.Port),
c.Params("lang"),
user.RecoveryUUID,
)
Expand Down
17 changes: 17 additions & 0 deletions internal/webserver/controller/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package controller

import "fmt"

const (
defaultHttpPort = 80
defaultHttpsPort = 443
)

func UrlPort(protocol string, port int) string {
urlPort := fmt.Sprintf(":%d", port)
if (port == defaultHttpPort && protocol == "http") ||
(port == defaultHttpsPort && protocol == "https") {
urlPort = ""
}
return urlPort
}
25 changes: 7 additions & 18 deletions internal/webserver/controller/document/controller.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package document

import (
"fmt"

"github.com/gofiber/fiber/v2"
"github.com/spf13/afero"
"github.com/svera/coreander/v3/internal/index"
"github.com/svera/coreander/v3/internal/metadata"
Expand Down Expand Up @@ -38,12 +35,13 @@ type highlightsRepository interface {
}

type Config struct {
WordsPerMinute float64
LibraryPath string
HomeDir string
CoverMaxWidth int
Hostname string
Port int
WordsPerMinute float64
LibraryPath string
HomeDir string
CoverMaxWidth int
Hostname string
Port int
UploadDocumentMaxSize int
}

type Controller struct {
Expand All @@ -70,12 +68,3 @@ func NewController(hlRepository highlightsRepository, sender Sender, idx IdxRead
appFs: appFs,
}
}

func (a *Controller) urlPort(c *fiber.Ctx) string {
port := fmt.Sprintf(":%d", a.config.Port)
if (a.config.Port == defaultHttpPort && c.Protocol() == "http") ||
(a.config.Port == defaultHttpsPort && c.Protocol() == "https") {
port = ""
}
return port
}
45 changes: 41 additions & 4 deletions internal/webserver/controller/document/upload.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package document

import (
"bytes"
"errors"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"slices"
"strings"

"github.com/gofiber/fiber/v2"
"github.com/svera/coreander/v3/internal/webserver/controller"
"github.com/svera/coreander/v3/internal/webserver/model"
"github.com/valyala/fasthttp"
)
Expand All @@ -22,22 +27,23 @@ func (d *Controller) UploadForm(c *fiber.Ctx) error {
return fiber.ErrForbidden
}

resetPassword := fmt.Sprintf(
upload := fmt.Sprintf(
"%s://%s%s/%s/upload",
c.Protocol(),
d.config.Hostname,
d.urlPort(c),
controller.UrlPort(c.Protocol(), d.config.Port),
c.Params("lang"),
)

msg := ""
if ref := string(c.Request().Header.Referer()); strings.HasPrefix(ref, resetPassword) {
if ref := string(c.Request().Header.Referer()); strings.HasPrefix(ref, upload) {
msg = "Document uploaded successfully."
}

return c.Render("upload", fiber.Map{
"Title": "Coreander",
"Message": msg,
"MaxSize": d.config.UploadDocumentMaxSize,
}, "layout")
}

Expand All @@ -64,13 +70,29 @@ func (d *Controller) Upload(c *fiber.Ctx) error {
}, "layout")
}

if file.Size > int64(d.config.UploadDocumentMaxSize*1024*1024) {
errorMessage := fmt.Sprintf("Document too large, the maximum allowed size is %d megabytes", d.config.UploadDocumentMaxSize)
return c.Status(fiber.StatusRequestEntityTooLarge).Render("upload", fiber.Map{
"Title": "Coreander",
"Error": errorMessage,
}, "layout")
}

errorMessage := ""
destination := filepath.Join(d.config.LibraryPath, file.Filename)
if err := c.SaveFile(file, destination); err != nil {

bytes, err := fileToBytes(file)
if err != nil {
errorMessage = "Error uploading document"
}
destFile, err := d.appFs.Create(destination)
if err != nil {
errorMessage = "Error uploading document"
}
destFile.Write(bytes)

if err := d.idx.AddFile(destination); err != nil {
os.Remove(destination)
errorMessage = "Error indexing document"
}

Expand All @@ -84,3 +106,18 @@ func (d *Controller) Upload(c *fiber.Ctx) error {

return c.Redirect(fmt.Sprintf("/%s/upload", c.Params("lang")))
}

func fileToBytes(fileHeader *multipart.FileHeader) ([]byte, error) {
f, err := fileHeader.Open()
if err != nil {
return []byte{}, err
}
defer f.Close()

buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, f); err != nil {
return []byte{}, err
}

return buf.Bytes(), nil
}
21 changes: 21 additions & 0 deletions internal/webserver/embedded/js/upload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const fileSelector = document.getElementById('file-selector');
fileSelector.addEventListener('change', (event) => {
const fileList = Array.from(event.target.files);
let fileSubmit = document.getElementById('file-submit');
let fileSelector = document.getElementById('file-selector');
let errorMessageContainer = document.getElementsByClassName('invalid-feedback')[0];

fileList.forEach(element => {
if (element.size > fileSelector.dataset.max_size) {
fileSubmit.setAttribute('disabled', '');
fileSelector.classList.add('is-invalid');
errorMessageContainer.classList.remove('visually-hidden');
errorMessageContainer.textContent = fileSelector.dataset.error_too_large;
} else {
fileSubmit.removeAttribute('disabled');
fileSelector.classList.remove('is-invalid');
errorMessageContainer.classList.add('visually-hidden');
errorMessageContainer.textContent = '';
}
});
});
3 changes: 2 additions & 1 deletion internal/webserver/embedded/translations/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,5 @@
"Upload": "Subir"
"Upload document": "Subir documento"
"Document uploaded successfully": "Documento subido con éxito."

"Document too large, the maximum allowed size is %d megabytes": "Documento demasiado grande, el tamaño máximo permitido es de %d megabytes"
"Request entity too large": "Petición demasiado grande"
2 changes: 2 additions & 0 deletions internal/webserver/embedded/translations/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,5 @@
"Upload": "Télécharger"
"Upload document": "Télécharger document"
"Document uploaded successfully": "Document téléchargé avec succès."
"Document too large, the maximum allowed size is %d megabytes": "Document trop volumineux, la taille maximale autorisée est de %d mégaoctets"
"Request entity too large": "Entité de requête trop grande"
3 changes: 3 additions & 0 deletions internal/webserver/embedded/views/errors/413.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="px-4 py-5 my-5 text-center">
<h2>{{t .Lang "Request entity too large"}}</h2>
</div>
10 changes: 7 additions & 3 deletions internal/webserver/embedded/views/upload.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ <h2>{{t .Lang "Upload document" }}</h2>
<div class="row mt-3 pe-0">
<form action="/{{.Lang}}/upload" method="post" enctype="multipart/form-data">
<div class="input-group">
<input type="file" name="filename" accept=".epub,.pdf, application/epub+zip,application/pdf" class="form-control form-control-lg" required>
<input type="submit" value='{{t .Lang "Upload"}}' class="btn btn-primary">
<input type="file" name="filename" id="file-selector" accept=".epub,.pdf, application/epub+zip,application/pdf" class="form-control form-control-lg" required data-max_size="{{.MaxSize}}" data-error_too_large='{{t .Lang "Document too large, the maximum allowed size is %d megabytes" .MaxSize}}'>
<input type="submit" id="file-submit" value='{{t .Lang "Upload"}}' class="btn btn-primary">
<div class="invalid-feedback visually-hidden">
</div>
</div>
</form>
</div>
</section>
</div>
</div>

<script src="/js/upload.js"></script>
1 change: 0 additions & 1 deletion internal/webserver/fixtures/file.txt

This file was deleted.

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.
2 changes: 1 addition & 1 deletion internal/webserver/highlights_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (

func TestHighlights(t *testing.T) {
db := infrastructure.Connect("file::memory:", 250)
appFS := loadFilesInMemoryFs([]string{"fixtures/metadata.epub"})
appFS := loadFilesInMemoryFs([]string{"fixtures/library/metadata.epub"})
app := bootstrapApp(db, &infrastructure.NoEmail{}, appFS)
data := url.Values{
"slug": {"john-doe-test-epub"},
Expand Down
4 changes: 2 additions & 2 deletions internal/webserver/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
func TestSearch(t *testing.T) {
db := infrastructure.Connect("file::memory:", 250)
smtpMock := &SMTPMock{}
appFS := loadDirInMemoryFs("fixtures")
appFS := loadDirInMemoryFs("fixtures/library")

app := bootstrapApp(db, smtpMock, appFS)

Expand Down Expand Up @@ -117,7 +117,7 @@ func TestSendDocument(t *testing.T) {
func TestRemoveDocument(t *testing.T) {
db := infrastructure.Connect("file::memory:", 250)
smtpMock := &SMTPMock{}
appFS := loadDirInMemoryFs("fixtures")
appFS := loadDirInMemoryFs("fixtures/library")
app := bootstrapApp(db, smtpMock, appFS)

assertSearchResults(app, t, "john+doe", 4)
Expand Down
Loading

0 comments on commit 8e1337b

Please sign in to comment.