2022-07-06 23:19:05 +02:00
|
|
|
package rest
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"mime/multipart"
|
|
|
|
"net/http"
|
|
|
|
"path/filepath"
|
2022-07-21 11:56:17 +02:00
|
|
|
"regexp"
|
2022-08-18 19:44:29 +02:00
|
|
|
"strings"
|
2022-07-06 23:19:05 +02:00
|
|
|
|
2022-10-30 10:28:14 +02:00
|
|
|
"github.com/gabriel-vasile/mimetype"
|
2022-08-18 19:44:29 +02:00
|
|
|
"github.com/pocketbase/pocketbase/tools/inflector"
|
2022-07-06 23:19:05 +02:00
|
|
|
"github.com/pocketbase/pocketbase/tools/security"
|
|
|
|
)
|
|
|
|
|
|
|
|
// DefaultMaxMemory defines the default max memory bytes that
|
|
|
|
// will be used when parsing a form request body.
|
|
|
|
const DefaultMaxMemory = 32 << 20 // 32mb
|
|
|
|
|
2022-07-21 11:56:17 +02:00
|
|
|
var extensionInvalidCharsRegex = regexp.MustCompile(`[^\w\.\*\-\+\=\#]+`)
|
|
|
|
|
2022-07-06 23:19:05 +02:00
|
|
|
// UploadedFile defines a single multipart uploaded file instance.
|
|
|
|
type UploadedFile struct {
|
|
|
|
name string
|
|
|
|
header *multipart.FileHeader
|
|
|
|
}
|
|
|
|
|
|
|
|
// Name returns an assigned unique name to the uploaded file.
|
|
|
|
func (f *UploadedFile) Name() string {
|
|
|
|
return f.name
|
|
|
|
}
|
|
|
|
|
|
|
|
// Header returns the file header that comes with the multipart request.
|
|
|
|
func (f *UploadedFile) Header() *multipart.FileHeader {
|
|
|
|
return f.header
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindUploadedFiles extracts all form files of `key` from a http request
|
|
|
|
// and returns a slice with `UploadedFile` instances (if any).
|
|
|
|
func FindUploadedFiles(r *http.Request, key string) ([]*UploadedFile, error) {
|
|
|
|
if r.MultipartForm == nil {
|
|
|
|
err := r.ParseMultipartForm(DefaultMaxMemory)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.MultipartForm == nil || r.MultipartForm.File == nil || len(r.MultipartForm.File[key]) == 0 {
|
|
|
|
return nil, http.ErrMissingFile
|
|
|
|
}
|
|
|
|
|
2022-10-30 10:28:14 +02:00
|
|
|
result := make([]*UploadedFile, 0, len(r.MultipartForm.File[key]))
|
2022-07-06 23:19:05 +02:00
|
|
|
|
2022-10-30 10:28:14 +02:00
|
|
|
for _, fh := range r.MultipartForm.File[key] {
|
2022-07-06 23:19:05 +02:00
|
|
|
file, err := fh.Open()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
2022-10-30 10:28:14 +02:00
|
|
|
// extension
|
|
|
|
// ---
|
2022-08-18 19:44:29 +02:00
|
|
|
originalExt := filepath.Ext(fh.Filename)
|
|
|
|
sanitizedExt := extensionInvalidCharsRegex.ReplaceAllString(originalExt, "")
|
2022-10-30 10:28:14 +02:00
|
|
|
if sanitizedExt == "" {
|
|
|
|
// try to detect the extension from the mime type
|
|
|
|
mt, err := mimetype.DetectReader(file)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
sanitizedExt = mt.Extension()
|
|
|
|
}
|
2022-08-18 19:44:29 +02:00
|
|
|
|
2022-10-30 10:28:14 +02:00
|
|
|
// name
|
|
|
|
// ---
|
2022-08-18 19:44:29 +02:00
|
|
|
originalName := strings.TrimSuffix(fh.Filename, originalExt)
|
|
|
|
sanitizedName := inflector.Snakecase(originalName)
|
|
|
|
if length := len(sanitizedName); length < 3 {
|
|
|
|
// the name is too short so we concatenate an additional random part
|
2022-12-05 13:57:09 +02:00
|
|
|
sanitizedName += security.RandomString(10)
|
2022-08-18 19:44:29 +02:00
|
|
|
} else if length > 100 {
|
|
|
|
// keep only the first 100 characters (it is multibyte safe after Snakecase)
|
|
|
|
sanitizedName = sanitizedName[:100]
|
|
|
|
}
|
|
|
|
|
|
|
|
uploadedFilename := fmt.Sprintf(
|
|
|
|
"%s_%s%s",
|
|
|
|
sanitizedName,
|
|
|
|
security.RandomString(10), // ensure that there is always a random part
|
|
|
|
sanitizedExt,
|
|
|
|
)
|
2022-07-21 11:56:17 +02:00
|
|
|
|
2022-10-30 10:28:14 +02:00
|
|
|
result = append(result, &UploadedFile{
|
2022-08-18 19:44:29 +02:00
|
|
|
name: uploadedFilename,
|
2022-07-06 23:19:05 +02:00
|
|
|
header: fh,
|
2022-10-30 10:28:14 +02:00
|
|
|
})
|
2022-07-06 23:19:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|