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
- Verify the path passed to Upload/UploadPromBlock points to the block directory (containing meta.json), not a file
- Run os.Stat and check IsDir() before calling upload
- 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
- Always pass the block directory root, never meta.json itself
- Resolve symlinks (filepath.EvalSymlinks) before uploading
- Validate paths in scripts that compute block directories
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)