diff --git a/weed/storage/disk_location.go b/weed/storage/disk_location.go index ebb6f675a..fbdc66353 100644 --- a/weed/storage/disk_location.go +++ b/weed/storage/disk_location.go @@ -152,6 +152,19 @@ func getValidVolumeName(basename string) string { return "" } +// hasEcxFile reports whether an .ecx for volumeName exists on this disk. +// Checks IdxDirectory first, then falls back to Directory (the .ecx may +// have been created before -dir.idx was configured). +func (l *DiskLocation) hasEcxFile(volumeName string) bool { + if util.FileExists(filepath.Join(l.IdxDirectory, volumeName+".ecx")) { + return true + } + if l.IdxDirectory != l.Directory { + return util.FileExists(filepath.Join(l.Directory, volumeName+".ecx")) + } + return false +} + func (l *DiskLocation) loadExistingVolume(dirEntry os.DirEntry, needleMapKind NeedleMapKind, skipIfEcVolumesExists bool, ldbTimeout int64, diskId uint32) bool { basename := dirEntry.Name() if dirEntry.IsDir() { @@ -169,14 +182,16 @@ func (l *DiskLocation) loadExistingVolume(dirEntry os.DirEntry, needleMapKind Ne return false } + // .vif next to .ecx is EC shard metadata, not a regular volume. + // Without this guard NewVolume below would create a phantom empty .dat. + if strings.HasSuffix(basename, ".vif") && l.hasEcxFile(volumeName) { + glog.V(1).Infof("loadExistingVolume: skipping .vif-only entry for volume %d (collection=%q); .ecx present", vid, collection) + return false + } + // skip if ec volumes exists, but validate EC files first if skipIfEcVolumesExists { - ecxFilePath := filepath.Join(l.IdxDirectory, volumeName+".ecx") - if !util.FileExists(ecxFilePath) && l.IdxDirectory != l.Directory { - // .ecx may have been created before -dir.idx was configured - ecxFilePath = filepath.Join(l.Directory, volumeName+".ecx") - } - if util.FileExists(ecxFilePath) { + if l.hasEcxFile(volumeName) { // Validate EC volume: shard count, size consistency, and expected size vs .dat file if !l.validateEcVolume(collection, vid) { glog.Warningf("EC volume %d validation failed, removing incomplete EC files to allow .dat file loading", vid) diff --git a/weed/storage/disk_location_ec_test.go b/weed/storage/disk_location_ec_test.go index 0210890f3..d794456d0 100644 --- a/weed/storage/disk_location_ec_test.go +++ b/weed/storage/disk_location_ec_test.go @@ -661,3 +661,78 @@ func TestDistributedEcVolumeNoFileDeletion(t *testing.T) { t.Logf("SUCCESS: Distributed EC volume files preserved (not deleted)") } + +// TestLoadExistingVolumeSkipsVifWhenEcxPresent pins the skip behavior on +// the LoadVolume / MountVolume path (skipIfEcVolumesExists=false) for the +// .vif + .ecx disk layout without .dat. Two variants cover both +// IdxDirectory==Directory and the split-idx-dir fallback. +func TestLoadExistingVolumeSkipsVifWhenEcxPresent(t *testing.T) { + const vid needle.VolumeId = 42 + + cases := []struct { + name string + splitDirs bool + }{ + {name: "same-idx-dir", splitDirs: false}, + {name: "split-idx-dir", splitDirs: true}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dataDir := t.TempDir() + idxDir := dataDir + if tc.splitDirs { + idxDir = t.TempDir() + } + + minFreeSpace := util.MinFreeSpace{Type: util.AsPercent, Percent: 1, Raw: "1"} + diskLocation := &DiskLocation{ + Directory: dataDir, + DirectoryUuid: "test-uuid", + IdxDirectory: idxDir, + DiskType: types.HddType, + MaxVolumeCount: 100, + OriginalMaxVolumeCount: 100, + MinFreeSpace: minFreeSpace, + } + diskLocation.volumes = make(map[needle.VolumeId]*Volume) + diskLocation.ecVolumes = make(map[needle.VolumeId]*erasure_coding.EcVolume) + + vifPath := erasure_coding.EcShardFileName("", dataDir, int(vid)) + ".vif" + ecxPath := erasure_coding.EcShardFileName("", idxDir, int(vid)) + ".ecx" + if err := os.WriteFile(vifPath, []byte{}, 0644); err != nil { + t.Fatalf("write .vif: %v", err) + } + if err := os.WriteFile(ecxPath, []byte{}, 0644); err != nil { + t.Fatalf("write .ecx: %v", err) + } + + entries, err := os.ReadDir(dataDir) + if err != nil { + t.Fatalf("read dir: %v", err) + } + var vifEntry os.DirEntry + for _, e := range entries { + if filepath.Ext(e.Name()) == ".vif" { + vifEntry = e + break + } + } + if vifEntry == nil { + t.Fatalf(".vif entry missing from dir listing") + } + + loaded := diskLocation.loadExistingVolume(vifEntry, NeedleMapInMemory, false, 0, 0) + if loaded { + t.Fatalf("loadExistingVolume should refuse to load a .vif-only entry when .ecx is present (volume %d)", vid) + } + if _, exists := diskLocation.volumes[vid]; exists { + t.Fatalf("volume %d should not be registered in l.volumes (would create phantom regular volume)", vid) + } + datPath := erasure_coding.EcShardFileName("", dataDir, int(vid)) + ".dat" + if util.FileExists(datPath) { + t.Fatalf("guard must not create a placeholder .dat for volume %d", vid) + } + }) + } +}