diff --git a/README.md b/README.md index 70f65e3b..22efc174 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ A personal documents server, Coreander indexes the documents (EPUBs and PDFs wit * [Send to email supported](#send-to-email). * Read indexed epubs and PDFs from Coreander's interface thanks to [epub.js](http://futurepress.org/) and [pdf.js](https://mozilla.github.io/pdf.js/). * Restrictable access only to registered users. +* Upload documents through the web interface. ## Installation @@ -79,7 +80,7 @@ Coreander can send documents through email. This way, you can take advantage of ### User management and access restriction -Coreander distinguish between two kinds of users: regular users and administrator users, with the latter being the only ones with the ability to create new users and delete documents. +Coreander distinguish between two kinds of users: regular users and administrator users, with the latter being the only ones with the ability to create new users and upload and delete documents. By default, Coreander allow unrestricted access to its contents, except management areas which require an administrator user. To allow access only to registered users in the whole application, pass the `REQUIRE_AUTH=true` environment variable. @@ -106,3 +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_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. + diff --git a/config.go b/config.go index 06ccb701..5633e580 100644 --- a/config.go +++ b/config.go @@ -32,4 +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"` + // 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. + UploadDocumentMaxSize int `env:"UPLOAD_DOCUMENT_MAX_SIZE" env-default:"20"` } diff --git a/internal/index/bleve_write.go b/internal/index/bleve_write.go index 11698490..c6c4cec1 100644 --- a/internal/index/bleve_write.go +++ b/internal/index/bleve_write.go @@ -17,7 +17,7 @@ import ( func (b *BleveIndexer) AddFile(file string) error { ext := strings.ToLower(filepath.Ext(file)) if _, ok := b.reader[ext]; !ok { - return nil + return fmt.Errorf("file extension %s not supported", ext) } meta, err := b.reader[ext].Metadata(file) if err != nil { diff --git a/internal/metadata/epub.go b/internal/metadata/epub.go index a1ad04a7..d8f917d0 100644 --- a/internal/metadata/epub.go +++ b/internal/metadata/epub.go @@ -170,7 +170,7 @@ func words(documentFullPath string) (int, error) { defer r.Close() count := 0 for _, f := range r.File { - isContent, err := doublestar.PathMatch("O*PS/**/*.*html", f.Name) + isContent, err := doublestar.PathMatch("O*PS/**/*.*htm*", f.Name) if err != nil { return 0, err } diff --git a/internal/webserver/controller.go b/internal/webserver/controller.go index 09587f39..22392702 100644 --- a/internal/webserver/controller.go +++ b/internal/webserver/controller.go @@ -49,10 +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, + 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) @@ -97,6 +100,10 @@ func SetupControllers(cfg Config, db *gorm.DB, metadataReaders map[string]metada SigningKey: cfg.JwtSecret, SigningMethod: "HS256", TokenLookup: "cookie:coreander", + SuccessHandler: func(c *fiber.Ctx) error { + c.Locals("Session", jwtclaimsreader.SessionData(c)) + return c.Next() + }, ErrorHandler: func(c *fiber.Ctx, err error) error { return forbidden(c) }, @@ -105,6 +112,10 @@ func SetupControllers(cfg Config, db *gorm.DB, metadataReaders map[string]metada SigningKey: cfg.JwtSecret, SigningMethod: "HS256", TokenLookup: "cookie:coreander", + SuccessHandler: func(c *fiber.Ctx) error { + c.Locals("Session", jwtclaimsreader.SessionData(c)) + return c.Next() + }, ErrorHandler: func(c *fiber.Ctx, err error) error { err = c.Next() if cfg.RequireAuth { @@ -116,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) { diff --git a/internal/webserver/controller/auth/controller.go b/internal/webserver/controller/auth/controller.go index 7ff35292..1705a8d7 100644 --- a/internal/webserver/controller/auth/controller.go +++ b/internal/webserver/controller/auth/controller.go @@ -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" ) @@ -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, @@ -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 -} diff --git a/internal/webserver/controller/auth/login.go b/internal/webserver/controller/auth/login.go index a1ae5e2c..ccbff48e 100644 --- a/internal/webserver/controller/auth/login.go +++ b/internal/webserver/controller/auth/login.go @@ -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" ) @@ -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"), ) diff --git a/internal/webserver/controller/auth/request.go b/internal/webserver/controller/auth/request.go index c60a08d3..b8661a1b 100644 --- a/internal/webserver/controller/auth/request.go +++ b/internal/webserver/controller/auth/request.go @@ -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" ) @@ -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, ) diff --git a/internal/webserver/controller/controller.go b/internal/webserver/controller/controller.go new file mode 100644 index 00000000..8338f241 --- /dev/null +++ b/internal/webserver/controller/controller.go @@ -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 +} diff --git a/internal/webserver/controller/document/controller.go b/internal/webserver/controller/document/controller.go index acb58656..3a3620d6 100644 --- a/internal/webserver/controller/document/controller.go +++ b/internal/webserver/controller/document/controller.go @@ -23,6 +23,7 @@ type IdxReaderWriter interface { SameSubjects(slug string, quantity int) ([]index.Document, error) SameAuthors(slug string, quantity int) ([]index.Document, error) SameSeries(slug string, quantity int) ([]index.Document, error) + AddFile(file string) error RemoveFile(file string) error Documents(IDs []string) (map[string]index.Document, error) } @@ -34,10 +35,13 @@ type highlightsRepository interface { } type Config struct { - WordsPerMinute float64 - LibraryPath string - HomeDir string - CoverMaxWidth int + WordsPerMinute float64 + LibraryPath string + HomeDir string + CoverMaxWidth int + Hostname string + Port int + UploadDocumentMaxSize int } type Controller struct { diff --git a/internal/webserver/controller/document/detail.go b/internal/webserver/controller/document/detail.go index 46f89f55..8e54e3d2 100644 --- a/internal/webserver/controller/document/detail.go +++ b/internal/webserver/controller/document/detail.go @@ -8,7 +8,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/svera/coreander/v3/internal/webserver/infrastructure" - "github.com/svera/coreander/v3/internal/webserver/jwtclaimsreader" + "github.com/svera/coreander/v3/internal/webserver/model" ) func (d *Controller) Detail(c *fiber.Ctx) error { @@ -17,7 +17,11 @@ func (d *Controller) Detail(c *fiber.Ctx) error { emailSendingConfigured = false } - session := jwtclaimsreader.SessionData(c) + var session model.User + if val, ok := c.Locals("Session").(model.User); ok { + session = val + } + if session.WordsPerMinute > 0 { d.config.WordsPerMinute = session.WordsPerMinute } @@ -61,7 +65,6 @@ func (d *Controller) Detail(c *fiber.Ctx) error { "Document": document, "EmailSendingConfigured": emailSendingConfigured, "EmailFrom": d.sender.From(), - "Session": session, "SameSeries": sameSeries, "SameAuthors": sameAuthors, "SameSubjects": sameSubjects, diff --git a/internal/webserver/controller/document/search.go b/internal/webserver/controller/document/search.go index 4d0bac0c..a308a1fc 100644 --- a/internal/webserver/controller/document/search.go +++ b/internal/webserver/controller/document/search.go @@ -7,7 +7,6 @@ import ( "github.com/svera/coreander/v3/internal/index" "github.com/svera/coreander/v3/internal/result" "github.com/svera/coreander/v3/internal/webserver/infrastructure" - "github.com/svera/coreander/v3/internal/webserver/jwtclaimsreader" "github.com/svera/coreander/v3/internal/webserver/model" "github.com/svera/coreander/v3/internal/webserver/view" ) @@ -23,7 +22,11 @@ func (d *Controller) Search(c *fiber.Ctx) error { page = 1 } - session := jwtclaimsreader.SessionData(c) + var session model.User + if val, ok := c.Locals("Session").(model.User); ok { + session = val + } + if session.WordsPerMinute > 0 { d.config.WordsPerMinute = session.WordsPerMinute } @@ -46,7 +49,6 @@ func (d *Controller) Search(c *fiber.Ctx) error { "Title": "Search results", "EmailSendingConfigured": emailSendingConfigured, "EmailFrom": d.sender.From(), - "Session": session, "WordsPerMinute": d.config.WordsPerMinute, }, "layout") } @@ -77,7 +79,6 @@ func (d *Controller) Search(c *fiber.Ctx) error { return c.Render("index", fiber.Map{ "Count": count, "Title": "Coreander", - "Session": session, "Highlights": docsSortedByHighlightedDate, "EmailSendingConfigured": emailSendingConfigured, "EmailFrom": d.sender.From(), diff --git a/internal/webserver/controller/document/upload.go b/internal/webserver/controller/document/upload.go new file mode 100644 index 00000000..f7c80a45 --- /dev/null +++ b/internal/webserver/controller/document/upload.go @@ -0,0 +1,124 @@ +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" +) + +func (d *Controller) UploadForm(c *fiber.Ctx) error { + var session model.User + if val, ok := c.Locals("Session").(model.User); ok { + session = val + } + + if session.Role != model.RoleAdmin { + return fiber.ErrForbidden + } + + upload := fmt.Sprintf( + "%s://%s%s/%s/upload", + c.Protocol(), + d.config.Hostname, + controller.UrlPort(c.Protocol(), d.config.Port), + c.Params("lang"), + ) + + msg := "" + 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") +} + +func (d *Controller) Upload(c *fiber.Ctx) error { + session := c.Locals("Session").(model.User) + + if session.Role != model.RoleAdmin { + return fiber.ErrForbidden + } + + file, err := c.FormFile("filename") + if err != nil { + if errors.Is(err, fasthttp.ErrMissingFile) { + return c.Status(fiber.StatusBadRequest).Render("upload", fiber.Map{ + "Title": "Coreander", + "Error": "Invalid file type", + }, "layout") + } + } + + allowedTypes := []string{"application/epub+zip", "application/pdf"} + if !slices.Contains(allowedTypes, file.Header.Get("Content-Type")) { + return c.Status(fiber.StatusBadRequest).Render("upload", fiber.Map{ + "Title": "Coreander", + "Error": "Invalid file type", + }, "layout") + } + + if file.Size > int64(d.config.UploadDocumentMaxSize*1024*1024) { + return c.Status(fiber.StatusRequestEntityTooLarge).Render("upload", fiber.Map{ + "Title": "Coreander", + "Error": fmt.Sprintf("Document too large, the maximum allowed size is %d megabytes", d.config.UploadDocumentMaxSize), + }, "layout") + } + + destination := filepath.Join(d.config.LibraryPath, file.Filename) + internalServerErrorStatus := c.Status(fiber.StatusInternalServerError).Render("upload", fiber.Map{ + "Title": "Coreander", + "Error": "Error uploading document", + }, "layout") + + bytes, err := fileToBytes(file) + if err != nil { + return internalServerErrorStatus + } + + destFile, err := d.appFs.Create(destination) + if err != nil { + return internalServerErrorStatus + } + + if _, err := destFile.Write(bytes); err != nil { + return internalServerErrorStatus + } + + destFile.Close() + if err := d.idx.AddFile(destination); err != nil { + os.Remove(destination) + return internalServerErrorStatus + } + + 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 +} diff --git a/internal/webserver/controller/highlight/highlights.go b/internal/webserver/controller/highlight/highlights.go index 1a958a75..66a48680 100644 --- a/internal/webserver/controller/highlight/highlights.go +++ b/internal/webserver/controller/highlight/highlights.go @@ -7,7 +7,6 @@ import ( "github.com/svera/coreander/v3/internal/index" "github.com/svera/coreander/v3/internal/result" "github.com/svera/coreander/v3/internal/webserver/infrastructure" - "github.com/svera/coreander/v3/internal/webserver/jwtclaimsreader" "github.com/svera/coreander/v3/internal/webserver/model" "github.com/svera/coreander/v3/internal/webserver/view" ) @@ -23,7 +22,11 @@ func (h *Controller) Highlights(c *fiber.Ctx) error { page = 1 } - session := jwtclaimsreader.SessionData(c) + var session model.User + if val, ok := c.Locals("Session").(model.User); ok { + session = val + } + if session.WordsPerMinute > 0 { h.wordsPerMinute = session.WordsPerMinute } @@ -66,7 +69,6 @@ func (h *Controller) Highlights(c *fiber.Ctx) error { "Title": "Highlights", "EmailSendingConfigured": emailSendingConfigured, "EmailFrom": h.sender.From(), - "Session": session, "WordsPerMinute": h.wordsPerMinute, }, "layout") } diff --git a/internal/webserver/controller/user/controller.go b/internal/webserver/controller/user/controller.go index 4456c3b8..35cd13d5 100644 --- a/internal/webserver/controller/user/controller.go +++ b/internal/webserver/controller/user/controller.go @@ -3,7 +3,6 @@ package user import ( "github.com/gofiber/fiber/v2" "github.com/svera/coreander/v3/internal/result" - "github.com/svera/coreander/v3/internal/webserver/jwtclaimsreader" "github.com/svera/coreander/v3/internal/webserver/model" ) @@ -38,7 +37,10 @@ func NewController(repository usersRepository, usersCfg Config) *Controller { // New renders the new user form func (u *Controller) New(c *fiber.Ctx) error { - session := jwtclaimsreader.SessionData(c) + var session model.User + if val, ok := c.Locals("Session").(model.User); ok { + session = val + } if session.Role != model.RoleAdmin { return fiber.ErrForbidden @@ -49,7 +51,6 @@ func (u *Controller) New(c *fiber.Ctx) error { } return c.Render("users/new", fiber.Map{ "Title": "Add user", - "Session": session, "MinPasswordLength": u.config.MinPasswordLength, "User": user, "Errors": map[string]string{}, diff --git a/internal/webserver/controller/user/create.go b/internal/webserver/controller/user/create.go index a105db1c..090b0957 100644 --- a/internal/webserver/controller/user/create.go +++ b/internal/webserver/controller/user/create.go @@ -6,13 +6,15 @@ import ( "github.com/gofiber/fiber/v2" "github.com/google/uuid" - "github.com/svera/coreander/v3/internal/webserver/jwtclaimsreader" "github.com/svera/coreander/v3/internal/webserver/model" ) // Create gathers information coming from the new user form and creates a new user func (u *Controller) Create(c *fiber.Ctx) error { - session := jwtclaimsreader.SessionData(c) + var session model.User + if val, ok := c.Locals("Session").(model.User); ok { + session = val + } if session.Role != model.RoleAdmin { return fiber.ErrForbidden @@ -35,10 +37,9 @@ func (u *Controller) Create(c *fiber.Ctx) error { if errs = user.ConfirmPassword(c.FormValue("confirm-password"), u.config.MinPasswordLength, errs); len(errs) > 0 { return c.Render("users/new", fiber.Map{ - "Title": "Add user", - "Session": session, - "Errors": errs, - "User": user, + "Title": "Add user", + "Errors": errs, + "User": user, }, "layout") } diff --git a/internal/webserver/controller/user/edit.go b/internal/webserver/controller/user/edit.go index da9dbc3f..9c2f26a9 100644 --- a/internal/webserver/controller/user/edit.go +++ b/internal/webserver/controller/user/edit.go @@ -2,7 +2,6 @@ package user import ( "github.com/gofiber/fiber/v2" - "github.com/svera/coreander/v3/internal/webserver/jwtclaimsreader" "github.com/svera/coreander/v3/internal/webserver/model" ) @@ -13,7 +12,10 @@ func (u *Controller) Edit(c *fiber.Ctx) error { return fiber.ErrNotFound } - session := jwtclaimsreader.SessionData(c) + var session model.User + if val, ok := c.Locals("Session").(model.User); ok { + session = val + } if session.Role != model.RoleAdmin && session.Uuid != c.Params("uuid") { return fiber.ErrForbidden @@ -22,7 +24,6 @@ func (u *Controller) Edit(c *fiber.Ctx) error { return c.Render("users/edit", fiber.Map{ "Title": "Edit user", "User": user, - "Session": session, "MinPasswordLength": u.config.MinPasswordLength, "Errors": map[string]string{}, }, "layout") diff --git a/internal/webserver/controller/user/list.go b/internal/webserver/controller/user/list.go index 27182263..b76c103e 100644 --- a/internal/webserver/controller/user/list.go +++ b/internal/webserver/controller/user/list.go @@ -4,14 +4,16 @@ import ( "strconv" "github.com/gofiber/fiber/v2" - "github.com/svera/coreander/v3/internal/webserver/jwtclaimsreader" "github.com/svera/coreander/v3/internal/webserver/model" "github.com/svera/coreander/v3/internal/webserver/view" ) // List list all users registered in the database func (u *Controller) List(c *fiber.Ctx) error { - session := jwtclaimsreader.SessionData(c) + var session model.User + if val, ok := c.Locals("Session").(model.User); ok { + session = val + } if session.Role != model.RoleAdmin { return fiber.ErrForbidden @@ -27,7 +29,6 @@ func (u *Controller) List(c *fiber.Ctx) error { "Title": "Users", "Users": users.Hits(), "Paginator": view.Pagination(model.MaxPagesNavigator, users, map[string]string{}), - "Session": session, "Admins": u.repository.Admins(), }, "layout") } diff --git a/internal/webserver/controller/user/update.go b/internal/webserver/controller/user/update.go index 4a5c30ad..ca8767c9 100644 --- a/internal/webserver/controller/user/update.go +++ b/internal/webserver/controller/user/update.go @@ -4,7 +4,6 @@ import ( "strconv" "github.com/gofiber/fiber/v2" - "github.com/svera/coreander/v3/internal/webserver/jwtclaimsreader" "github.com/svera/coreander/v3/internal/webserver/model" ) @@ -15,7 +14,10 @@ func (u *Controller) Update(c *fiber.Ctx) error { return fiber.ErrNotFound } - session := jwtclaimsreader.SessionData(c) + var session model.User + if val, ok := c.Locals("Session").(model.User); ok { + session = val + } if session.Role != model.RoleAdmin && session.Uuid != c.Params("uuid") { return fiber.ErrForbidden @@ -34,7 +36,6 @@ func (u *Controller) Update(c *fiber.Ctx) error { return c.Render("users/edit", fiber.Map{ "Title": "Edit user", "User": user, - "Session": session, "MinPasswordLength": u.config.MinPasswordLength, "Errors": errs, }, "layout") @@ -47,7 +48,6 @@ func (u *Controller) Update(c *fiber.Ctx) error { return c.Render("users/edit", fiber.Map{ "Title": "Edit user", "User": user, - "Session": session, "MinPasswordLength": u.config.MinPasswordLength, "Errors": errs, "Message": "Profile updated", @@ -76,7 +76,6 @@ func (u *Controller) updatePassword(c *fiber.Ctx, session, user model.User) erro return c.Render("users/edit", fiber.Map{ "Title": "Edit user", "User": user, - "Session": session, "MinPasswordLength": u.config.MinPasswordLength, "ActiveTab": "password", "Errors": errs, @@ -91,7 +90,6 @@ func (u *Controller) updatePassword(c *fiber.Ctx, session, user model.User) erro return c.Render("users/edit", fiber.Map{ "Title": "Edit user", "User": user, - "Session": session, "MinPasswordLength": u.config.MinPasswordLength, "ActiveTab": "password", "Errors": errs, diff --git a/internal/webserver/embedded/js/upload.js b/internal/webserver/embedded/js/upload.js new file mode 100644 index 00000000..3bfff7d2 --- /dev/null +++ b/internal/webserver/embedded/js/upload.js @@ -0,0 +1,29 @@ + const fileSelector = document.getElementById('file-selector'); + const uploadForm = document.getElementById('upload-form'); + let fileSubmit = document.getElementById('file-submit'); + + fileSelector.addEventListener('change', (event) => { + const fileList = Array.from(event.target.files); + let fileSelector = document.getElementById('file-selector'); + let errorMessageContainer = document.getElementsByClassName('invalid-feedback')[0]; + + fileList.forEach(element => { + if (element.size > fileSelector.dataset.max_size * 1024 * 1024) { + 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 = ''; + } + }); + }); + + uploadForm.addEventListener('submit', (event) => { + let spinner = document.querySelector('.spinner-border'); + spinner.classList.remove('visually-hidden') + fileSubmit.setAttribute('disabled', ''); + }); diff --git a/internal/webserver/embedded/translations/es.yml b/internal/webserver/embedded/translations/es.yml index 5c81af78..ba1d781d 100644 --- a/internal/webserver/embedded/translations/es.yml +++ b/internal/webserver/embedded/translations/es.yml @@ -108,3 +108,9 @@ "Method not allowed": "Método no permitido" "Your highlights": "Tus destacados" "You haven't highlighted any document yet.": "Aún no has destacado ningún documento." +"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" +"Error uploading document": "Error al subir el documento" +"Invalid file type": "Tipo de archivo no válido" diff --git a/internal/webserver/embedded/translations/fr.yml b/internal/webserver/embedded/translations/fr.yml index c7fdc8c4..766b7d05 100644 --- a/internal/webserver/embedded/translations/fr.yml +++ b/internal/webserver/embedded/translations/fr.yml @@ -108,3 +108,9 @@ "Method not allowed": "Méthode Non Autorisée" "Your highlights": "Vos favoris" "You haven't highlighted any document yet.": "Vous n'avez encore ajouté aucun document à vos favoris." +"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" +"Error uploading document": "Erreur lors du téléchargement du document" +"Invalid file type": "Type de fichier invalide" diff --git a/internal/webserver/embedded/views/errors/413.html b/internal/webserver/embedded/views/errors/413.html new file mode 100644 index 00000000..1dba9b0e --- /dev/null +++ b/internal/webserver/embedded/views/errors/413.html @@ -0,0 +1,3 @@ +