vxcontrol/pentagi · error

failed to delete blob %s: %w

Error message

failed to delete blob %s: %w

What it means

DeleteBlob (resources.go:124) removes the .blob file for a hash after validating it. If os.Remove fails for any reason other than 'file does not exist' (which is silently treated as success), the error 'failed to delete blob %s: %w' wraps the filesystem error. So this surfaces real removal problems: permissions, directory issues, or the path being a non-empty directory.

Source

Thrown at backend/pkg/resources/resources.go:124

	_, err := os.Lstat(BlobPath(dataDir, hash))
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

// DeleteBlob removes the .blob file for hash. It is safe to call if the file
// does not exist (returns nil in that case).
func DeleteBlob(dataDir, hash string) error {
	if err := validateBlobHash(hash); err != nil {
		return err
	}
	err := os.Remove(BlobPath(dataDir, hash))
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("failed to delete blob %s: %w", hash, err)
	}
	return nil
}

// SanitizeResourcePath normalises a client-supplied virtual path and ensures it
// is safe to use:
//   - trims whitespace
//   - converts backslashes to forward slashes
//   - cleans the path (removes .., double slashes, etc.)
//   - rejects absolute paths, dot-only components, and paths that exceed MaxPathLength
//   - returns an error for the empty path
func SanitizeResourcePath(p string) (string, error) {
	trimmed := strings.TrimSpace(p)
	if trimmed == "" {
		return "", fmt.Errorf("path must not be empty")
	}
	if len(trimmed) > MaxPathLength {
		return "", fmt.Errorf("path exceeds maximum allowed length of %d characters", MaxPathLength)

View on GitHub (pinned to ea665308ba)

Solutions

  1. Check the wrapped cause (errors.Unwrap / os.RemoveAll behavior) and the path from BlobPath(dataDir, hash)
  2. Verify write permission on the blob directory for the process user (ls -ld dataDir)
  3. Confirm the volume is not mounted read-only (mount | grep, kubectl describe)
  4. If the entry is a directory or corrupted, remove it manually or with os.RemoveAll after verifying it is safe

Example fix

// before
if err := resources.DeleteBlob(dataDir, hash); err != nil { return err }
// after
if err := resources.DeleteBlob(dataDir, hash); err != nil {
    var perr *fs.PathError
    if errors.As(err, &perr) && (errors.Is(perr, os.ErrPermission) || errors.Is(perr, syscall.EACCES)) {
        log.Warn("skipping blob delete, permission denied", "hash", hash)
        return nil // or escalate
    }
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

if !resources.IsValidBlobHash(hash) { return fmt.Errorf("invalid hash %q", hash) }
if info, err := os.Stat(resources.BlobPath(dataDir, hash)); err == nil && info.IsDir() {
    return fmt.Errorf("blob path is a directory")
}

Type guard

func isPermissionErr(err error) bool {
    var perr *fs.PathError
    return errors.As(err, &perr) && errors.Is(perr, os.ErrPermission)
}

Try / catch

if err := resources.DeleteBlob(dataDir, hash); err != nil {
    if isPermissionErr(err) { log.Warn("no permission to delete blob", "hash", hash); return nil }
    return fmt.Errorf("blob delete failed: %w", err)
}

Prevention

When it happens

Trigger: os.Remove returns EACCES/EPERM (no write permission on the blob directory), the blob path exists as a directory, the filesystem is read-only or full (rare for unlink), or the dataDir points to the wrong location holding a similarly-named directory.

Common situations: Running the process as a non-root user after blobs were created by another user; mounting the blob volume read-only; orphan-blob cleanup workers (deleteOrphanBlobsIfUnreferenced, cleanupOrphanBlobs) hitting blobs locked down by backup tooling; k8s volumes remounted read-only.

Related errors


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