package weed_server import ( "context" "fmt" "io" "math" "os" "strings" "time" "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" "github.com/seaweedfs/seaweedfs/weed/storage/backend" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/operation" "github.com/seaweedfs/seaweedfs/weed/pb" "github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb" "github.com/seaweedfs/seaweedfs/weed/storage" "github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding" "github.com/seaweedfs/seaweedfs/weed/storage/idx" "github.com/seaweedfs/seaweedfs/weed/storage/needle" "github.com/seaweedfs/seaweedfs/weed/storage/types" "github.com/seaweedfs/seaweedfs/weed/util" ) const BufferSizeLimit = 1024 * 1024 * 2 // VolumeCopy copy the .idx .dat .vif files, and mount the volume func (vs *VolumeServer) VolumeCopy(req *volume_server_pb.VolumeCopyRequest, stream volume_server_pb.VolumeServer_VolumeCopyServer) error { if err := vs.checkGrpcAdminAuth(stream.Context()); err != nil { return err } if err := vs.CheckMaintenanceMode(); err != nil { return err } // A pre-existing local replica is NOT deleted up front. Deleting before the // source is confirmed reachable destroys a healthy copy on a transient // source outage (and, on retry, can lose the volume entirely). The delete is // deferred until ReadVolumeFileStatus below proves the source holds the // volume; readability alone is the gate (size/count comparisons invert after // divergent vacuum/compaction and would block valid re-replication). hasExistingVolume := vs.store.GetVolume(needle.VolumeId(req.VolumeId)) != nil // the master will not start compaction for read-only volumes, so it is safe to just copy files directly // copy .dat and .idx files // read .idx .dat file size and timestamp // send .idx file // send .dat file // confirm size and timestamp var volFileInfoResp *volume_server_pb.ReadVolumeFileStatusResponse var dataBaseFileName, indexBaseFileName, idxFileName, datFileName string var hasRemoteDatFile bool err := operation.WithVolumeServerClient(true, pb.ServerAddress(req.SourceDataNode), vs.grpcDialOption, func(client volume_server_pb.VolumeServerClient) error { var err error volFileInfoResp, err = client.ReadVolumeFileStatus(context.Background(), &volume_server_pb.ReadVolumeFileStatusRequest{ VolumeId: req.VolumeId, }) if nil != err { return fmt.Errorf("read volume file status failed, %w", err) } // Source is reachable and holds the volume: only now is it safe to drop // an existing local replica before overwriting its files. if hasExistingVolume { glog.V(0).Infof("volume %d already exists. deleting before copying from %s...", req.VolumeId, req.SourceDataNode) // keep remote data: the inbound copy carries a .vif that may point at // the same cloud-tier object the existing volume references. if delErr := vs.store.DeleteVolume(needle.VolumeId(req.VolumeId), false, true); delErr != nil { return fmt.Errorf("failed to delete existing volume %d: %v", req.VolumeId, delErr) } glog.V(0).Infof("deleted existing volume %d before copying.", req.VolumeId) } diskType := volFileInfoResp.DiskType if req.DiskType != "" { diskType = req.DiskType } hasRemoteDatFile = volFileInfoResp.VolumeInfo != nil && len(volFileInfoResp.VolumeInfo.Files) > 0 // a remote-backed volume only lands its .idx/.vif locally; the .dat stays in the tier neededSpace := volFileInfoResp.DatFileSize if hasRemoteDatFile { neededSpace = volFileInfoResp.IdxFileSize } location := vs.store.FindFreeLocation(func(location *storage.DiskLocation) bool { return location.DiskType == types.ToDiskType(diskType) && location.AvailableSpace.Load() > neededSpace }) if location == nil { return fmt.Errorf("%s %s", util.ErrVolumeNoSpaceLeft, types.ToDiskType(diskType).ReadableString()) } dataBaseFileName = storage.VolumeFileName(location.Directory, volFileInfoResp.Collection, int(req.VolumeId)) indexBaseFileName = storage.VolumeFileName(location.IdxDirectory, volFileInfoResp.Collection, int(req.VolumeId)) // The .note marks the copy as in-progress; a leftover note fails the // volume load on restart, so a write failure must abort the copy. if noteErr := util.WriteFile(dataBaseFileName+".note", []byte(fmt.Sprintf("copying from %s", req.SourceDataNode)), 0755); noteErr != nil { err = noteErr return fmt.Errorf("write .note for volume %d: %w", req.VolumeId, noteErr) } defer func() { if err != nil { os.Remove(dataBaseFileName + ".dat") os.Remove(indexBaseFileName + ".idx") os.Remove(dataBaseFileName + ".vif") os.Remove(dataBaseFileName + ".note") } }() var preallocateSize int64 if grpcErr := pb.WithMasterClient(context.Background(), false, vs.GetMaster(context.Background()), vs.grpcDialOption, false, func(client master_pb.SeaweedClient) error { resp, err := client.GetMasterConfiguration(context.Background(), &master_pb.GetMasterConfigurationRequest{}) if err != nil { return fmt.Errorf("get master %s configuration: %v", vs.GetMaster(context.Background()), err) } if resp.VolumePreallocate { preallocateSize = int64(resp.VolumeSizeLimitMB) * (1 << 20) } return nil }); grpcErr != nil { glog.V(0).Infof("connect to %s: %v", vs.GetMaster(context.Background()), grpcErr) } if preallocateSize > 0 && !hasRemoteDatFile { volumeFile := dataBaseFileName + ".dat" _, err := backend.CreateVolumeFile(volumeFile, preallocateSize, 0) if err != nil { return fmt.Errorf("create volume file %s: %v", volumeFile, err) } } // println("source:", volFileInfoResp.String()) copyResponse := &volume_server_pb.VolumeCopyResponse{} reportInterval := int64(1024 * 1024 * 128) nextReportTarget := reportInterval var modifiedTsNs int64 var sendErr error var ioBytePerSecond int64 if req.IoBytePerSecond <= 0 { ioBytePerSecond = vs.maintenanceBytePerSecond } else { ioBytePerSecond = req.IoBytePerSecond } throttler := util.NewWriteThrottler(ioBytePerSecond) if !hasRemoteDatFile { if modifiedTsNs, err = vs.doCopyFileWithThrottler(client, false, req.Collection, req.VolumeId, volFileInfoResp.CompactionRevision, volFileInfoResp.DatFileSize, dataBaseFileName, ".dat", false, true, func(processed int64) bool { if processed > nextReportTarget { copyResponse.ProcessedBytes = processed if sendErr = stream.Send(copyResponse); sendErr != nil { return false } nextReportTarget = processed + reportInterval } return true }, throttler); err != nil { return err } if sendErr != nil { return sendErr } if modifiedTsNs > 0 { os.Chtimes(dataBaseFileName+".dat", time.Unix(0, modifiedTsNs), time.Unix(0, modifiedTsNs)) } } if modifiedTsNs, err = vs.doCopyFileWithThrottler(client, false, req.Collection, req.VolumeId, volFileInfoResp.CompactionRevision, volFileInfoResp.IdxFileSize, indexBaseFileName, ".idx", false, false, nil, throttler); err != nil { return err } if modifiedTsNs > 0 { os.Chtimes(indexBaseFileName+".idx", time.Unix(0, modifiedTsNs), time.Unix(0, modifiedTsNs)) } if modifiedTsNs, err = vs.doCopyFileWithThrottler(client, false, req.Collection, req.VolumeId, volFileInfoResp.CompactionRevision, 1024*1024, dataBaseFileName, ".vif", false, true, nil, throttler); err != nil { return err } if modifiedTsNs > 0 { os.Chtimes(dataBaseFileName+".vif", time.Unix(0, modifiedTsNs), time.Unix(0, modifiedTsNs)) } // A leftover .note fails the load on the next restart, so a removal // failure must fail the copy rather than be silently swallowed. if noteErr := os.Remove(dataBaseFileName + ".note"); noteErr != nil && !os.IsNotExist(noteErr) { err = noteErr return fmt.Errorf("remove .note for volume %d: %w", req.VolumeId, noteErr) } return nil }) if err != nil { return err } if dataBaseFileName == "" { return fmt.Errorf("not found volume %d file", req.VolumeId) } idxFileName = indexBaseFileName + ".idx" datFileName = dataBaseFileName + ".dat" defer func() { if err != nil && dataBaseFileName != "" { os.Remove(idxFileName) os.Remove(datFileName) os.Remove(dataBaseFileName + ".vif") } }() if err = checkCopyFiles(volFileInfoResp, hasRemoteDatFile, idxFileName, datFileName); err != nil { // added by panyc16 return err } var lastAppendAtNs = volFileInfoResp.DatFileTimestampSeconds * uint64(time.Second) if !hasRemoteDatFile { if appendAtNs, appendErr := findLastAppendAtNsFromCopiedFiles(idxFileName, datFileName, needle.Version(volFileInfoResp.Version)); appendErr == nil && appendAtNs > 0 { lastAppendAtNs = appendAtNs } else if appendErr != nil { glog.V(1).Infof("failed to find last append timestamp for volume %d: %v", req.VolumeId, appendErr) } } // mount the volume err = vs.store.MountVolume(needle.VolumeId(req.VolumeId)) if err != nil { return fmt.Errorf("failed to mount volume %d: %v", req.VolumeId, err) } if err = stream.Send(&volume_server_pb.VolumeCopyResponse{ LastAppendAtNs: lastAppendAtNs, }); err != nil { glog.Errorf("send response: %v", err) } return err } func (vs *VolumeServer) doCopyFile(client volume_server_pb.VolumeServerClient, isEcVolume bool, collection string, vid, compactRevision uint32, stopOffset uint64, baseFileName, ext string, isAppend, ignoreSourceFileNotFound bool, progressFn storage.ProgressFunc) (modifiedTsNs int64, err error) { return vs.doCopyFileWithThrottler(client, isEcVolume, collection, vid, compactRevision, stopOffset, baseFileName, ext, isAppend, ignoreSourceFileNotFound, progressFn, util.NewWriteThrottler(vs.maintenanceBytePerSecond)) } func (vs *VolumeServer) doCopyFileWithThrottler(client volume_server_pb.VolumeServerClient, isEcVolume bool, collection string, vid, compactRevision uint32, stopOffset uint64, baseFileName, ext string, isAppend, ignoreSourceFileNotFound bool, progressFn storage.ProgressFunc, throttler *util.WriteThrottler) (modifiedTsNs int64, err error) { copyFileClient, err := client.CopyFile(context.Background(), &volume_server_pb.CopyFileRequest{ VolumeId: vid, Ext: ext, CompactionRevision: compactRevision, StopOffset: stopOffset, Collection: collection, IsEcVolume: isEcVolume, IgnoreSourceFileNotFound: ignoreSourceFileNotFound, }) if err != nil { return modifiedTsNs, fmt.Errorf("failed to start copying volume %d %s file: %v", vid, ext, err) } modifiedTsNs, err = writeToFile(copyFileClient, baseFileName+ext, throttler, isAppend, ignoreSourceFileNotFound, progressFn) if err != nil { return modifiedTsNs, fmt.Errorf("failed to copy %s file: %v", baseFileName+ext, err) } return modifiedTsNs, nil } /* * only check the differ of the file size todo: maybe should check the received count and deleted count of the volume */ func checkCopyFiles(originFileInf *volume_server_pb.ReadVolumeFileStatusResponse, hasRemoteDatFile bool, idxFileName, datFileName string) error { stat, err := os.Stat(idxFileName) if err != nil { // If the idx file doesn't exist but the expected size is 0, that's OK (empty volume) if os.IsNotExist(err) && originFileInf.IdxFileSize == 0 { // empty volume, idx file not needed } else { return fmt.Errorf("stat idx file %s failed: %v", idxFileName, err) } } else if originFileInf.IdxFileSize != uint64(stat.Size()) { return fmt.Errorf("idx file %s size [%v] is not same as origin file size [%v]", idxFileName, stat.Size(), originFileInf.IdxFileSize) } if hasRemoteDatFile { return nil } stat, err = os.Stat(datFileName) if err != nil { return fmt.Errorf("get dat file info failed, %w", err) } if originFileInf.DatFileSize != uint64(stat.Size()) { return fmt.Errorf("the dat file size [%v] is not same as origin file size [%v]", stat.Size(), originFileInf.DatFileSize) } return nil } func findLastAppendAtNsFromCopiedFiles(idxFileName, datFileName string, version needle.Version) (uint64, error) { if version < needle.Version3 { return 0, nil } idxFile, err := os.Open(idxFileName) if err != nil { return 0, fmt.Errorf("open idx file %s: %w", idxFileName, err) } defer idxFile.Close() fi, err := idxFile.Stat() if err != nil { return 0, fmt.Errorf("stat idx file %s: %w", idxFileName, err) } if fi.Size() == 0 { return 0, nil } if fi.Size()%int64(types.NeedleMapEntrySize) != 0 { return 0, fmt.Errorf("unexpected idx file %s size: %d", idxFileName, fi.Size()) } buf := make([]byte, types.NeedleMapEntrySize) if _, err := idxFile.ReadAt(buf, fi.Size()-int64(types.NeedleMapEntrySize)); err != nil { return 0, fmt.Errorf("read idx file %s: %w", idxFileName, err) } _, offset, _ := idx.IdxFileEntry(buf) if offset.IsZero() { return 0, nil } datFile, err := os.Open(datFileName) if err != nil { return 0, fmt.Errorf("open dat file %s: %w", datFileName, err) } defer datFile.Close() datBackend := backend.NewDiskFile(datFile) n, _, _, err := needle.ReadNeedleHeader(datBackend, version, offset.ToActualOffset()) if err != nil { return 0, fmt.Errorf("read needle header %s offset %d: %w", datFileName, offset.ToActualOffset(), err) } tailOffset := offset.ToActualOffset() + int64(types.NeedleHeaderSize) + int64(n.Size) tail := make([]byte, needle.NeedleChecksumSize+types.TimestampSize) readCount, readErr := datBackend.ReadAt(tail, tailOffset) if readErr == io.EOF && readCount == len(tail) { readErr = nil } if readErr != nil { return 0, fmt.Errorf("read needle tail %s offset %d: %w", datFileName, tailOffset, readErr) } return util.BytesToUint64(tail[needle.NeedleChecksumSize : needle.NeedleChecksumSize+types.TimestampSize]), nil } func writeToFile(client volume_server_pb.VolumeServer_CopyFileClient, fileName string, wt *util.WriteThrottler, isAppend, ignoreSourceFileNotFound bool, progressFn storage.ProgressFunc) (modifiedTsNs int64, err error) { glog.V(4).Infof("writing to %s", fileName) // For an optional copy (ignoreSourceFileNotFound), stage into a temp sibling // and atomically rename on success, so a source that lacks the file cannot // truncate a valid pre-existing destination. Mandatory copies write in place. writePath := fileName stageThenCommit := ignoreSourceFileNotFound && !isAppend if stageThenCommit { writePath = fileName + ".copying" } flags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC if isAppend { flags = os.O_WRONLY | os.O_CREATE } dst, err := os.OpenFile(writePath, flags, 0644) if err != nil { return modifiedTsNs, fmt.Errorf("open file %s: %w", writePath, err) } // Track the destination handle through a closer that runs at most once. // On Windows os.Remove fails while the file is still open, so any path // that wants to delete the file we just created must close the handle // first. The deferred call here is the safety net for normal returns. dstClosed := false closeDst := func() { if dstClosed { return } dstClosed = true _ = dst.Close() } defer closeDst() // removeIncomplete deletes the partially-written file we just opened // with O_TRUNC. Used on stream / write / cancellation errors so a // caller (notably VolumeEcShardsCopy distributing .ecx) doesn't end // up with a 0-byte stub that downstream code mistakes for a valid // empty file. Skip in isAppend mode — the existing content is not // ours to remove, and resumable appends rely on partial state. removeIncomplete := func(reason string) { if isAppend { return } closeDst() if removeErr := os.Remove(writePath); removeErr != nil && !os.IsNotExist(removeErr) { glog.Warningf("failed to remove incomplete file %s after %s: %v", writePath, reason, removeErr) } else if removeErr == nil { glog.V(1).Infof("removed incomplete file %s after %s", writePath, reason) } } var progressedBytes int64 for { resp, receiveErr := client.Recv() if receiveErr == io.EOF { break } if resp != nil && resp.ModifiedTsNs != 0 { modifiedTsNs = resp.ModifiedTsNs } if receiveErr != nil { removeIncomplete("receive error") return modifiedTsNs, fmt.Errorf("receiving %s: %w", fileName, receiveErr) } if _, writeErr := dst.Write(resp.FileContent); writeErr != nil { removeIncomplete("write error") return modifiedTsNs, fmt.Errorf("write file %s: %w", fileName, writeErr) } progressedBytes += int64(len(resp.FileContent)) if progressFn != nil { if !progressFn(progressedBytes) { removeIncomplete("progress cancelled") return modifiedTsNs, fmt.Errorf("interrupted copy operation") } } wt.MaybeSlowdown(int64(len(resp.FileContent))) } // If we never received a modifiedTsNs, it means the source file did not exist. // Remove the empty file we created to avoid leaving corrupted empty files. // Note: We check modifiedTsNs (not progressedBytes) because an empty source file // is valid and should result in an empty destination file. if modifiedTsNs == 0 && !isAppend { closeDst() if removeErr := os.Remove(writePath); removeErr != nil && !os.IsNotExist(removeErr) { glog.V(1).Infof("failed to remove empty file %s: %v", writePath, removeErr) } else if removeErr == nil { glog.V(1).Infof("removed empty file %s (source file not found)", writePath) } return modifiedTsNs, nil } // Commit the staged temp into place. if stageThenCommit { closeDst() if renameErr := os.Rename(writePath, fileName); renameErr != nil { os.Remove(writePath) return modifiedTsNs, fmt.Errorf("commit copied file %s: %w", fileName, renameErr) } } return modifiedTsNs, nil } func (vs *VolumeServer) ReadVolumeFileStatus(ctx context.Context, req *volume_server_pb.ReadVolumeFileStatusRequest) (*volume_server_pb.ReadVolumeFileStatusResponse, error) { resp := &volume_server_pb.ReadVolumeFileStatusResponse{} v := vs.store.GetVolume(needle.VolumeId(req.VolumeId)) if v == nil { return nil, fmt.Errorf("not found volume id %d", req.VolumeId) } resp.VolumeId = req.VolumeId datSize, idxSize, modTime := v.FileStat() resp.DatFileSize = datSize resp.IdxFileSize = idxSize resp.DatFileTimestampSeconds = uint64(modTime.Unix()) resp.IdxFileTimestampSeconds = uint64(modTime.Unix()) resp.FileCount = v.FileCount() resp.CompactionRevision = uint32(v.CompactionRevision) resp.Collection = v.Collection resp.DiskType = string(v.DiskType()) resp.VolumeInfo = v.GetVolumeInfo() resp.Version = uint32(v.Version()) return resp, nil } // checkVolumeFileExtension guards the client-supplied Ext that CopyFile and // ReceiveFile turn into an on-disk path. Both RPCs are intentionally ungated // for cluster-internal peers (see volume_grpc_admin_auth_coverage_test.go), so // this is the only check standing between a peer request and the os.Open / // os.Create target: without it an Ext like "/../../x" is joined onto the volume // directory and, once path-cleaned, resolves outside it. A genuine extension is // a leading dot followed by alphanumerics -- ".dat", ".idx", ".vif", ".ecx", // ".ecj", ".ecsum", ".ec00".. -- and never contains a separator or "..". func checkVolumeFileExtension(ext string) error { if len(ext) < 2 || ext[0] != '.' { return fmt.Errorf("invalid file extension %q", ext) } for _, r := range ext[1:] { if r < '0' || (r > '9' && r < 'A') || (r > 'Z' && r < 'a') || r > 'z' { return fmt.Errorf("invalid file extension %q", ext) } } return nil } // checkVolumeCollection guards the client-supplied Collection, which CopyFile // and ReceiveFile fold into a path component ("_"). An empty // collection is the default; any other value must be a single path element so a // collection like "../../x" cannot climb out of the volume directory once // path-cleaned. Collection names are user-facing and may hold '.' or '-', so // this rejects only separators and bare parent references rather than the // stricter alphanumeric rule used for extensions. func checkVolumeCollection(collection string) error { if collection == "." || collection == ".." || strings.ContainsAny(collection, `/\`) { return fmt.Errorf("invalid collection %q", collection) } return nil } // CopyFile client pulls the volume related file from the source server. // if req.CompactionRevision != math.MaxUint32, it ensures the compact revision is as expected // The copying still stop at req.StopOffset, but you can set it to math.MaxUint64 in order to read all data. func (vs *VolumeServer) CopyFile(req *volume_server_pb.CopyFileRequest, stream volume_server_pb.VolumeServer_CopyFileServer) error { if err := checkVolumeFileExtension(req.Ext); err != nil { return err } if err := checkVolumeCollection(req.Collection); err != nil { return err } var fileName string if !req.IsEcVolume { v := vs.store.GetVolume(needle.VolumeId(req.VolumeId)) if v == nil { return fmt.Errorf("not found volume id %d", req.VolumeId) } if uint32(v.CompactionRevision) != req.CompactionRevision && req.CompactionRevision != math.MaxUint32 { return fmt.Errorf("volume %d is compacted", req.VolumeId) } v.SyncToDisk() fileName = v.FileName(req.Ext) } else { // Sync EC volume files to disk before copying to ensure deletions are visible // This fixes issue #7751 where deleted files in encoded volumes were not // properly marked as deleted when decoded. if ecVolume, found := vs.store.FindEcVolume(needle.VolumeId(req.VolumeId)); found { ecVolume.Sync() } baseFileName := erasure_coding.EcShardBaseFileName(req.Collection, int(req.VolumeId)) + req.Ext for _, location := range vs.store.Locations { tName := util.Join(location.Directory, baseFileName) if util.FileExists(tName) { fileName = tName } tName = util.Join(location.IdxDirectory, baseFileName) if util.FileExists(tName) { fileName = tName } } if fileName == "" { if req.IgnoreSourceFileNotFound { return nil } return fmt.Errorf("CopyFile not found ec volume id %d", req.VolumeId) } } bytesToRead := int64(req.StopOffset) file, err := os.Open(fileName) if err != nil { if os.IsNotExist(err) { // If file doesn't exist and we're asked to copy 0 bytes (empty file), // or if IgnoreSourceFileNotFound is set, treat as success if req.IgnoreSourceFileNotFound || req.StopOffset == 0 { return nil } } return err } defer file.Close() fileInfo, err := file.Stat() if err != nil { return err } fileModTsNs := fileInfo.ModTime().UnixNano() buffer := make([]byte, BufferSizeLimit) for bytesToRead > 0 { bytesread, err := file.Read(buffer) // println(fileName, "read", bytesread, "bytes, with target", bytesToRead) if err != nil { if err != io.EOF { return err } // println(fileName, "read", bytesread, "bytes, with target", bytesToRead, "err", err.Error()) break } if int64(bytesread) > bytesToRead { bytesread = int(bytesToRead) } err = stream.Send(&volume_server_pb.CopyFileResponse{ FileContent: buffer[:bytesread], ModifiedTsNs: fileModTsNs, }) if err != nil { // println("sending", bytesread, "bytes err", err.Error()) return err } fileModTsNs = 0 // only send once bytesToRead -= int64(bytesread) } // If no data has been sent in the loop (e.g. for an empty file, or when stopOffset is 0), // we still need to send the ModifiedTsNs so the client knows the source file exists. // fileModTsNs is set to 0 after the first send, so if it's still non-zero, // we haven't sent anything yet. if fileModTsNs != 0 { err = stream.Send(&volume_server_pb.CopyFileResponse{ ModifiedTsNs: fileModTsNs, }) if err != nil { return err } } return nil } // diskHoldsEcShardFile reports whether dir contains any .ecNN shard file, // so a decoded .dat is never staged beside a shard. Unlike FindEcVolume, // it also catches shards present on disk but not mounted. func diskHoldsEcShardFile(dir, collection string, vid needle.VolumeId) bool { base := erasure_coding.EcShardFileName(collection, dir, int(vid)) for i := 0; i < erasure_coding.MaxShardCount; i++ { if fi, err := os.Stat(base + erasure_coding.ToExt(i)); err == nil && !fi.IsDir() && fi.Size() > 0 { return true } } return false } // ReceiveFile receives a file stream from client and writes it to storage func (vs *VolumeServer) ReceiveFile(stream volume_server_pb.VolumeServer_ReceiveFileServer) error { if err := vs.CheckMaintenanceMode(); err != nil { return err } var fileInfo *volume_server_pb.ReceiveFileInfo var targetFile *os.File var filePath string var bytesWritten uint64 defer func() { if targetFile != nil { targetFile.Close() } }() for { req, err := stream.Recv() if err == io.EOF { // Stream completed successfully if targetFile != nil { targetFile.Sync() glog.V(1).Infof("Successfully received file %s (%d bytes)", filePath, bytesWritten) } return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{ BytesWritten: bytesWritten, }) } if err != nil { // Clean up on error if targetFile != nil { targetFile.Close() os.Remove(filePath) } glog.Errorf("Failed to receive stream: %v", err) return fmt.Errorf("failed to receive stream: %v", err) } switch data := req.Data.(type) { case *volume_server_pb.ReceiveFileRequest_Info: // First message contains file info fileInfo = data.Info glog.V(1).Infof("ReceiveFile: volume %d, ext %s, collection %s, shard %d, size %d", fileInfo.VolumeId, fileInfo.Ext, fileInfo.Collection, fileInfo.ShardId, fileInfo.FileSize) if err := checkVolumeFileExtension(fileInfo.Ext); err != nil { glog.Errorf("ReceiveFile: %v", err) return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{ Error: err.Error(), }) } if err := checkVolumeCollection(fileInfo.Collection); err != nil { glog.Errorf("ReceiveFile: %v", err) return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{ Error: err.Error(), }) } if fileInfo.IsEcVolume { // os.Create below truncates in place; a mounted EcVolume // holds fds on the same inodes, so overwriting corrupts // live readers. if _, mounted := vs.store.FindEcVolume(needle.VolumeId(fileInfo.VolumeId)); mounted { mountedDisks := vs.store.FindEcVolumeDiskIds(needle.VolumeId(fileInfo.VolumeId)) glog.Errorf("ReceiveFile: ec volume %d is mounted on disk_ids:%v; refusing overwrite for %s", fileInfo.VolumeId, mountedDisks, fileInfo.Ext) return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{ Error: fmt.Sprintf("ec volume %d is mounted on disk_ids:%v; unmount before ReceiveFile", fileInfo.VolumeId, mountedDisks), }) } // disk_id=0 means "unset" (protobuf default), so auto-select // using the same primitive as VolumeEcShardsCopy: prefer a // disk that has the EC volume mounted, then a disk that owns // the .ecx on disk (the volume hasn't been mounted yet — // relevant when shards stream in mid-rebuild before any // mount has happened; see #9212), then any HDD, then any // disk. var targetLocation *storage.DiskLocation if fileInfo.DiskId > 0 { if fileInfo.DiskId >= uint32(len(vs.store.Locations)) { glog.Errorf("ReceiveFile: invalid disk_id %d: only have %d disks", fileInfo.DiskId, len(vs.store.Locations)) return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{ Error: fmt.Sprintf("invalid disk_id %d: only have %d disks", fileInfo.DiskId, len(vs.store.Locations)), }) } targetLocation = vs.store.Locations[fileInfo.DiskId] } else { // Pass the build's default data-shard count for the helper's // free-slot maths; it's a parameter so custom-ratio builds // (e.g. enterprise) can swap it without touching this file. targetLocation = vs.store.FindEcShardTargetLocation(fileInfo.Collection, needle.VolumeId(fileInfo.VolumeId), erasure_coding.DataShardsCount) } if targetLocation == nil { glog.Errorf("ReceiveFile: no storage location available") return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{ Error: "no storage location available", }) } // Create EC shard file path baseFileName := erasure_coding.EcShardBaseFileName(fileInfo.Collection, int(fileInfo.VolumeId)) filePath = util.Join(targetLocation.Directory, baseFileName+fileInfo.Ext) } else { // Regular volume file v := vs.store.GetVolume(needle.VolumeId(fileInfo.VolumeId)) if v == nil { if fileInfo.DiskType == "" { glog.Errorf("ReceiveFile: volume %d not found", fileInfo.VolumeId) return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{ Error: fmt.Sprintf("volume %d not found", fileInfo.VolumeId), }) } // Staged-new-volume mode (EC decode onto a clean peer): the // volume does not exist here yet. Pick a free-slot disk location // of the requested medium and stage the file as // .copying, to be renamed into place and mounted by // VolumeEcShardsToVolume(from_staged). .idx.copying/.vif.copying // are not valid volume names, so the scanner never half-loads. want := types.ToDiskType(fileInfo.DiskType) stagedVid := needle.VolumeId(fileInfo.VolumeId) loc := vs.store.FindFreeLocation(func(l *storage.DiskLocation) bool { if l.DiskType != want { return false } // Don't stage the decoded .dat onto a disk that holds a shard // of this vid. Check the mounted map and the on-disk files, so // an unmounted or orphan shard (on disk, absent from the map) // is caught too. if _, holds := l.FindEcVolume(stagedVid); holds { return false } return !diskHoldsEcShardFile(l.Directory, fileInfo.Collection, stagedVid) }) if loc == nil { return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{ Error: fmt.Sprintf("no %s disk location with a free slot for volume %d", fileInfo.DiskType, fileInfo.VolumeId), }) } filePath = storage.VolumeFileName(loc.Directory, fileInfo.Collection, int(fileInfo.VolumeId)) + fileInfo.Ext + ".copying" } else { filePath = v.FileName(fileInfo.Ext) } } // Create target file targetFile, err = os.Create(filePath) if err != nil { glog.Errorf("ReceiveFile: failed to create file %s: %v", filePath, err) return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{ Error: fmt.Sprintf("failed to create file: %v", err), }) } glog.V(1).Infof("ReceiveFile: created target file %s", filePath) case *volume_server_pb.ReceiveFileRequest_FileContent: // Subsequent messages contain file content if targetFile == nil { glog.Errorf("ReceiveFile: file info must be sent first") return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{ Error: "file info must be sent first", }) } n, err := targetFile.Write(data.FileContent) if err != nil { targetFile.Close() os.Remove(filePath) glog.Errorf("ReceiveFile: failed to write to file %s: %v", filePath, err) return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{ Error: fmt.Sprintf("failed to write file: %v", err), }) } bytesWritten += uint64(n) glog.V(2).Infof("ReceiveFile: wrote %d bytes to %s (total: %d)", n, filePath, bytesWritten) default: glog.Errorf("ReceiveFile: unknown message type") return stream.SendAndClose(&volume_server_pb.ReceiveFileResponse{ Error: "unknown message type", }) } } }