Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## Unreleased

- Add an `extended` object naming mode, seeded with `--object-naming=extended`
or `MINIO_OBJECT_NAMING=extended` on a cluster's first start and recorded in
every `format.json`. In this mode an object and a prefix of the same name
(`obj1` and `obj1/obj2`) coexist and are both listed: object names are
escaped and regular objects carry a terminal marker on disk, so an object
directory can never double as another key's parent. The default (safe) mode
keeps its existing behavior and disk layout; the mode cannot be changed or
migrated in place, and mixed modes across drives, pools or nodes are rejected
at startup. See `docs/minio-limits.md` for the resulting restrictions.

## RELEASE.2026-10-26T00-00-00Z

Package version: `20261026000000.0.0`; published from source `c3ad836aed0d`.
Expand Down
1 change: 1 addition & 0 deletions buildscripts/rebrand-guard/compat-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@
"MINIO_NOTIFY_WEBHOOK_QUEUE_DIR",
"MINIO_NOTIFY_WEBHOOK_QUEUE_LIMIT",
"MINIO_NTP_SERVER",
"MINIO_OBJECT_NAMING",
"MINIO_OLD",
"MINIO_OPERATOR_VERSION",
"MINIO_OPTS",
Expand Down
2 changes: 1 addition & 1 deletion cmd/batch-expire.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ func batchObjsForDelete(ctx context.Context, r *BatchJobExpire, ri *batchJobInfo
default:
}
stopFn := globalBatchJobsMetrics.trace(batchJobMetricExpire, ri.JobID, attempts)
_, err := api.DeleteObject(ctx, exp.Bucket, encodeDirObject(exp.Name), ObjectOptions{
_, err := api.DeleteObject(ctx, exp.Bucket, exp.Name, ObjectOptions{
DeletePrefix: true,
DeletePrefixObject: true, // use prefix delete on exact object (this is an optimization to avoid fan-out calls)
})
Expand Down
6 changes: 3 additions & 3 deletions cmd/bucket-object-lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,17 +275,17 @@ func checkObjectLockedForOverwrite(ctx context.Context, bucket, object string, g
return err
}
if objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined).Status == objectlock.LegalHoldOn {
return ObjectLocked{Bucket: bucket, Object: decodeDirObject(object), VersionID: nullVersionID}
return ObjectLocked{Bucket: bucket, Object: decodeObjectName(object), VersionID: nullVersionID}
}
r := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
if r.Mode == objectlock.RetCompliance || r.Mode == objectlock.RetGovernance {
t, err := objectlock.UTCNowNTP()
if err != nil {
internalLogIf(ctx, err, logger.WarningKind)
return ObjectLocked{Bucket: bucket, Object: decodeDirObject(object), VersionID: nullVersionID}
return ObjectLocked{Bucket: bucket, Object: decodeObjectName(object), VersionID: nullVersionID}
}
if r.RetainUntilDate.After(t) {
return ObjectLocked{Bucket: bucket, Object: decodeDirObject(object), VersionID: nullVersionID}
return ObjectLocked{Bucket: bucket, Object: decodeObjectName(object), VersionID: nullVersionID}
}
}
return nil
Expand Down
11 changes: 11 additions & 0 deletions cmd/common-main.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ func buildServerCtxt(ctx *cli.Context, ctxt *serverCtxt) (err error) {
ctxt.FTP = ctx.StringSlice("ftp")
ctxt.SFTP = ctx.StringSlice("sftp")
ctxt.Interface = ctx.String("interface")
ctxt.ObjectNaming = ctx.String("object-naming")
ctxt.UserTimeout = ctx.Duration("conn-user-timeout")
ctxt.SendBufSize = ctx.Int("send-buf-size")
ctxt.RecvBufSize = ctx.Int("recv-buf-size")
Expand Down Expand Up @@ -771,6 +772,16 @@ func serverHandleEarlyEnvVars() {
func serverHandleEnvVars() {
var err error

// Object naming mode must be valid before any format is written. The
// recorded format is authoritative for an existing cluster; a mismatch is
// rejected when the object layer starts.
if mode := globalServerCtxt.ObjectNaming; mode != "" {
if !isObjectNamingModeValid(mode) {
logger.Fatal(fmt.Errorf("invalid object naming mode %q: must be %q or %q", mode, objectNamingSafe, objectNamingExtended), "Invalid %s value in environment variable", config.EnvObjectNaming)
}
}
globalObjectNaming = globalServerCtxt.ObjectNaming

// Re-read the source-address trust policy now that loadEnvVarsFromFiles has
// run: a policy taken at package initialisation would miss every deployment
// configured through MINIO_CONFIG_ENV_FILE. Refuse to start on a malformed
Expand Down
19 changes: 15 additions & 4 deletions cmd/data-scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,9 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int

// If there are lifecycle rules for the prefix.
_, prefix := path2BucketObjectWithBasePath(f.root, folder.name)
// Folder names are disk paths; rules and reported prefixes use the
// user-domain key, which differs in extended naming mode.
prefix = decodeExtendedDiskPath(prefix)
var activeLifeCycle *lifecycle.Lifecycle
if f.oldCache.Info.lifeCycle != nil && f.oldCache.Info.lifeCycle.HasActiveRules(prefix) {
if f.dataUsageScannerDebug {
Expand Down Expand Up @@ -447,7 +450,13 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
}
return nil
}
bucket, prefix := path2BucketObjectWithBasePath(f.root, entName)
// Object API calls below run in the user domain, while Path stays
// the raw disk path for metadata reads.
userEntName := entName
if globalObjectNamingExtended {
userEntName = decodeExtendedDiskPath(entName)
}
bucket, prefix := path2BucketObjectWithBasePath(f.root, userEntName)
if bucket == "" {
if f.dataUsageScannerDebug {
console.Debugf(scannerLogPrefix+" no bucket (%s,%s)\n", f.root, entName)
Expand Down Expand Up @@ -522,8 +531,8 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int

// Object already accounted for, remove from heal map,
// simply because getSize() function already heals the
// object.
delete(abandonedChildren, pathJoin(item.bucket, item.objectPath()))
// object. Cache children are keyed by disk paths.
delete(abandonedChildren, path.Dir(entName))

if err != errIgnoreFileContrib {
into.addSizes(sz)
Expand Down Expand Up @@ -713,6 +722,8 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
break
}
bucket, prefix := path2BucketObject(k)
// WalkDir takes user-domain paths.
prefix = decodeObjectName(prefix)
stopFn := globalScannerMetrics.time(scannerMetricCheckMissing)
f.updateCurrentPath(k)

Expand Down Expand Up @@ -1260,7 +1271,7 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
}
}()

dobj, err = objLayer.DeleteObject(ctx, obj.Bucket, encodeDirObject(obj.Name), opts)
dobj, err = objLayer.DeleteObject(ctx, obj.Bucket, obj.Name, opts)
if err != nil {
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
traceFn(ILMExpiry, nil, nil)
Expand Down
4 changes: 2 additions & 2 deletions cmd/data-usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ func loadPrefixUsageFromBackend(ctx context.Context, objAPI ObjectLayer, bucket
}

for id, usageInfo := range cache.flattenChildrens(*root) {
prefix := decodeDirObject(strings.TrimPrefix(id, bucket+slashSeparator))
// decodeDirObject to avoid any __XLDIR__ objects
prefix := decodeObjectName(strings.TrimPrefix(id, bucket+slashSeparator))
// decodeObjectName undoes __XLDIR__ and extended naming.
m[prefix] += uint64(usageInfo.Size)
}
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/erasure-healing.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func (er *erasureObjects) auditHealObject(ctx context.Context, bucket, object, v
opts := AuditLogOptions{
Event: "HealObject",
Bucket: bucket,
Object: decodeDirObject(object),
Object: decodeObjectName(object),
VersionID: versionID,
}
if err != nil {
Expand Down Expand Up @@ -1120,7 +1120,7 @@ func healTrace(funcName healingMetric, startTime time.Time, bucket, object strin
NodeName: globalLocalNodeName,
FuncName: "heal." + funcName.String(),
Duration: time.Since(startTime),
Path: pathJoin(bucket, decodeDirObject(object)),
Path: pathJoin(bucket, decodeObjectName(object)),
}
if opts != nil {
tr.Custom = map[string]string{
Expand Down
2 changes: 1 addition & 1 deletion cmd/erasure-object.go
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,7 @@ func (er erasureObjects) readListingFileInfo(ctx context.Context, bucket, object
}

func listingReadQuorumError(bucket, object string) error {
return InsufficientReadQuorum{Bucket: bucket, Object: decodeDirObject(object), Err: errErasureReadQuorum, Type: RQInconsistentMeta}
return InsufficientReadQuorum{Bucket: bucket, Object: decodeObjectName(object), Err: errErasureReadQuorum, Type: RQInconsistentMeta}
}

func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object string, opts ObjectOptions, readData bool) (FileInfo, []FileInfo, []StorageAPI, error) {
Expand Down
20 changes: 10 additions & 10 deletions cmd/erasure-server-pool-consistency.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ func (z *erasureServerPools) objectPoolInfos(ctx context.Context, bucket, object
})
if len(copies) == 0 {
if opts.VersionID != "" {
return nil, VersionNotFound{Bucket: bucket, Object: decodeDirObject(object), VersionID: opts.VersionID}
return nil, VersionNotFound{Bucket: bucket, Object: decodeObjectNameForBucket(bucket, object), VersionID: opts.VersionID}
}
return nil, ObjectNotFound{Bucket: bucket, Object: decodeDirObject(object)}
return nil, ObjectNotFound{Bucket: bucket, Object: decodeObjectNameForBucket(bucket, object)}
}
return copies, nil
}
Expand Down Expand Up @@ -102,7 +102,7 @@ func (z *erasureServerPools) checkDataMovementCopies(ctx context.Context, bucket
}
}
if !sourcePresent {
return nil, VersionNotFound{Bucket: bucket, Object: decodeDirObject(object), VersionID: versionID}
return nil, VersionNotFound{Bucket: bucket, Object: decodeObjectNameForBucket(bucket, object), VersionID: versionID}
}
for _, copy := range copies {
if copy.Index != opts.SrcPoolIdx && copy.ObjInfo.ModTime.After(opts.MTime) {
Expand Down Expand Up @@ -131,7 +131,7 @@ func (z *erasureServerPools) checkDataMovementCopies(ctx context.Context, bucket
// snapshot. A concurrent delete can create a new marker in the source pool;
// deleting the whole prefix would remove that marker and expose moved data.
func (z *erasureServerPools) removeMovedVersions(ctx context.Context, bucket, object string, source int, versions []FileInfo) error {
object = encodeDirObject(object)
object = encodeObjectNameForBucket(bucket, object)
lk := z.NewNSLock(bucket, object)
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
if err != nil {
Expand Down Expand Up @@ -265,7 +265,7 @@ func replicaCopiesInfo(copies []PoolObjInfo, err error, bucket, object, versionI
return ObjectInfo{}, err
}
if copies[0].ObjInfo.DeleteMarker {
return copies[0].ObjInfo, MethodNotAllowed{Bucket: bucket, Object: decodeDirObject(object), VersionID: versionID}
return copies[0].ObjInfo, MethodNotAllowed{Bucket: bucket, Object: decodeObjectNameForBucket(bucket, object), VersionID: versionID}
}
return mergedPoolObjectInfo(copies), nil
}
Expand All @@ -278,7 +278,7 @@ func (z *erasureServerPools) metadataPoolInfos(ctx context.Context, bucket, obje
return nil, err
}
if copies[0].ObjInfo.DeleteMarker {
return nil, MethodNotAllowed{Bucket: bucket, Object: decodeDirObject(object), VersionID: opts.VersionID}
return nil, MethodNotAllowed{Bucket: bucket, Object: decodeObjectNameForBucket(bucket, object), VersionID: opts.VersionID}
}
if opts.VersionID == "" {
opts.VersionID = copies[0].ObjInfo.VersionID
Expand Down Expand Up @@ -417,7 +417,7 @@ func (z *erasureServerPools) retireReplicaCopies(ctx context.Context, bucket, ob
}
}
if len(retained) == 0 {
return VersionNotFound{Bucket: bucket, Object: decodeDirObject(object), VersionID: versionID}
return VersionNotFound{Bucket: bucket, Object: decodeObjectNameForBucket(bucket, object), VersionID: versionID}
}
for i, copy := range copies {
if copy.Index == keep {
Expand Down Expand Up @@ -513,7 +513,7 @@ func (z *erasureServerPools) deleteObjectReconciled(ctx context.Context, bucket,
// A single new delete marker hides the current version. Older versions
// remain history and must not be removed by an unqualified DELETE.
oi, err := z.serverPools[primary.Index].DeleteObject(ctx, bucket, object, opts)
oi.Name = decodeDirObject(object)
oi.Name = decodeObjectNameForBucket(bucket, object)
oi.replicationDecision = opts.DeleteReplication.ReplicateDecisionStr
return oi, err
}
Expand All @@ -530,7 +530,7 @@ func (z *erasureServerPools) deleteObjectReconciled(ctx context.Context, bucket,
}
}
oi, err := z.serverPools[primary.Index].DeleteObject(ctx, bucket, object, opts)
oi.Name = decodeDirObject(object)
oi.Name = decodeObjectNameForBucket(bucket, object)
oi.replicationDecision = opts.DeleteReplication.ReplicateDecisionStr
return oi, err
}
Expand Down Expand Up @@ -562,7 +562,7 @@ func (z *erasureServerPools) deleteObjectsReconciled(ctx context.Context, bucket
}
} else if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
if isErrReadQuorum(err) {
err = InsufficientWriteQuorum{Bucket: bucket, Object: decodeDirObject(obj.ObjectName)}
err = InsufficientWriteQuorum{Bucket: bucket, Object: decodeObjectNameForBucket(bucket, obj.ObjectName)}
}
errs[i] = err
}
Expand Down
9 changes: 7 additions & 2 deletions cmd/erasure-server-pool-decom.go
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ func (z *erasureServerPools) decommissionObject(ctx context.Context, idx int, bu

if objInfo.isMultipart() {
// Completion repeats this check under the object write lock.
if _, err := z.checkDataMovementCopies(ctx, bucket, encodeDirObject(objInfo.Name), ObjectOptions{
if _, err := z.checkDataMovementCopies(ctx, bucket, encodeObjectNameForBucket(bucket, objInfo.Name), ObjectOptions{
VersionID: objInfo.VersionID, MTime: objInfo.ModTime, SrcPoolIdx: idx,
}); err != nil {
return err
Expand Down Expand Up @@ -921,7 +921,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
}
gr, err := set.GetObjectNInfo(ctx,
bi.Name,
encodeDirObject(version.Name),
encodeObjectNameForBucket(bi.Name, version.Name),
nil,
http.Header{},
ObjectOptions{
Expand Down Expand Up @@ -951,6 +951,11 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
stopFn(version.Size, err)
continue
}
// Restore the user-domain name: the set layer works on the
// storage-stack form, while the pooled APIs below expect the
// original key.
gr.ObjInfo.Name = version.Name

if err = z.decommissionObject(ctx, idx, bi.Name, gr); err != nil && !isDataMovementSuperseded(err) {
if isErrObjectNotFound(err) || isErrVersionNotFound(err) || isDataMovementOverWriteErr(err) {
ignore = true
Expand Down
9 changes: 7 additions & 2 deletions cmd/erasure-server-pool-rebalance.go
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,7 @@ func (z *erasureServerPools) rebalanceBucket(ctx context.Context, bucket string,
// GetObjectReader.Close is called by rebalanceObject
gr, err := set.GetObjectNInfo(ctx,
bucket,
encodeDirObject(version.Name),
encodeObjectNameForBucket(bucket, version.Name),
nil,
http.Header{},
ObjectOptions{
Expand All @@ -712,6 +712,11 @@ func (z *erasureServerPools) rebalanceBucket(ctx context.Context, bucket string,
continue
}

// Restore the user-domain name: the set layer works on the
// storage-stack form, while the pooled APIs below expect the
// original key.
gr.ObjInfo.Name = version.Name

if err = z.rebalanceObject(ctx, poolIdx, bucket, gr); err != nil && !isDataMovementSuperseded(err) {
// This can happen when rebalance stop races with ongoing rebalance workers.
// These rebalance failures can be ignored.
Expand Down Expand Up @@ -849,7 +854,7 @@ func (z *erasureServerPools) rebalanceObject(ctx context.Context, poolIdx int, b

if oi.isMultipart() {
// Completion repeats this check under the object write lock.
if _, err := z.checkDataMovementCopies(ctx, bucket, encodeDirObject(oi.Name), ObjectOptions{
if _, err := z.checkDataMovementCopies(ctx, bucket, encodeObjectNameForBucket(bucket, oi.Name), ObjectOptions{
VersionID: oi.VersionID, MTime: oi.ModTime, SrcPoolIdx: poolIdx,
}); err != nil {
return err
Expand Down
Loading