thanos-io/thanos · error

is not a directory

Error message

%s is not a directory

What it means

upload() stats the given block directory before pushing it to object storage. If the path exists but is a regular file (or other non-directory), it refuses with '%s is not a directory' because Thanos block uploads only operate on ULID-named block directories.

Solutions

  1. Verify the path passed to Upload/UploadPromBlock points to the block directory (containing meta.json), not a file
  2. Run os.Stat and check IsDir() before calling upload
  3. Fix symlinks or stale paths in tooling that computes the block dir

Example fix

// before
err := block.Upload(ctx, logger, bkt, "path/to/meta.json", hf, true)
// after
if fi, err := os.Stat("path/to/block"); err == nil && fi.IsDir() {
    err = block.Upload(ctx, logger, bkt, "path/to/block", hf, true)
}
Defensive patterns

Strategy: validation

Validate before calling

fi, err := os.Stat(bdir)
if err != nil { return err }
if !fi.IsDir() { return fmt.Errorf("%s is not a directory", bdir) }

Prevention

When it happens

Trigger: Calling block.Upload / UploadPromBlock with a bdir path that resolves to a file, or a symlink pointing to a file, instead of a block directory.

Common situations: Passing a meta.json file path instead of the block dir; path resolved through a symlink; a script variable pointing at the wrong file; a partially deleted directory replaced by a file.

Related errors


AI-assisted analysis of thanos-io/thanos@35b8b99117 (2026-09-07). Data as JSON: /api/errors/2bfa35fdf724dcf4. Report an issue: GitHub.

Appendix: source

Thrown at pkg/block/block.go:117

}

// UploadPromBlock uploads a TSDB block to the object storage. It assumes
// the block is used in Prometheus so it doesn't check Thanos external labels.
func UploadPromBlock(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bdir string, hf metadata.HashFunc, options ...objstore.UploadOption) error {
	return upload(ctx, logger, bkt, bdir, hf, false, options...)
}

// upload uploads block from given block dir that ends with block id.
// It makes sure cleanup is done on error to avoid partial block uploads.
// TODO(bplotka): Ensure bucket operations have reasonable backoff retries.
// NOTE: Upload updates `meta.Thanos.File` section.
func upload(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bdir string, hf metadata.HashFunc, checkExternalLabels bool, options ...objstore.UploadOption) error {
	df, err := os.Stat(bdir)
	if err != nil {
		return err
	}
	if !df.IsDir() {
		return errors.Errorf("%s is not a directory", bdir)
	}

	// Verify dir.
	id, err := ulid.Parse(df.Name())
	if err != nil {
		return errors.Wrap(err, "not a block dir")
	}

	meta, err := metadata.ReadFromDir(bdir)
	if err != nil {
		// No meta or broken meta file.
		return errors.Wrap(err, "read meta")
	}

	if checkExternalLabels {
		if len(meta.Thanos.Labels) == 0 {
			return errors.New("empty external labels are not allowed for Thanos block.")
		}

View on GitHub (pinned to 35b8b99117)