1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2024-12-11 13:18:31 +02:00
pocketbase/forms/record_upsert.go

782 lines
21 KiB
Go
Raw Normal View History

2022-07-06 23:19:05 +02:00
package forms
import (
"encoding/json"
"errors"
"fmt"
"log"
2022-07-06 23:19:05 +02:00
"net/http"
"regexp"
"strconv"
2022-10-30 10:28:14 +02:00
"strings"
2022-07-06 23:19:05 +02:00
2022-08-06 21:16:58 +02:00
validation "github.com/go-ozzo/ozzo-validation/v4"
2022-10-30 10:28:14 +02:00
"github.com/go-ozzo/ozzo-validation/v4/is"
2022-07-06 23:19:05 +02:00
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/daos"
"github.com/pocketbase/pocketbase/forms/validators"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/rest"
2022-10-30 10:28:14 +02:00
"github.com/pocketbase/pocketbase/tools/security"
2022-07-06 23:19:05 +02:00
"github.com/spf13/cast"
)
2022-10-30 10:28:14 +02:00
// username value regex pattern
var usernameRegex = regexp.MustCompile(`^[\w][\w\.]*$`)
// RecordUpsert is a [models.Record] upsert (create/update) form.
2022-07-06 23:19:05 +02:00
type RecordUpsert struct {
2022-10-30 10:28:14 +02:00
app core.App
dao *daos.Dao
manageAccess bool
record *models.Record
2022-07-06 23:19:05 +02:00
2022-10-30 10:28:14 +02:00
filesToUpload map[string][]*rest.UploadedFile
filesToDelete []string // names list
2022-10-30 10:28:14 +02:00
// base model fields
Id string `json:"id"`
// auth collection fields
// ---
Username string `json:"username"`
Email string `json:"email"`
EmailVisibility bool `json:"emailVisibility"`
Verified bool `json:"verified"`
Password string `json:"password"`
PasswordConfirm string `json:"passwordConfirm"`
OldPassword string `json:"oldPassword"`
// ---
2022-07-06 23:19:05 +02:00
Data map[string]any `json:"data"`
}
2022-08-06 21:16:58 +02:00
// NewRecordUpsert creates a new [RecordUpsert] form with initializer
// config created from the provided [core.App] and [models.Record] instances
2022-10-30 10:28:14 +02:00
// (for create you could pass a pointer to an empty Record - models.NewRecord(collection)).
2022-08-06 21:16:58 +02:00
//
2022-10-30 10:28:14 +02:00
// If you want to submit the form as part of a transaction,
// you can change the default Dao via [SetDao()].
2022-07-06 23:19:05 +02:00
func NewRecordUpsert(app core.App, record *models.Record) *RecordUpsert {
form := &RecordUpsert{
2022-10-30 10:28:14 +02:00
app: app,
dao: app.Dao(),
2022-07-06 23:19:05 +02:00
record: record,
filesToDelete: []string{},
2022-10-30 10:28:14 +02:00
filesToUpload: map[string][]*rest.UploadedFile{},
2022-07-06 23:19:05 +02:00
}
2022-10-30 10:28:14 +02:00
form.loadFormDefaults()
2022-08-06 21:16:58 +02:00
2022-10-30 10:28:14 +02:00
return form
}
2022-08-07 14:38:21 +02:00
2022-10-30 10:28:14 +02:00
// SetFullManageAccess sets the manageAccess bool flag of the current
// form to enable/disable directly changing some system record fields
// (often used with auth collection records).
func (form *RecordUpsert) SetFullManageAccess(fullManageAccess bool) {
form.manageAccess = fullManageAccess
}
2022-08-06 21:16:58 +02:00
2022-10-30 10:28:14 +02:00
// SetDao replaces the default form Dao instance with the provided one.
func (form *RecordUpsert) SetDao(dao *daos.Dao) {
form.dao = dao
}
func (form *RecordUpsert) loadFormDefaults() {
form.Id = form.record.Id
if form.record.Collection().IsAuth() {
form.Username = form.record.Username()
form.Email = form.record.Email()
form.EmailVisibility = form.record.EmailVisibility()
form.Verified = form.record.Verified()
2022-07-06 23:19:05 +02:00
}
2022-10-30 10:28:14 +02:00
form.Data = map[string]any{}
for _, field := range form.record.Collection().Schema.Fields() {
form.Data[field.Name] = form.record.Get(field.Name)
}
2022-07-06 23:19:05 +02:00
}
func (form *RecordUpsert) getContentType(r *http.Request) string {
t := r.Header.Get("Content-Type")
for i, c := range t {
if c == ' ' || c == ';' {
return t[:i]
}
}
return t
}
2022-10-30 10:28:14 +02:00
func (form *RecordUpsert) extractRequestData(r *http.Request, keyPrefix string) (map[string]any, error) {
2022-07-06 23:19:05 +02:00
switch form.getContentType(r) {
case "application/json":
2022-10-30 10:28:14 +02:00
return form.extractJsonData(r, keyPrefix)
2022-07-06 23:19:05 +02:00
case "multipart/form-data":
2022-10-30 10:28:14 +02:00
return form.extractMultipartFormData(r, keyPrefix)
2022-07-06 23:19:05 +02:00
default:
return nil, errors.New("Unsupported request Content-Type.")
}
}
2022-10-30 10:28:14 +02:00
func (form *RecordUpsert) extractJsonData(r *http.Request, keyPrefix string) (map[string]any, error) {
2022-07-06 23:19:05 +02:00
result := map[string]any{}
2022-10-30 10:28:14 +02:00
err := rest.CopyJsonBody(r, &result)
if keyPrefix != "" {
parts := strings.Split(keyPrefix, ".")
for _, part := range parts {
if result[part] == nil {
break
}
if v, ok := result[part].(map[string]any); ok {
result = v
}
}
}
2022-07-06 23:19:05 +02:00
return result, err
}
2022-10-30 10:28:14 +02:00
func (form *RecordUpsert) extractMultipartFormData(r *http.Request, keyPrefix string) (map[string]any, error) {
2022-07-06 23:19:05 +02:00
result := map[string]any{}
// parse form data (if not already)
if err := r.ParseMultipartForm(rest.DefaultMaxMemory); err != nil {
return result, err
}
arrayValueSupportTypes := schema.ArraybleFieldTypes()
2022-10-30 10:28:14 +02:00
form.filesToUpload = map[string][]*rest.UploadedFile{}
for fullKey, values := range r.PostForm {
key := fullKey
if keyPrefix != "" {
key = strings.TrimPrefix(key, keyPrefix+".")
}
2022-07-06 23:19:05 +02:00
if len(values) == 0 {
result[key] = nil
continue
}
field := form.record.Collection().Schema.GetFieldByName(key)
if field != nil && list.ExistInSlice(field.Type, arrayValueSupportTypes) {
result[key] = values
} else {
result[key] = values[0]
}
}
2022-10-30 10:28:14 +02:00
// load uploaded files (if any)
for _, field := range form.record.Collection().Schema.Fields() {
if field.Type != schema.FieldTypeFile {
continue // not a file field
}
key := field.Name
fullKey := key
if keyPrefix != "" {
fullKey = keyPrefix + "." + key
}
files, err := rest.FindUploadedFiles(r, fullKey)
if err != nil || len(files) == 0 {
if err != nil && err != http.ErrMissingFile && form.app.IsDebug() {
log.Printf("%q uploaded file error: %v\n", fullKey, err)
}
// skip invalid or missing file(s)
continue
}
options, ok := field.Options.(*schema.FileOptions)
if !ok {
continue
}
if form.filesToUpload[key] == nil {
form.filesToUpload[key] = []*rest.UploadedFile{}
}
if options.MaxSelect == 1 {
form.filesToUpload[key] = append(form.filesToUpload[key], files[0])
} else if options.MaxSelect > 1 {
form.filesToUpload[key] = append(form.filesToUpload[key], files...)
}
}
2022-07-06 23:19:05 +02:00
return result, nil
}
func (form *RecordUpsert) normalizeData() error {
for _, field := range form.record.Collection().Schema.Fields() {
if v, ok := form.Data[field.Name]; ok {
form.Data[field.Name] = field.PrepareValue(v)
}
}
return nil
}
2022-10-30 10:28:14 +02:00
// LoadRequest extracts the json or multipart/form-data request data
// and lods it into the form.
2022-07-06 23:19:05 +02:00
//
// File upload is supported only via multipart/form-data.
//
// To DELETE previously uploaded file(s) you can suffix the field name
2022-10-30 10:28:14 +02:00
// with the file index or filename (eg. `myfile.0`) and set it to null or empty string.
2022-07-06 23:19:05 +02:00
// For single file upload fields, you can skip the index and directly
2022-10-30 10:28:14 +02:00
// reset the field using its field name (eg. `myfile = null`).
func (form *RecordUpsert) LoadRequest(r *http.Request, keyPrefix string) error {
requestData, err := form.extractRequestData(r, keyPrefix)
2022-07-06 23:19:05 +02:00
if err != nil {
return err
}
2022-10-30 10:28:14 +02:00
return form.LoadData(requestData)
}
// LoadData loads and normalizes the provided data into the form.
//
// To DELETE previously uploaded file(s) you can suffix the field name
// with the file index or filename (eg. `myfile.0`) and set it to null or empty string.
// For single file upload fields, you can skip the index and directly
// reset the field using its field name (eg. `myfile = null`).
func (form *RecordUpsert) LoadData(requestData map[string]any) error {
// load base system fields
if v, ok := requestData["id"]; ok {
form.Id = cast.ToString(v)
2022-08-06 21:16:58 +02:00
}
2022-10-30 10:28:14 +02:00
// load auth system fields
if form.record.Collection().IsAuth() {
if v, ok := requestData["username"]; ok {
form.Username = cast.ToString(v)
}
if v, ok := requestData["email"]; ok {
form.Email = cast.ToString(v)
}
if v, ok := requestData["emailVisibility"]; ok {
form.EmailVisibility = cast.ToBool(v)
}
if v, ok := requestData["verified"]; ok {
form.Verified = cast.ToBool(v)
}
if v, ok := requestData["password"]; ok {
form.Password = cast.ToString(v)
}
if v, ok := requestData["passwordConfirm"]; ok {
form.PasswordConfirm = cast.ToString(v)
}
if v, ok := requestData["oldPassword"]; ok {
form.OldPassword = cast.ToString(v)
}
}
// extend the record schema data with the request data
extendedData := form.record.SchemaData()
2022-07-06 23:19:05 +02:00
rawData, err := json.Marshal(requestData)
if err != nil {
return err
}
if err := json.Unmarshal(rawData, &extendedData); err != nil {
return err
}
for _, field := range form.record.Collection().Schema.Fields() {
key := field.Name
value := extendedData[key]
2022-07-06 23:19:05 +02:00
value = field.PrepareValue(value)
if field.Type != schema.FieldTypeFile {
form.Data[key] = value
continue
}
options, _ := field.Options.(*schema.FileOptions)
oldNames := list.ToUniqueStringSlice(form.Data[key])
// -----------------------------------------------------------
// Delete previously uploaded file(s)
// -----------------------------------------------------------
// if empty value was set, mark all previously uploaded files for deletion
if len(list.ToUniqueStringSlice(value)) == 0 && len(oldNames) > 0 {
form.filesToDelete = append(form.filesToDelete, oldNames...)
form.Data[key] = []string{}
} else if len(oldNames) > 0 {
indexesToDelete := make([]int, 0, len(extendedData))
2022-07-06 23:19:05 +02:00
// search for individual file name to delete (eg. "file.test.png = null")
for i, name := range oldNames {
if v, ok := extendedData[key+"."+name]; ok && cast.ToString(v) == "" {
indexesToDelete = append(indexesToDelete, i)
}
}
// search for individual file index to delete (eg. "file.0 = null")
keyExp, _ := regexp.Compile(`^` + regexp.QuoteMeta(key) + `\.\d+$`)
for indexedKey := range extendedData {
if keyExp.MatchString(indexedKey) && cast.ToString(extendedData[indexedKey]) == "" {
index, indexErr := strconv.Atoi(indexedKey[len(key)+1:])
if indexErr != nil || index >= len(oldNames) {
2022-07-06 23:19:05 +02:00
continue
}
indexesToDelete = append(indexesToDelete, index)
2022-07-06 23:19:05 +02:00
}
}
// slice to fill only with the non-deleted indexes
nonDeleted := make([]string, 0, len(oldNames))
for i, name := range oldNames {
// not marked for deletion
if !list.ExistInSlice(i, indexesToDelete) {
nonDeleted = append(nonDeleted, name)
continue
}
// store the id to actually delete the file later
form.filesToDelete = append(form.filesToDelete, name)
2022-07-06 23:19:05 +02:00
}
form.Data[key] = nonDeleted
}
2022-07-06 23:19:05 +02:00
// -----------------------------------------------------------
// Check for new uploaded file
// -----------------------------------------------------------
2022-10-30 10:28:14 +02:00
if len(form.filesToUpload[key]) == 0 {
continue
}
2022-07-06 23:19:05 +02:00
// refresh oldNames list
oldNames = list.ToUniqueStringSlice(form.Data[key])
if options.MaxSelect == 1 {
// delete previous file(s) before replacing
if len(oldNames) > 0 {
form.filesToDelete = list.ToUniqueStringSlice(append(form.filesToDelete, oldNames...))
2022-07-06 23:19:05 +02:00
}
2022-10-30 10:28:14 +02:00
form.Data[key] = form.filesToUpload[key][0].Name()
} else if options.MaxSelect > 1 {
// append the id of each uploaded file instance
2022-10-30 10:28:14 +02:00
for _, file := range form.filesToUpload[key] {
oldNames = append(oldNames, file.Name())
}
form.Data[key] = oldNames
2022-07-06 23:19:05 +02:00
}
}
return form.normalizeData()
}
// Validate makes the form validatable by implementing [validation.Validatable] interface.
func (form *RecordUpsert) Validate() error {
2022-08-06 21:16:58 +02:00
// base form fields validator
2022-10-30 10:28:14 +02:00
baseFieldsRules := []*validation.FieldRules{
2022-08-06 21:16:58 +02:00
validation.Field(
&form.Id,
validation.When(
form.record.IsNew(),
validation.Length(models.DefaultIdLength, models.DefaultIdLength),
2022-08-11 09:29:01 +02:00
validation.Match(idRegex),
2022-08-06 21:16:58 +02:00
).Else(validation.In(form.record.Id)),
),
2022-10-30 10:28:14 +02:00
}
// auth fields validators
if form.record.Collection().IsAuth() {
baseFieldsRules = append(baseFieldsRules,
validation.Field(
&form.Username,
// require only on update, because on create we fallback to auto generated username
validation.When(!form.record.IsNew(), validation.Required),
validation.Length(3, 100),
2022-10-30 10:28:14 +02:00
validation.Match(usernameRegex),
validation.By(form.checkUniqueUsername),
),
validation.Field(
&form.Email,
validation.When(
form.record.Collection().AuthOptions().RequireEmail,
validation.Required,
),
// don't allow direct email change (or unset) if the form doesn't have manage access permissions
// (aka. allow only admin or authorized auth models to directly update the field)
validation.When(
!form.record.IsNew() && !form.manageAccess,
validation.In(form.record.Email()),
),
validation.Length(1, 255),
is.EmailFormat,
validation.By(form.checkEmailDomain),
validation.By(form.checkUniqueEmail),
),
validation.Field(
&form.Verified,
// don't allow changing verified if the form doesn't have manage access permissions
// (aka. allow only admin or authorized auth models to directly change the field)
validation.When(
!form.manageAccess,
validation.In(form.record.Verified()),
),
),
validation.Field(
&form.Password,
validation.When(form.record.IsNew(), validation.Required),
validation.Length(form.record.Collection().AuthOptions().MinPasswordLength, 72),
),
validation.Field(
&form.PasswordConfirm,
validation.When(
(form.record.IsNew() || form.Password != ""),
validation.Required,
),
validation.By(validators.Compare(form.Password)),
),
validation.Field(
&form.OldPassword,
// require old password only on update when:
// - form.manageAccess is not set
// - changing the existing password
validation.When(
!form.record.IsNew() && !form.manageAccess && form.Password != "",
validation.Required,
validation.By(form.checkOldPassword),
),
),
)
}
if err := validation.ValidateStruct(form, baseFieldsRules...); err != nil {
return err
2022-08-06 21:16:58 +02:00
}
// record data validator
2022-10-30 10:28:14 +02:00
return validators.NewRecordDataValidator(
form.dao,
2022-07-06 23:19:05 +02:00
form.record,
form.filesToUpload,
2022-10-30 10:28:14 +02:00
).Validate(form.Data)
}
func (form *RecordUpsert) checkUniqueUsername(value any) error {
v, _ := value.(string)
if v == "" {
return nil
}
isUnique := form.dao.IsRecordValueUnique(
form.record.Collection().Id,
schema.FieldNameUsername,
v,
form.record.Id,
2022-07-06 23:19:05 +02:00
)
2022-10-30 10:28:14 +02:00
if !isUnique {
return validation.NewError("validation_invalid_username", "The username is invalid or already in use.")
}
2022-07-06 23:19:05 +02:00
2022-10-30 10:28:14 +02:00
return nil
2022-07-06 23:19:05 +02:00
}
2022-10-30 10:28:14 +02:00
func (form *RecordUpsert) checkUniqueEmail(value any) error {
v, _ := value.(string)
if v == "" {
return nil
}
isUnique := form.dao.IsRecordValueUnique(
form.record.Collection().Id,
schema.FieldNameEmail,
v,
form.record.Id,
)
if !isUnique {
return validation.NewError("validation_invalid_email", "The email is invalid or already in use.")
}
return nil
}
func (form *RecordUpsert) checkEmailDomain(value any) error {
val, _ := value.(string)
if val == "" {
return nil // nothing to check
}
domain := val[strings.LastIndex(val, "@")+1:]
only := form.record.Collection().AuthOptions().OnlyEmailDomains
except := form.record.Collection().AuthOptions().ExceptEmailDomains
// only domains check
if len(only) > 0 && !list.ExistInSlice(domain, only) {
return validation.NewError("validation_email_domain_not_allowed", "Email domain is not allowed.")
}
// except domains check
if len(except) > 0 && list.ExistInSlice(domain, except) {
return validation.NewError("validation_email_domain_not_allowed", "Email domain is not allowed.")
}
return nil
}
func (form *RecordUpsert) checkOldPassword(value any) error {
v, _ := value.(string)
if v == "" {
return nil // nothing to check
}
if !form.record.ValidatePassword(v) {
return validation.NewError("validation_invalid_old_password", "Missing or invalid old password.")
}
return nil
}
func (form *RecordUpsert) ValidateAndFill() error {
2022-07-06 23:19:05 +02:00
if err := form.Validate(); err != nil {
return err
}
2022-08-08 18:16:33 +02:00
isNew := form.record.IsNew()
2022-08-06 21:16:58 +02:00
// custom insertion id can be set only on create
2022-08-08 18:16:33 +02:00
if isNew && form.Id != "" {
2022-08-06 21:16:58 +02:00
form.record.SetId(form.Id)
2022-10-30 10:28:14 +02:00
form.record.MarkAsNew()
}
// set auth fields
if form.record.Collection().IsAuth() {
// generate a default username during create (if missing)
if form.record.IsNew() && form.Username == "" {
baseUsername := form.record.Collection().Name + security.RandomStringWithAlphabet(5, "123456789")
form.Username = form.dao.SuggestUniqueAuthRecordUsername(form.record.Collection().Id, baseUsername)
}
if form.Username != "" {
if err := form.record.SetUsername(form.Username); err != nil {
return err
}
}
if isNew || form.manageAccess {
if err := form.record.SetEmail(form.Email); err != nil {
return err
}
}
if err := form.record.SetEmailVisibility(form.EmailVisibility); err != nil {
return err
}
if form.manageAccess {
if err := form.record.SetVerified(form.Verified); err != nil {
return err
}
}
if form.Password != "" {
if err := form.record.SetPassword(form.Password); err != nil {
return err
}
}
2022-08-06 21:16:58 +02:00
}
2022-10-30 10:28:14 +02:00
// bulk load the remaining form data
form.record.Load(form.Data)
return nil
}
// DrySubmit performs a form submit within a transaction and reverts it.
// For actual record persistence, check the `form.Submit()` method.
//
// This method doesn't handle file uploads/deletes or trigger any app events!
func (form *RecordUpsert) DrySubmit(callback func(txDao *daos.Dao) error) error {
isNew := form.record.IsNew()
if err := form.ValidateAndFill(); err != nil {
2022-07-06 23:19:05 +02:00
return err
}
2022-10-30 10:28:14 +02:00
// use the default app.Dao to prevent changing the transaction form.Dao
// and causing "transaction has already been committed or rolled back" error
return form.app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
2022-07-06 23:19:05 +02:00
tx, ok := txDao.DB().(*dbx.Tx)
if !ok {
return errors.New("failed to get transaction db")
}
defer tx.Rollback()
2022-08-08 18:16:33 +02:00
2022-07-06 23:19:05 +02:00
txDao.BeforeCreateFunc = nil
txDao.AfterCreateFunc = nil
txDao.BeforeUpdateFunc = nil
txDao.AfterUpdateFunc = nil
if err := txDao.SaveRecord(form.record); err != nil {
return err
}
2022-08-08 18:16:33 +02:00
// restore record isNew state
if isNew {
form.record.MarkAsNew()
}
if callback != nil {
return callback(txDao)
}
return nil
2022-07-06 23:19:05 +02:00
})
}
// Submit validates the form and upserts the form Record model.
//
// You can optionally provide a list of InterceptorFunc to further
// modify the form behavior before persisting it.
func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc) error {
2022-10-30 10:28:14 +02:00
if err := form.ValidateAndFill(); err != nil {
2022-07-06 23:19:05 +02:00
return err
}
return runInterceptors(func() error {
if !form.record.HasId() {
form.record.RefreshId()
form.record.MarkAsNew()
}
2022-07-06 23:19:05 +02:00
// upload new files (if any)
if err := form.processFilesToUpload(); err != nil {
return fmt.Errorf("failed to process the uploaded files: %w", err)
}
2022-07-06 23:19:05 +02:00
// persist the record model
if saveErr := form.dao.SaveRecord(form.record); saveErr != nil {
// try to cleanup the successfully uploaded files
if _, err := form.deleteFilesByNamesList(form.getFilesToUploadNames()); err != nil && form.app.IsDebug() {
log.Println(err)
}
2022-07-06 23:19:05 +02:00
return fmt.Errorf("failed to save the record: %w", saveErr)
}
// delete old files (if any)
//
// for now fail silently to avoid reupload when `form.Submit()`
// is called manually (aka. not from an api request)...
if err := form.processFilesToDelete(); err != nil && form.app.IsDebug() {
log.Println(err)
}
return nil
}, interceptors...)
2022-07-06 23:19:05 +02:00
}
func (form *RecordUpsert) getFilesToUploadNames() []string {
names := []string{}
for fieldKey := range form.filesToUpload {
for _, file := range form.filesToUpload[fieldKey] {
names = append(names, file.Name())
}
}
return names
}
2022-07-06 23:19:05 +02:00
func (form *RecordUpsert) processFilesToUpload() error {
if len(form.filesToUpload) == 0 {
2022-10-30 10:28:14 +02:00
return nil // no parsed file fields
2022-07-06 23:19:05 +02:00
}
if !form.record.HasId() {
return errors.New("the record is not persisted yet")
2022-07-06 23:19:05 +02:00
}
2022-10-30 10:28:14 +02:00
fs, err := form.app.NewFilesystem()
2022-07-06 23:19:05 +02:00
if err != nil {
return err
}
defer fs.Close()
var uploadErrors []error // list of upload errors
var uploaded []string // list of uploaded file paths
2022-07-06 23:19:05 +02:00
2022-10-30 10:28:14 +02:00
for fieldKey := range form.filesToUpload {
for i, file := range form.filesToUpload[fieldKey] {
2022-10-30 10:28:14 +02:00
path := form.record.BaseFilesPath() + "/" + file.Name()
if err := fs.UploadMultipart(file.Header(), path); err == nil {
// keep track of the already uploaded file
uploaded = append(uploaded, path)
2022-10-30 10:28:14 +02:00
} else {
// store the upload error
uploadErrors = append(uploadErrors, fmt.Errorf("file %d: %v", i, err))
2022-10-30 10:28:14 +02:00
}
2022-07-06 23:19:05 +02:00
}
}
if len(uploadErrors) > 0 {
// cleanup - try to delete the successfully uploaded files (if any)
form.deleteFilesByNamesList(uploaded)
return fmt.Errorf("failed to upload all files: %v", uploadErrors)
2022-07-06 23:19:05 +02:00
}
return nil
}
func (form *RecordUpsert) processFilesToDelete() (err error) {
form.filesToDelete, err = form.deleteFilesByNamesList(form.filesToDelete)
return
}
// deleteFiles deletes a list of record files by their names.
// Returns the failed/remaining files.
func (form *RecordUpsert) deleteFilesByNamesList(filenames []string) ([]string, error) {
if len(filenames) == 0 {
return filenames, nil // nothing to delete
2022-07-06 23:19:05 +02:00
}
if !form.record.HasId() {
return filenames, errors.New("the record doesn't have a unique ID")
2022-07-06 23:19:05 +02:00
}
2022-10-30 10:28:14 +02:00
fs, err := form.app.NewFilesystem()
2022-07-06 23:19:05 +02:00
if err != nil {
return filenames, err
2022-07-06 23:19:05 +02:00
}
defer fs.Close()
var deleteErrors []error
for i := len(filenames) - 1; i >= 0; i-- {
filename := filenames[i]
2022-07-06 23:19:05 +02:00
path := form.record.BaseFilesPath() + "/" + filename
if err := fs.Delete(path); err == nil {
// remove the deleted file from the list
filenames = append(filenames[:i], filenames[i+1:]...)
// try to delete the related file thumbs (if any)
fs.DeletePrefix(form.record.BaseFilesPath() + "/thumbs_" + filename + "/")
} else {
// store the delete error
deleteErrors = append(deleteErrors, fmt.Errorf("file %d: %v", i, err))
2022-07-06 23:19:05 +02:00
}
}
if len(deleteErrors) > 0 {
return filenames, fmt.Errorf("failed to delete all files: %v", deleteErrors)
2022-07-06 23:19:05 +02:00
}
return filenames, nil
2022-07-06 23:19:05 +02:00
}