2022-07-06 23:19:05 +02:00
|
|
|
package apis
|
|
|
|
|
|
|
|
import (
|
2023-02-18 19:33:42 +02:00
|
|
|
"fmt"
|
|
|
|
|
2022-07-06 23:19:05 +02:00
|
|
|
"github.com/labstack/echo/v5"
|
|
|
|
"github.com/pocketbase/pocketbase/core"
|
|
|
|
"github.com/pocketbase/pocketbase/models"
|
|
|
|
"github.com/pocketbase/pocketbase/models/schema"
|
|
|
|
"github.com/pocketbase/pocketbase/tools/list"
|
|
|
|
)
|
|
|
|
|
2022-10-30 10:28:14 +02:00
|
|
|
var imageContentTypes = []string{"image/png", "image/jpg", "image/jpeg", "image/gif"}
|
2022-07-06 23:19:05 +02:00
|
|
|
var defaultThumbSizes = []string{"100x100"}
|
|
|
|
|
2022-10-30 10:28:14 +02:00
|
|
|
// bindFileApi registers the file api endpoints and the corresponding handlers.
|
|
|
|
func bindFileApi(app core.App, rg *echo.Group) {
|
2022-07-06 23:19:05 +02:00
|
|
|
api := fileApi{app: app}
|
|
|
|
|
|
|
|
subGroup := rg.Group("/files", ActivityLogger(app))
|
2023-03-05 15:37:50 +02:00
|
|
|
subGroup.HEAD("/:collection/:recordId/:filename", api.download, LoadCollectionContext(api.app))
|
2022-07-06 23:19:05 +02:00
|
|
|
subGroup.GET("/:collection/:recordId/:filename", api.download, LoadCollectionContext(api.app))
|
|
|
|
}
|
|
|
|
|
|
|
|
type fileApi struct {
|
|
|
|
app core.App
|
|
|
|
}
|
|
|
|
|
|
|
|
func (api *fileApi) download(c echo.Context) error {
|
|
|
|
collection, _ := c.Get(ContextCollectionKey).(*models.Collection)
|
|
|
|
if collection == nil {
|
2022-10-30 10:28:14 +02:00
|
|
|
return NewNotFoundError("", nil)
|
2022-07-06 23:19:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
recordId := c.PathParam("recordId")
|
|
|
|
if recordId == "" {
|
2022-10-30 10:28:14 +02:00
|
|
|
return NewNotFoundError("", nil)
|
2022-07-06 23:19:05 +02:00
|
|
|
}
|
|
|
|
|
2022-10-30 10:28:14 +02:00
|
|
|
record, err := api.app.Dao().FindRecordById(collection.Id, recordId)
|
2022-07-06 23:19:05 +02:00
|
|
|
if err != nil {
|
2022-10-30 10:28:14 +02:00
|
|
|
return NewNotFoundError("", err)
|
2022-07-06 23:19:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
filename := c.PathParam("filename")
|
|
|
|
|
|
|
|
fileField := record.FindFileFieldByFile(filename)
|
|
|
|
if fileField == nil {
|
2022-10-30 10:28:14 +02:00
|
|
|
return NewNotFoundError("", nil)
|
2022-07-06 23:19:05 +02:00
|
|
|
}
|
2023-02-18 19:33:42 +02:00
|
|
|
|
2022-07-06 23:19:05 +02:00
|
|
|
options, _ := fileField.Options.(*schema.FileOptions)
|
|
|
|
|
2023-02-18 19:33:42 +02:00
|
|
|
baseFilesPath := record.BaseFilesPath()
|
|
|
|
|
|
|
|
// fetch the original view file field related record
|
|
|
|
if collection.IsView() {
|
|
|
|
fileRecord, err := api.app.Dao().FindRecordByViewFile(collection.Id, fileField.Name, filename)
|
|
|
|
if err != nil {
|
|
|
|
return NewNotFoundError("", fmt.Errorf("Failed to fetch view file field record: %w", err))
|
|
|
|
}
|
|
|
|
baseFilesPath = fileRecord.BaseFilesPath()
|
|
|
|
}
|
|
|
|
|
2022-07-06 23:19:05 +02:00
|
|
|
fs, err := api.app.NewFilesystem()
|
|
|
|
if err != nil {
|
2022-10-30 10:28:14 +02:00
|
|
|
return NewBadRequestError("Filesystem initialization failure.", err)
|
2022-07-06 23:19:05 +02:00
|
|
|
}
|
|
|
|
defer fs.Close()
|
|
|
|
|
2023-02-18 19:33:42 +02:00
|
|
|
originalPath := baseFilesPath + "/" + filename
|
2022-07-06 23:19:05 +02:00
|
|
|
servedPath := originalPath
|
|
|
|
servedName := filename
|
|
|
|
|
|
|
|
// check for valid thumb size param
|
|
|
|
thumbSize := c.QueryParam("thumb")
|
|
|
|
if thumbSize != "" && (list.ExistInSlice(thumbSize, defaultThumbSizes) || list.ExistInSlice(thumbSize, options.Thumbs)) {
|
|
|
|
// extract the original file meta attributes and check it existence
|
|
|
|
oAttrs, oAttrsErr := fs.Attributes(originalPath)
|
|
|
|
if oAttrsErr != nil {
|
2022-10-30 10:28:14 +02:00
|
|
|
return NewNotFoundError("", err)
|
2022-07-06 23:19:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// check if it is an image
|
|
|
|
if list.ExistInSlice(oAttrs.ContentType, imageContentTypes) {
|
|
|
|
// add thumb size as file suffix
|
|
|
|
servedName = thumbSize + "_" + filename
|
2023-02-18 19:33:42 +02:00
|
|
|
servedPath = baseFilesPath + "/thumbs_" + filename + "/" + servedName
|
2022-07-06 23:19:05 +02:00
|
|
|
|
|
|
|
// check if the thumb exists:
|
|
|
|
// - if doesn't exist - create a new thumb with the specified thumb size
|
|
|
|
// - if exists - compare last modified dates to determine whether the thumb should be recreated
|
|
|
|
tAttrs, tAttrsErr := fs.Attributes(servedPath)
|
|
|
|
if tAttrsErr != nil || oAttrs.ModTime.After(tAttrs.ModTime) {
|
2022-08-17 21:29:11 +02:00
|
|
|
if err := fs.CreateThumb(originalPath, servedPath, thumbSize); err != nil {
|
2022-07-06 23:19:05 +02:00
|
|
|
servedPath = originalPath // fallback to the original
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-27 22:19:08 +02:00
|
|
|
event := new(core.FileDownloadEvent)
|
|
|
|
event.HttpContext = c
|
|
|
|
event.Collection = collection
|
|
|
|
event.Record = record
|
|
|
|
event.FileField = fileField
|
|
|
|
event.ServedPath = servedPath
|
|
|
|
event.ServedName = servedName
|
2022-07-06 23:19:05 +02:00
|
|
|
|
2023-01-30 12:54:51 +02:00
|
|
|
// clickjacking shouldn't be a concern when serving uploaded files,
|
|
|
|
// so it safe to unset the global X-Frame-Options to allow files embedding
|
|
|
|
// (note: it is out of the hook to allow users to customize the behavior)
|
|
|
|
c.Response().Header().Del("X-Frame-Options")
|
|
|
|
|
2022-07-06 23:19:05 +02:00
|
|
|
return api.app.OnFileDownloadRequest().Trigger(event, func(e *core.FileDownloadEvent) error {
|
2022-11-29 15:52:37 +02:00
|
|
|
res := e.HttpContext.Response()
|
|
|
|
req := e.HttpContext.Request()
|
|
|
|
if err := fs.Serve(res, req, e.ServedPath, e.ServedName); err != nil {
|
2022-10-30 10:28:14 +02:00
|
|
|
return NewNotFoundError("", err)
|
2022-07-06 23:19:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|