vxcontrol/pentagi · error

failed to move resource %q: %w

Error message

failed to move resource %q: %w

What it means

updateMovedResource rewrites a user resource row's path/name/updated_at inside a DB transaction during a move. If the GORM Updates call fails (connection issue, constraint violation, deadlock, row lock), the move is aborted and this wrapped error carries the original path and the underlying DB error. It propagates up to moveFileResource / moveDirResourceToAbsentDestination / moveDirResourceMerge and the transaction is rolled back by the caller.

Source

Thrown at backend/pkg/server/services/resources.go:1176

}

func resourcesByPath(recs []models.UserResource) map[string]models.UserResource {
	byPath := make(map[string]models.UserResource, len(recs))
	for _, rec := range recs {
		byPath[rec.Path] = rec
	}
	return byPath
}

func updateMovedResource(tx *gorm.DB, rec models.UserResource, newPath string, now time.Time) (models.UserResource, error) {
	if err := tx.Model(&models.UserResource{}).
		Where("id = ?", rec.ID).
		Updates(map[string]interface{}{
			"path":       newPath,
			"name":       path.Base(newPath),
			"updated_at": now,
		}).Error; err != nil {
		return models.UserResource{}, fmt.Errorf("failed to move resource %q: %w", rec.Path, err)
	}
	rec.Path = newPath
	rec.Name = path.Base(newPath)
	rec.UpdatedAt = now
	return rec, nil
}

// ---- POST /resources/copy --------------------------------------------------

// CopyResource copies one or more resource files / directories to a new path.
//
// Single-source behaviour (exactly one unique source after dedup):
//
//	Destination is the exact target path, inheriting existing trailing-slash
//	and existing-directory semantics (unchanged from original behaviour).
//
// Multi-source behaviour (two or more unique sources after dedup):
//

View on GitHub (pinned to ea665308ba)

Solutions

  1. Check the wrapped underlying DB error (errors.Unwrap / log) to identify whether it is a unique violation, deadlock, or connectivity problem.
  2. Retry the move once; deadlocks and transient connection errors are usually retryable.
  3. Pre-check that no resource already exists at newPath (findResourceByPath) or delete/archive the conflicting destination first.
  4. Verify DB health: connection pool limits, disk space, and that migrations ran cleanly.

Example fix

// before
updated, err := updateMovedResource(tx, entry, newPath, now)
if err != nil { return result, err }
// after
updated, err := updateMovedResource(tx, entry, newPath, now)
if err != nil {
    tx.Rollback()
    if errors.Is(err, syscall.ECONNRESET) || isDeadlock(err) { /* retry once */ }
    return result, err
}
Defensive patterns

Strategy: try-catch

Validate before calling

var existing models.UserResource
err := db.Where("user_id = ? AND path = ?", uid, newPath).First(&existing).Error
if err == nil { return errors.New("destination path already exists") }

Type guard

func isUniqueViolation(err error) bool {
    var pgErr *pgconn.PgError
    return errors.As(err, &pgErr) && pgErr.Code == "23505"
}

Try / catch

updated, err := updateMovedResource(tx, rec, newPath, now)
if err != nil {
    tx.Rollback()
    if isUniqueViolation(err) || isDeadlock(err) { /* retry or surface 409 */ }
    return fmt.Errorf("move %q failed: %w", rec.Path, err)
}

Prevention

When it happens

Trigger: Any move (REST move endpoint) whose UPDATE statement fails: database connection loss or timeout mid-transaction, a unique-index violation on (user_id, path) if another row already holds newPath, a deadlock with a concurrent write, or the DB being read-only / out of disk.

Common situations: Two users (or two browser tabs) racing to move resources to the same destination path; PostgreSQL restart or connection-pool exhaustion during a large directory move; a unique constraint on path added by a migration while legacy duplicate rows exist.

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/f79dec81bb8da885. Report an issue: GitHub.