diff --git a/cmd/erasure-multipart-cleanup_test.go b/cmd/erasure-multipart-cleanup_test.go new file mode 100644 index 000000000..fa0fe92ba --- /dev/null +++ b/cmd/erasure-multipart-cleanup_test.go @@ -0,0 +1,84 @@ +// Copyright (c) 2026 clown15 +// +// This file is part of Silo Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "encoding/base64" + "fmt" + "testing" + "time" +) + +func TestExtractModTimeFromUploadIDDir(t *testing.T) { + now := time.Now().UTC().Truncate(time.Millisecond) + nano := now.UnixNano() + uuid := "d16ad42a-6fe5-4a3b-8092-69983142238f" + + testCases := []struct { + name string + dirName string + expectedMod time.Time + wantZero bool + }{ + { + name: "Modern Plaintext Format (x)", + dirName: fmt.Sprintf("%sx%d", uuid, nano), + expectedMod: time.Unix(0, nano), + wantZero: false, + }, + { + name: "Legacy Format Without Timestamp ()", + dirName: uuid, + expectedMod: time.Time{}, + wantZero: true, + }, + { + name: "Base64 Encoded Format", + dirName: base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%sx%d", uuid, nano))), + expectedMod: time.Unix(0, nano), + wantZero: false, + }, + { + name: "Malformed Suffix (xinvalid)", + dirName: fmt.Sprintf("%sxnotanumber", uuid), + expectedMod: time.Time{}, + wantZero: true, + }, + { + name: "Empty Directory Name", + dirName: "", + expectedMod: time.Time{}, + wantZero: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := extractModTimeFromUploadIDDir(tc.dirName) + if tc.wantZero { + if !got.IsZero() { + t.Fatalf("expected zero time for %q, got %v", tc.dirName, got) + } + return + } + if !got.Equal(tc.expectedMod) { + t.Fatalf("extractModTimeFromUploadIDDir(%q) = %v; want %v", tc.dirName, got, tc.expectedMod) + } + }) + } +} diff --git a/cmd/erasure-multipart.go b/cmd/erasure-multipart.go index 89355c762..b48b62d78 100644 --- a/cmd/erasure-multipart.go +++ b/cmd/erasure-multipart.go @@ -174,6 +174,23 @@ func (er erasureObjects) deleteAll(ctx context.Context, bucket, prefix string) { wg.Wait() } +// extractModTimeFromUploadIDDir extracts creation time from the upload ID directory on disk. +// On-disk directory names are of the form x. +// For compatibility with legacy or encoded representations, if the directory is Base64 encoded, +// it decodes the name before parsing. +func extractModTimeFromUploadIDDir(uploadIDDir string) time.Time { + target := uploadIDDir + if b64, err := base64.RawURLEncoding.DecodeString(uploadIDDir); err == nil && strings.Contains(string(b64), "x") { + target = string(b64) + } + if split := strings.Split(target, "x"); len(split) == 2 { + if t, err := strconv.ParseInt(split[1], 10, 64); err == nil { + return time.Unix(0, t) + } + } + return time.Time{} +} + // Remove the old multipart uploads on the given disk. func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk StorageAPI) { drivePath := disk.Endpoint().Path @@ -181,16 +198,7 @@ func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk Sto readDirFn(pathJoin(drivePath, minioMetaMultipartBucket), func(shaDir string, typ os.FileMode) error { readDirFn(pathJoin(drivePath, minioMetaMultipartBucket, shaDir), func(uploadIDDir string, typ os.FileMode) error { uploadIDPath := pathJoin(shaDir, uploadIDDir) - var modTime time.Time - // Upload IDs are of the form base64_url(x), we can extract the time from the UUID. - if b64, err := base64.RawURLEncoding.DecodeString(uploadIDDir); err == nil { - if split := strings.Split(string(b64), "x"); len(split) == 2 { - t, err := strconv.ParseInt(split[1], 10, 64) - if err == nil { - modTime = time.Unix(0, t) - } - } - } + modTime := extractModTimeFromUploadIDDir(uploadIDDir) // Fallback for older uploads without time in the ID. if modTime.IsZero() { wait := deleteMultipartCleanupSleeper.Timer(ctx)