1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-04-22 15:57:52 +02:00

[#1187] move file upload and delete out of the record save transaction

This commit is contained in:
Gani Georgiev 2022-12-06 12:26:29 +02:00
parent 808f5054d0
commit 355f7053fd
3 changed files with 77 additions and 47 deletions

View File

@ -317,14 +317,14 @@ func (dao *Dao) SuggestUniqueAuthRecordUsername(
func (dao *Dao) SaveRecord(record *models.Record) error { func (dao *Dao) SaveRecord(record *models.Record) error {
if record.Collection().IsAuth() { if record.Collection().IsAuth() {
if record.Username() == "" { if record.Username() == "" {
return errors.New("Unable to save auth record without username.") return errors.New("unable to save auth record without username")
} }
// Cross-check that the auth record id is unique for all auth collections. // Cross-check that the auth record id is unique for all auth collections.
// This is to make sure that the filter `@request.auth.id` always returns a unique id. // This is to make sure that the filter `@request.auth.id` always returns a unique id.
authCollections, err := dao.FindCollectionsByType(models.CollectionTypeAuth) authCollections, err := dao.FindCollectionsByType(models.CollectionTypeAuth)
if err != nil { if err != nil {
return fmt.Errorf("Unable to fetch the auth collections for cross-id unique check: %w", err) return fmt.Errorf("unable to fetch the auth collections for cross-id unique check: %w", err)
} }
for _, collection := range authCollections { for _, collection := range authCollections {
if record.Collection().Id == collection.Id { if record.Collection().Id == collection.Id {
@ -332,7 +332,7 @@ func (dao *Dao) SaveRecord(record *models.Record) error {
} }
isUnique := dao.IsRecordValueUnique(collection.Id, schema.FieldNameId, record.Id) isUnique := dao.IsRecordValueUnique(collection.Id, schema.FieldNameId, record.Id)
if !isUnique { if !isUnique {
return errors.New("The auth record ID must be unique across all auth collections.") return errors.New("the auth record ID must be unique across all auth collections")
} }
} }
} }

View File

@ -34,8 +34,8 @@ type RecordUpsert struct {
manageAccess bool manageAccess bool
record *models.Record record *models.Record
filesToDelete []string // names list
filesToUpload map[string][]*rest.UploadedFile filesToUpload map[string][]*rest.UploadedFile
filesToDelete []string // names list
// base model fields // base model fields
Id string `json:"id"` Id string `json:"id"`
@ -648,35 +648,57 @@ func (form *RecordUpsert) Submit(interceptors ...InterceptorFunc) error {
} }
return runInterceptors(func() error { return runInterceptors(func() error {
return form.dao.RunInTransaction(func(txDao *daos.Dao) error { if !form.record.HasId() {
// persist record model form.record.RefreshId()
if err := txDao.SaveRecord(form.record); err != nil { form.record.MarkAsNew()
return fmt.Errorf("Failed to save the record: %w", err)
} }
// upload new files (if any) // upload new files (if any)
if err := form.processFilesToUpload(); err != nil { if err := form.processFilesToUpload(); err != nil {
return fmt.Errorf("Failed to process the upload files: %w", err) return fmt.Errorf("failed to process the uploaded files: %w", err)
}
// 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)
}
return fmt.Errorf("failed to save the record: %w", saveErr)
} }
// delete old files (if any) // delete old files (if any)
if err := form.processFilesToDelete(); err != nil { //nolint:staticcheck //
// for now fail silently to avoid reupload when `form.Submit()` // for now fail silently to avoid reupload when `form.Submit()`
// is called manually (aka. not from an api request)... // is called manually (aka. not from an api request)...
if err := form.processFilesToDelete(); err != nil && form.app.IsDebug() {
log.Println(err)
} }
return nil return nil
})
}, interceptors...) }, interceptors...)
} }
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
}
func (form *RecordUpsert) processFilesToUpload() error { func (form *RecordUpsert) processFilesToUpload() error {
if len(form.filesToUpload) == 0 { if len(form.filesToUpload) == 0 {
return nil // no parsed file fields return nil // no parsed file fields
} }
if !form.record.HasId() { if !form.record.HasId() {
return errors.New("The record is not persisted yet.") return errors.New("the record is not persisted yet")
} }
fs, err := form.app.NewFilesystem() fs, err := form.app.NewFilesystem()
@ -685,65 +707,75 @@ func (form *RecordUpsert) processFilesToUpload() error {
} }
defer fs.Close() defer fs.Close()
var uploadErrors []error var uploadErrors []error // list of upload errors
var uploaded []string // list of uploaded file paths
for fieldKey := range form.filesToUpload { for fieldKey := range form.filesToUpload {
for i := len(form.filesToUpload[fieldKey]) - 1; i >= 0; i-- { for i, file := range form.filesToUpload[fieldKey] {
file := form.filesToUpload[fieldKey][i]
path := form.record.BaseFilesPath() + "/" + file.Name() path := form.record.BaseFilesPath() + "/" + file.Name()
if err := fs.UploadMultipart(file.Header(), path); err == nil { if err := fs.UploadMultipart(file.Header(), path); err == nil {
// remove the uploaded file from the list // keep track of the already uploaded file
form.filesToUpload[fieldKey] = append(form.filesToUpload[fieldKey][:i], form.filesToUpload[fieldKey][i+1:]...) uploaded = append(uploaded, path)
} else { } else {
// store the upload error // store the upload error
uploadErrors = append(uploadErrors, fmt.Errorf("File %d: %v", i, err)) uploadErrors = append(uploadErrors, fmt.Errorf("file %d: %v", i, err))
} }
} }
} }
if len(uploadErrors) > 0 { if len(uploadErrors) > 0 {
return fmt.Errorf("Failed to upload all files: %v", uploadErrors) // cleanup - try to delete the successfully uploaded files (if any)
form.deleteFilesByNamesList(uploaded)
return fmt.Errorf("failed to upload all files: %v", uploadErrors)
} }
return nil return nil
} }
func (form *RecordUpsert) processFilesToDelete() error { func (form *RecordUpsert) processFilesToDelete() (err error) {
if len(form.filesToDelete) == 0 { form.filesToDelete, err = form.deleteFilesByNamesList(form.filesToDelete)
return nil // nothing to delete 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
} }
if !form.record.HasId() { if !form.record.HasId() {
return errors.New("The record is not persisted yet.") return filenames, errors.New("the record doesn't have a unique ID")
} }
fs, err := form.app.NewFilesystem() fs, err := form.app.NewFilesystem()
if err != nil { if err != nil {
return err return filenames, err
} }
defer fs.Close() defer fs.Close()
var deleteErrors []error var deleteErrors []error
for i := len(form.filesToDelete) - 1; i >= 0; i-- {
filename := form.filesToDelete[i] for i := len(filenames) - 1; i >= 0; i-- {
filename := filenames[i]
path := form.record.BaseFilesPath() + "/" + filename path := form.record.BaseFilesPath() + "/" + filename
if err := fs.Delete(path); err == nil { if err := fs.Delete(path); err == nil {
// remove the deleted file from the list // remove the deleted file from the list
form.filesToDelete = append(form.filesToDelete[:i], form.filesToDelete[i+1:]...) filenames = append(filenames[:i], filenames[i+1:]...)
} else {
// store the delete error
deleteErrors = append(deleteErrors, fmt.Errorf("File %d: %v", i, err))
}
// try to delete the related file thumbs (if any) // try to delete the related file thumbs (if any)
fs.DeletePrefix(form.record.BaseFilesPath() + "/thumbs_" + filename + "/") fs.DeletePrefix(form.record.BaseFilesPath() + "/thumbs_" + filename + "/")
} else {
// store the delete error
deleteErrors = append(deleteErrors, fmt.Errorf("file %d: %v", i, err))
}
} }
if len(deleteErrors) > 0 { if len(deleteErrors) > 0 {
return fmt.Errorf("Failed to delete all files: %v", deleteErrors) return filenames, fmt.Errorf("failed to delete all files: %v", deleteErrors)
} }
return nil return filenames, nil
} }

View File

@ -176,13 +176,11 @@ func (s *System) DeletePrefix(prefix string) []error {
dirsMap := map[string]struct{}{} dirsMap := map[string]struct{}{}
dirsMap[prefix] = struct{}{} dirsMap[prefix] = struct{}{}
opts := blob.ListOptions{
Prefix: prefix,
}
// delete all files with the prefix // delete all files with the prefix
// --- // ---
iter := s.bucket.List(&opts) iter := s.bucket.List(&blob.ListOptions{
Prefix: prefix,
})
for { for {
obj, err := iter.Next(s.ctx) obj, err := iter.Next(s.ctx)
if err == io.EOF { if err == io.EOF {