[#1194] refactored forms.RecordUpsert to allow easier file upload
This commit is contained in:
		
							parent
							
								
									972b06c708
								
							
						
					
					
						commit
						707f35f461
					
				
							
								
								
									
										22
									
								
								CHANGELOG.md
								
								
								
								
							
							
						
						
									
										22
									
								
								CHANGELOG.md
								
								
								
								
							| 
						 | 
				
			
			@ -1,3 +1,25 @@
 | 
			
		|||
## (WIP) v0.10.0
 | 
			
		||||
 | 
			
		||||
- Removed `rest.UploadedFile` struct (see below `filesystem.File`).
 | 
			
		||||
 | 
			
		||||
- Added generic file resource struct that allows loading and uploading file content from
 | 
			
		||||
  different sources (at the moment multipart/formdata requests and from the local filesystem).
 | 
			
		||||
  ```
 | 
			
		||||
  filesystem.File{}
 | 
			
		||||
  filesystem.NewFileFromPath(path)
 | 
			
		||||
  filesystem.NewFileFromMultipart(multipartHeader)
 | 
			
		||||
  filesystem/System.UploadFile(file)
 | 
			
		||||
  ```
 | 
			
		||||
 | 
			
		||||
- Refactored `forms.RecordUpsert` to allow more easily loading and removing files programmatically.
 | 
			
		||||
  ```
 | 
			
		||||
  forms.RecordUpsert.LoadFiles(key, filesystem.File...) // add new filesystem.File to the form for upload
 | 
			
		||||
  forms.RecordUpsert.RemoveFiles(key, filenames...)     // marks the filenames for deletion
 | 
			
		||||
  ```
 | 
			
		||||
 | 
			
		||||
- Optimized memory allocations (~20% improvement).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## v0.9.2
 | 
			
		||||
 | 
			
		||||
- Fixed field column name conflict on record deletion ([#1220](https://github.com/pocketbase/pocketbase/discussions/1220)).
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -52,7 +52,7 @@ type RecordUpsert struct {
 | 
			
		|||
	OldPassword     string `json:"oldPassword"`
 | 
			
		||||
	// ---
 | 
			
		||||
 | 
			
		||||
	Data map[string]any `json:"data"`
 | 
			
		||||
	data map[string]any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewRecordUpsert creates a new [RecordUpsert] form with initializer
 | 
			
		||||
| 
						 | 
				
			
			@ -75,6 +75,11 @@ func NewRecordUpsert(app core.App, record *models.Record) *RecordUpsert {
 | 
			
		|||
	return form
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Data returns the loaded form's data.
 | 
			
		||||
func (form *RecordUpsert) Data() map[string]any {
 | 
			
		||||
	return form.data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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).
 | 
			
		||||
| 
						 | 
				
			
			@ -97,9 +102,9 @@ func (form *RecordUpsert) loadFormDefaults() {
 | 
			
		|||
		form.Verified = form.record.Verified()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	form.Data = map[string]any{}
 | 
			
		||||
	form.data = map[string]any{}
 | 
			
		||||
	for _, field := range form.record.Collection().Schema.Fields() {
 | 
			
		||||
		form.Data[field.Name] = form.record.Get(field.Name)
 | 
			
		||||
		form.data[field.Name] = form.record.Get(field.Name)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -113,49 +118,56 @@ func (form *RecordUpsert) getContentType(r *http.Request) string {
 | 
			
		|||
	return t
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (form *RecordUpsert) extractRequestData(r *http.Request, keyPrefix string) (map[string]any, error) {
 | 
			
		||||
func (form *RecordUpsert) extractRequestData(
 | 
			
		||||
	r *http.Request,
 | 
			
		||||
	keyPrefix string,
 | 
			
		||||
) (map[string]any, map[string][]*filesystem.File, error) {
 | 
			
		||||
	switch form.getContentType(r) {
 | 
			
		||||
	case "application/json":
 | 
			
		||||
		return form.extractJsonData(r, keyPrefix)
 | 
			
		||||
	case "multipart/form-data":
 | 
			
		||||
		return form.extractMultipartFormData(r, keyPrefix)
 | 
			
		||||
	default:
 | 
			
		||||
		return nil, errors.New("Unsupported request Content-Type.")
 | 
			
		||||
		return nil, nil, errors.New("unsupported request content-type")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (form *RecordUpsert) extractJsonData(r *http.Request, keyPrefix string) (map[string]any, error) {
 | 
			
		||||
	result := map[string]any{}
 | 
			
		||||
func (form *RecordUpsert) extractJsonData(
 | 
			
		||||
	r *http.Request,
 | 
			
		||||
	keyPrefix string,
 | 
			
		||||
) (map[string]any, map[string][]*filesystem.File, error) {
 | 
			
		||||
	data := map[string]any{}
 | 
			
		||||
 | 
			
		||||
	err := rest.CopyJsonBody(r, &result)
 | 
			
		||||
	err := rest.CopyJsonBody(r, &data)
 | 
			
		||||
 | 
			
		||||
	if keyPrefix != "" {
 | 
			
		||||
		parts := strings.Split(keyPrefix, ".")
 | 
			
		||||
		for _, part := range parts {
 | 
			
		||||
			if result[part] == nil {
 | 
			
		||||
			if data[part] == nil {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			if v, ok := result[part].(map[string]any); ok {
 | 
			
		||||
				result = v
 | 
			
		||||
			if v, ok := data[part].(map[string]any); ok {
 | 
			
		||||
				data = v
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return result, err
 | 
			
		||||
	return data, nil, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (form *RecordUpsert) extractMultipartFormData(r *http.Request, keyPrefix string) (map[string]any, error) {
 | 
			
		||||
	result := map[string]any{}
 | 
			
		||||
 | 
			
		||||
func (form *RecordUpsert) extractMultipartFormData(
 | 
			
		||||
	r *http.Request,
 | 
			
		||||
	keyPrefix string,
 | 
			
		||||
) (map[string]any, map[string][]*filesystem.File, error) {
 | 
			
		||||
	// parse form data (if not already)
 | 
			
		||||
	if err := r.ParseMultipartForm(rest.DefaultMaxMemory); err != nil {
 | 
			
		||||
		return result, err
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data := map[string]any{}
 | 
			
		||||
	filesToUpload := map[string][]*filesystem.File{}
 | 
			
		||||
	arrayValueSupportTypes := schema.ArraybleFieldTypes()
 | 
			
		||||
 | 
			
		||||
	form.filesToUpload = map[string][]*filesystem.File{}
 | 
			
		||||
 | 
			
		||||
	for fullKey, values := range r.PostForm {
 | 
			
		||||
		key := fullKey
 | 
			
		||||
		if keyPrefix != "" {
 | 
			
		||||
| 
						 | 
				
			
			@ -163,15 +175,15 @@ func (form *RecordUpsert) extractMultipartFormData(r *http.Request, keyPrefix st
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		if len(values) == 0 {
 | 
			
		||||
			result[key] = nil
 | 
			
		||||
			data[key] = nil
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		field := form.record.Collection().Schema.GetFieldByName(key)
 | 
			
		||||
		if field != nil && list.ExistInSlice(field.Type, arrayValueSupportTypes) {
 | 
			
		||||
			result[key] = values
 | 
			
		||||
			data[key] = values
 | 
			
		||||
		} else {
 | 
			
		||||
			result[key] = values[0]
 | 
			
		||||
			data[key] = values[0]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -197,32 +209,10 @@ func (form *RecordUpsert) extractMultipartFormData(r *http.Request, keyPrefix st
 | 
			
		|||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		options, ok := field.Options.(*schema.FileOptions)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if form.filesToUpload[key] == nil {
 | 
			
		||||
			form.filesToUpload[key] = []*filesystem.File{}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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...)
 | 
			
		||||
		}
 | 
			
		||||
		filesToUpload[key] = append(filesToUpload[key], files...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
	return data, filesToUpload, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadRequest extracts the json or multipart/form-data request data
 | 
			
		||||
| 
						 | 
				
			
			@ -235,15 +225,126 @@ func (form *RecordUpsert) normalizeData() error {
 | 
			
		|||
// 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) LoadRequest(r *http.Request, keyPrefix string) error {
 | 
			
		||||
	requestData, err := form.extractRequestData(r, keyPrefix)
 | 
			
		||||
	requestData, uploadedFiles, err := form.extractRequestData(r, keyPrefix)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return form.LoadData(requestData)
 | 
			
		||||
	if err := form.LoadData(requestData); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for key, files := range uploadedFiles {
 | 
			
		||||
		form.AddFiles(key, files...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadData loads and normalizes the provided data into the form.
 | 
			
		||||
// AddFiles adds the provided file(s) to the specified file field.
 | 
			
		||||
//
 | 
			
		||||
// If the file field is a SINGLE-value file field (aka. "Max Select = 1"),
 | 
			
		||||
// then the newly added file will REPLACE the existing one.
 | 
			
		||||
// In this case if you pass more than 1 files only the first one will be assigned.
 | 
			
		||||
//
 | 
			
		||||
// If the file field is a MULTI-value file field (aka. "Max Select > 1"),
 | 
			
		||||
// then the newly added file(s) will be APPENDED to the existing one(s).
 | 
			
		||||
//
 | 
			
		||||
// Example
 | 
			
		||||
//
 | 
			
		||||
// 	f1, _ := filesystem.NewFileFromPath("/path/to/file1.txt")
 | 
			
		||||
// 	f2, _ := filesystem.NewFileFromPath("/path/to/file2.txt")
 | 
			
		||||
// 	form.AddFiles("documents", f1, f2)
 | 
			
		||||
func (form *RecordUpsert) AddFiles(key string, files ...*filesystem.File) error {
 | 
			
		||||
	field := form.record.Collection().Schema.GetFieldByName(key)
 | 
			
		||||
	if field == nil || field.Type != schema.FieldTypeFile {
 | 
			
		||||
		return errors.New("invalid field key")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	options, ok := field.Options.(*schema.FileOptions)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return errors.New("failed to initilize field options")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(files) == 0 {
 | 
			
		||||
		return nil // nothing to upload
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if form.filesToUpload == nil {
 | 
			
		||||
		form.filesToUpload = map[string][]*filesystem.File{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	oldNames := list.ToUniqueStringSlice(form.data[key])
 | 
			
		||||
 | 
			
		||||
	if options.MaxSelect == 1 {
 | 
			
		||||
		// mark previous file(s) for deletion before replacing
 | 
			
		||||
		if len(oldNames) > 0 {
 | 
			
		||||
			form.filesToDelete = list.ToUniqueStringSlice(append(form.filesToDelete, oldNames...))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// replace
 | 
			
		||||
		form.filesToUpload[key] = []*filesystem.File{files[0]}
 | 
			
		||||
		form.data[key] = field.PrepareValue(files[0].Name)
 | 
			
		||||
	} else {
 | 
			
		||||
		// append
 | 
			
		||||
		form.filesToUpload[key] = append(form.filesToUpload[key], files...)
 | 
			
		||||
		for _, f := range files {
 | 
			
		||||
			oldNames = append(oldNames, f.Name)
 | 
			
		||||
		}
 | 
			
		||||
		form.data[key] = field.PrepareValue(oldNames)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RemoveFiles removes a single or multiple file from the specified file field.
 | 
			
		||||
//
 | 
			
		||||
// NB! If filesToDelete is not set it will remove all existing files
 | 
			
		||||
// assigned to the file field (including those assigned with AddFiles)!
 | 
			
		||||
//
 | 
			
		||||
// Example
 | 
			
		||||
//
 | 
			
		||||
//  // mark only only 2 files for removal
 | 
			
		||||
// 	form.AddFiles("documents", "file1_aw4bdrvws6.txt", "file2_xwbs36bafv.txt")
 | 
			
		||||
//
 | 
			
		||||
// 	// mark all "documents" files for removal
 | 
			
		||||
// 	form.AddFiles("documents")
 | 
			
		||||
func (form *RecordUpsert) RemoveFiles(key string, toDelete ...string) error {
 | 
			
		||||
	field := form.record.Collection().Schema.GetFieldByName(key)
 | 
			
		||||
	if field == nil || field.Type != schema.FieldTypeFile {
 | 
			
		||||
		return errors.New("invalid field key")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	existing := list.ToUniqueStringSlice(form.data[key])
 | 
			
		||||
 | 
			
		||||
	// mark all files for deletion
 | 
			
		||||
	if len(toDelete) == 0 {
 | 
			
		||||
		toDelete = make([]string, len(existing))
 | 
			
		||||
		copy(toDelete, existing)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check for existing files
 | 
			
		||||
	for i := len(existing) - 1; i >= 0; i-- {
 | 
			
		||||
		if list.ExistInSlice(existing[i], toDelete) {
 | 
			
		||||
			form.filesToDelete = append(form.filesToDelete, existing[i])
 | 
			
		||||
			existing = append(existing[:i], existing[i+1:]...)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check for newly uploaded files
 | 
			
		||||
	for i := len(form.filesToUpload[key]) - 1; i >= 0; i-- {
 | 
			
		||||
		f := form.filesToUpload[key][i]
 | 
			
		||||
		if list.ExistInSlice(f.Name, toDelete) {
 | 
			
		||||
			form.filesToUpload[key] = append(form.filesToUpload[key][:i], form.filesToUpload[key][i+1:]...)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	form.data[key] = field.PrepareValue(existing)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadData loads and normalizes the provided regular record data fields 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.
 | 
			
		||||
| 
						 | 
				
			
			@ -296,28 +397,26 @@ func (form *RecordUpsert) LoadData(requestData map[string]any) error {
 | 
			
		|||
		value = field.PrepareValue(value)
 | 
			
		||||
 | 
			
		||||
		if field.Type != schema.FieldTypeFile {
 | 
			
		||||
			form.Data[key] = value
 | 
			
		||||
			form.data[key] = value
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		options, _ := field.Options.(*schema.FileOptions)
 | 
			
		||||
		oldNames := list.ToUniqueStringSlice(form.Data[key])
 | 
			
		||||
 | 
			
		||||
		// -----------------------------------------------------------
 | 
			
		||||
		// Delete previously uploaded file(s)
 | 
			
		||||
		// -----------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
		oldNames := list.ToUniqueStringSlice(form.data[key])
 | 
			
		||||
 | 
			
		||||
		// 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{}
 | 
			
		||||
			form.RemoveFiles(key)
 | 
			
		||||
		} else if len(oldNames) > 0 {
 | 
			
		||||
			indexesToDelete := make([]int, 0, len(extendedData))
 | 
			
		||||
			toDelete := []string{}
 | 
			
		||||
 | 
			
		||||
			// search for individual file name to delete (eg. "file.test.png = null")
 | 
			
		||||
			for i, name := range oldNames {
 | 
			
		||||
			for _, name := range oldNames {
 | 
			
		||||
				if v, ok := extendedData[key+"."+name]; ok && cast.ToString(v) == "" {
 | 
			
		||||
					indexesToDelete = append(indexesToDelete, i)
 | 
			
		||||
					toDelete = append(toDelete, name)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -329,52 +428,17 @@ func (form *RecordUpsert) LoadData(requestData map[string]any) error {
 | 
			
		|||
					if indexErr != nil || index >= len(oldNames) {
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
					indexesToDelete = append(indexesToDelete, index)
 | 
			
		||||
					toDelete = append(toDelete, oldNames[index])
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 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)
 | 
			
		||||
			if len(toDelete) > 0 {
 | 
			
		||||
				form.RemoveFiles(key, toDelete...)
 | 
			
		||||
			}
 | 
			
		||||
			form.Data[key] = nonDeleted
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// -----------------------------------------------------------
 | 
			
		||||
		// Check for new uploaded file
 | 
			
		||||
		// -----------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
		if len(form.filesToUpload[key]) == 0 {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 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...))
 | 
			
		||||
			}
 | 
			
		||||
			form.Data[key] = form.filesToUpload[key][0].Name
 | 
			
		||||
		} else if options.MaxSelect > 1 {
 | 
			
		||||
			// append the id of each uploaded file instance
 | 
			
		||||
			for _, file := range form.filesToUpload[key] {
 | 
			
		||||
				oldNames = append(oldNames, file.Name)
 | 
			
		||||
			}
 | 
			
		||||
			form.Data[key] = oldNames
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return form.normalizeData()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Validate makes the form validatable by implementing [validation.Validatable] interface.
 | 
			
		||||
| 
						 | 
				
			
			@ -464,7 +528,7 @@ func (form *RecordUpsert) Validate() error {
 | 
			
		|||
		form.dao,
 | 
			
		||||
		form.record,
 | 
			
		||||
		form.filesToUpload,
 | 
			
		||||
	).Validate(form.Data)
 | 
			
		||||
	).Validate(form.data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (form *RecordUpsert) checkUniqueUsername(value any) error {
 | 
			
		||||
| 
						 | 
				
			
			@ -592,7 +656,7 @@ func (form *RecordUpsert) ValidateAndFill() error {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	// bulk load the remaining form data
 | 
			
		||||
	form.record.Load(form.Data)
 | 
			
		||||
	form.record.Load(form.data)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ import (
 | 
			
		|||
	"errors"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httptest"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +17,7 @@ import (
 | 
			
		|||
	"github.com/pocketbase/pocketbase/forms"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/models"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/tests"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/tools/filesystem"
 | 
			
		||||
	"github.com/pocketbase/pocketbase/tools/list"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -44,9 +46,9 @@ func TestNewRecordUpsert(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
	form := forms.NewRecordUpsert(app, record)
 | 
			
		||||
 | 
			
		||||
	val := form.Data["title"]
 | 
			
		||||
	val := form.Data()["title"]
 | 
			
		||||
	if val != "test_value" {
 | 
			
		||||
		t.Errorf("Expected record data to be loaded, got %v", form.Data)
 | 
			
		||||
		t.Errorf("Expected record data to be loaded, got %v", form.Data())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -107,15 +109,15 @@ func TestRecordUpsertLoadRequestJson(t *testing.T) {
 | 
			
		|||
		t.Fatalf("Expect id field to be %q, got %q", "test_id", form.Id)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if v, ok := form.Data["text"]; !ok || v != "test123" {
 | 
			
		||||
	if v, ok := form.Data()["text"]; !ok || v != "test123" {
 | 
			
		||||
		t.Fatalf("Expect title field to be %q, got %q", "test123", v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if v, ok := form.Data["unknown"]; ok {
 | 
			
		||||
	if v, ok := form.Data()["unknown"]; ok {
 | 
			
		||||
		t.Fatalf("Didn't expect unknown field to be set, got %v", v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileOne, ok := form.Data["file_one"]
 | 
			
		||||
	fileOne, ok := form.Data()["file_one"]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Fatal("Expect file_one field to be set")
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -123,7 +125,7 @@ func TestRecordUpsertLoadRequestJson(t *testing.T) {
 | 
			
		|||
		t.Fatalf("Expect file_one field to be empty string, got %v", fileOne)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileMany, ok := form.Data["file_many"]
 | 
			
		||||
	fileMany, ok := form.Data()["file_many"]
 | 
			
		||||
	if !ok || fileMany == nil {
 | 
			
		||||
		t.Fatal("Expect file_many field to be set")
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -168,15 +170,15 @@ func TestRecordUpsertLoadRequestMultipart(t *testing.T) {
 | 
			
		|||
		t.Fatalf("Expect id field to be %q, got %q", "test_id", form.Id)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if v, ok := form.Data["text"]; !ok || v != "test123" {
 | 
			
		||||
	if v, ok := form.Data()["text"]; !ok || v != "test123" {
 | 
			
		||||
		t.Fatalf("Expect text field to be %q, got %q", "test123", v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if v, ok := form.Data["unknown"]; ok {
 | 
			
		||||
	if v, ok := form.Data()["unknown"]; ok {
 | 
			
		||||
		t.Fatalf("Didn't expect unknown field to be set, got %v", v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileOne, ok := form.Data["file_one"]
 | 
			
		||||
	fileOne, ok := form.Data()["file_one"]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Fatal("Expect file_one field to be set")
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -184,7 +186,7 @@ func TestRecordUpsertLoadRequestMultipart(t *testing.T) {
 | 
			
		|||
		t.Fatalf("Expect file_one field to be empty string, got %v", fileOne)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileMany, ok := form.Data["file_many"]
 | 
			
		||||
	fileMany, ok := form.Data()["file_many"]
 | 
			
		||||
	if !ok || fileMany == nil {
 | 
			
		||||
		t.Fatal("Expect file_many field to be set")
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -214,11 +216,11 @@ func TestRecordUpsertLoadData(t *testing.T) {
 | 
			
		|||
		t.Fatal(loadErr)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if v, ok := form.Data["title"]; !ok || v != "test_new" {
 | 
			
		||||
	if v, ok := form.Data()["title"]; !ok || v != "test_new" {
 | 
			
		||||
		t.Fatalf("Expect title field to be %v, got %v", "test_new", v)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if v, ok := form.Data["active"]; !ok || v != true {
 | 
			
		||||
	if v, ok := form.Data()["active"]; !ok || v != true {
 | 
			
		||||
		t.Fatalf("Expect active field to be %v, got %v", true, v)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -498,7 +500,7 @@ func TestRecordUpsertSubmitInterceptors(t *testing.T) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	form := forms.NewRecordUpsert(app, record)
 | 
			
		||||
	form.Data["title"] = "test_new"
 | 
			
		||||
	form.Data()["title"] = "test_new"
 | 
			
		||||
 | 
			
		||||
	testErr := errors.New("test_error")
 | 
			
		||||
	interceptorRecordTitle := ""
 | 
			
		||||
| 
						 | 
				
			
			@ -533,7 +535,7 @@ func TestRecordUpsertSubmitInterceptors(t *testing.T) {
 | 
			
		|||
		t.Fatalf("Expected interceptor2 to be called")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if interceptorRecordTitle != form.Data["title"].(string) {
 | 
			
		||||
	if interceptorRecordTitle != form.Data()["title"].(string) {
 | 
			
		||||
		t.Fatalf("Expected the form model to be filled before calling the interceptors")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -863,3 +865,75 @@ func TestRecordUpsertAuthRecord(t *testing.T) {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRecordUpsertAddAndRemoveFiles(t *testing.T) {
 | 
			
		||||
	app, _ := tests.NewTestApp()
 | 
			
		||||
	defer app.Cleanup()
 | 
			
		||||
 | 
			
		||||
	recordBefore, err := app.Dao().FindRecordById("demo1", "84nmscqy84lsi1t")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// create test temp files
 | 
			
		||||
	tempDir := filepath.Join(app.DataDir(), "temp")
 | 
			
		||||
	if err := os.MkdirAll(app.DataDir(), os.ModePerm); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer os.RemoveAll(tempDir)
 | 
			
		||||
	tmpFile, _ := os.CreateTemp(os.TempDir(), "tmpfile1-*.txt")
 | 
			
		||||
	tmpFile.Close()
 | 
			
		||||
 | 
			
		||||
	form := forms.NewRecordUpsert(app, recordBefore)
 | 
			
		||||
 | 
			
		||||
	f1, err := filesystem.NewFileFromPath(tmpFile.Name())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	f2, err := filesystem.NewFileFromPath(tmpFile.Name())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	f3, err := filesystem.NewFileFromPath(tmpFile.Name())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	form.AddFiles("file_one", f1) // should replace the existin file
 | 
			
		||||
 | 
			
		||||
	form.AddFiles("file_many", f2, f3) // should append
 | 
			
		||||
 | 
			
		||||
	form.RemoveFiles("file_many", "300_WlbFWSGmW9.png", "logo_vcfJJG5TAh.svg") // should remove
 | 
			
		||||
 | 
			
		||||
	if err := form.Submit(); err != nil {
 | 
			
		||||
		t.Fatalf("Failed to submit the RecordUpsert form, got %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	recordAfter, err := app.Dao().FindRecordById("demo1", "84nmscqy84lsi1t")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// ensure files deletion
 | 
			
		||||
	if hasRecordFile(app, recordAfter, "test_d61b33QdDU.txt") {
 | 
			
		||||
		t.Fatalf("Expected the old file_one file to be deleted")
 | 
			
		||||
	}
 | 
			
		||||
	if hasRecordFile(app, recordAfter, "300_WlbFWSGmW9.png") {
 | 
			
		||||
		t.Fatalf("Expected 300_WlbFWSGmW9.png to be deleted")
 | 
			
		||||
	}
 | 
			
		||||
	if hasRecordFile(app, recordAfter, "logo_vcfJJG5TAh.svg") {
 | 
			
		||||
		t.Fatalf("Expected logo_vcfJJG5TAh.svg to be deleted")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileOne := recordAfter.GetStringSlice("file_one")
 | 
			
		||||
	if len(fileOne) == 0 {
 | 
			
		||||
		t.Fatalf("Expected new file_one file to be uploaded")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileMany := recordAfter.GetStringSlice("file_many")
 | 
			
		||||
	if len(fileMany) != 3 {
 | 
			
		||||
		t.Fatalf("Expected file_many to be 3, got %v", fileMany)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,24 +29,32 @@ func MockMultipartData(data map[string]string, fileFields ...string) (*bytes.Buf
 | 
			
		|||
	// write file fields
 | 
			
		||||
	for _, fileField := range fileFields {
 | 
			
		||||
		// create a test temporary file
 | 
			
		||||
		tmpFile, err := os.CreateTemp(os.TempDir(), "tmpfile-*.txt")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
		if _, err := tmpFile.Write([]byte("test")); err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
		tmpFile.Seek(0, 0)
 | 
			
		||||
		defer tmpFile.Close()
 | 
			
		||||
		defer os.Remove(tmpFile.Name())
 | 
			
		||||
		err := func() error {
 | 
			
		||||
			tmpFile, err := os.CreateTemp(os.TempDir(), "tmpfile-*.txt")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		// stub uploaded file
 | 
			
		||||
		w, err := mp.CreateFormFile(fileField, tmpFile.Name())
 | 
			
		||||
			if _, err := tmpFile.Write([]byte("test")); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			tmpFile.Seek(0, 0)
 | 
			
		||||
			defer tmpFile.Close()
 | 
			
		||||
			defer os.Remove(tmpFile.Name())
 | 
			
		||||
 | 
			
		||||
			// stub uploaded file
 | 
			
		||||
			w, err := mp.CreateFormFile(fileField, tmpFile.Name())
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			if _, err := io.Copy(w, tmpFile); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return nil
 | 
			
		||||
		}()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, mp, err
 | 
			
		||||
		}
 | 
			
		||||
		if _, err := io.Copy(w, tmpFile); err != nil {
 | 
			
		||||
			return nil, mp, err
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue