You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by fr...@apache.org on 2025/06/27 13:44:53 UTC

(cassandra-sidecar) branch trunk updated: CASSSIDECAR-223: Create API that allows downloading of files listed by Live Migration (#215)

This is an automated email from the ASF dual-hosted git repository.

frankgh pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 64f7dcd2 CASSSIDECAR-223: Create API that allows downloading of files listed by Live Migration (#215)
64f7dcd2 is described below

commit 64f7dcd26f4aaa4a38c475269aa49503327d39e4
Author: N V Harikrishna <n....@gmail.com>
AuthorDate: Fri Jun 27 19:14:48 2025 +0530

    CASSSIDECAR-223: Create API that allows downloading of files listed by Live Migration (#215)
    
    Patch by Venkata Harikrishna Nukala; reviewed by Yifan Cai, Francisco Guerrero for CASSSIDECAR-223
---
 CHANGES.txt                                        |   1 +
 .../cassandra/sidecar/common/ApiEndpointsV1.java   |  16 +-
 .../acl/authorization/BasicPermissions.java        |   1 +
 .../livemigration/LiveMigrationDirType.java        |  57 +++
 .../LiveMigrationFileStreamHandler.java            | 256 ++++++++++++
 .../LiveMigrationInstanceMetadataUtil.java         |  58 ++-
 .../LiveMigrationPlaceholderUtil.java              |  42 +-
 .../sidecar/modules/LiveMigrationModule.java       |  16 +
 .../modules/multibindings/VertxRouteMapKeys.java   |   5 +
 .../LiveMigrationFileStreamHandlerTest.java        | 439 +++++++++++++++++++++
 .../LiveMigrationListInstanceFilesHandlerTest.java |   8 +-
 .../CassandraInstanceFilesImplTest.java            |  12 +-
 .../LiveMigrationInstanceMetadataUtilTest.java     |  64 ++-
 .../LiveMigrationPlaceholderUtilTest.java          |  95 +++++
 14 files changed, 1039 insertions(+), 31 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 3b6984e1..b342cfe3 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 0.2.0
 -----
+ * Add API that allows downloading of files listed by Live Migration (CASSSIDECAR-223)
  * Fixing settings endpoint should return Service Unavailable when Cassandra instance is down (CASSSIDECAR-264)
  * Add start/stop binary transport&gossip endpoints (CASSSIDECAR-253)
  * Support a flag & implementation for only forward DNS resolution (CASSSIDECAR-263)
diff --git a/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java b/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
index cd47c812..9da97827 100644
--- a/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
+++ b/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
@@ -148,14 +148,14 @@ public final class ApiEndpointsV1
 
     public static final String LIVE_MIGRATION_FILES_API = LIVE_MIGRATION_API_PREFIX + "/files";
 
-    public static final String LIVE_MIGRATION_CDC_RAW_DIR_PATH = LIVE_MIGRATION_FILES_API + "/cdc_raw";
-    public static final String LIVE_MIGRATION_COMMITLOG_DIR_PATH = LIVE_MIGRATION_FILES_API + "/commitlog";
-    public static final String LIVE_MIGRATION_DATA_FILE_DIR_PATH = LIVE_MIGRATION_FILES_API + "/data";
-    public static final String LIVE_MIGRATION_HINTS_DIR_PATH = LIVE_MIGRATION_FILES_API + "/hints";
-    public static final String LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH = LIVE_MIGRATION_FILES_API
-                                                                                + "/local_system_data";
-    public static final String LIVE_MIGRATION_SAVED_CACHES_DIR_PATH = LIVE_MIGRATION_FILES_API + "/saved_caches";
-
+    public static final String DIR_TYPE_PARAM = "dirType";
+    public static final String DIR_INDEX_PARAM = "dirIndex";
+    // API endpoint allows files transfer of specific directories handled by Cassandra during live migration operation.
+    // dirType path parameter    : The type of directory (data, commitlog, etc.)
+    // dirIndex path parameter   : The index of the directory
+    // The remaining path ('/*') : Represents the relative path within the specified directory
+    public static final String LIVE_MIGRATION_FILE_TRANSFER_API = LIVE_MIGRATION_FILES_API + "/:" + DIR_TYPE_PARAM
+                                                                  + "/:" + DIR_INDEX_PARAM + "/*";
 
     private ApiEndpointsV1()
     {
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java b/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
index a9ec98bf..0a809b1b 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
@@ -83,4 +83,5 @@ public class BasicPermissions
 
     // Live Migration permissions
     public static final Permission LIST_FILES = new DomainAwarePermission("LIVE_MIGRATION:LIST_FILES", CLUSTER_SCOPE);
+    public static final Permission STREAM_FILES = new DomainAwarePermission("LIVE_MIGRATION:STREAM", CLUSTER_SCOPE);
 }
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationDirType.java b/server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationDirType.java
new file mode 100644
index 00000000..18f724c2
--- /dev/null
+++ b/server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationDirType.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.handlers.livemigration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Enum for holding different type of directories handled by Live Migration.
+ */
+public enum LiveMigrationDirType
+{
+    CDC_RAW_DIR("cdc_raw"),
+    COMMIT_LOG_DIR("commitlog"),
+    DATA_FIlE_DIR("data"),
+    HINTS_DIR("hints"),
+    LOCAL_SYSTEM_DATA_FILE_DIR("local_system_data"),
+    SAVED_CACHES_DIR("saved_caches");
+
+    private static final Map<String, LiveMigrationDirType> DIR_TYPE_MAP = new HashMap<>();
+
+    static
+    {
+        for (LiveMigrationDirType type : values())
+        {
+            DIR_TYPE_MAP.put(type.dirType, type);
+        }
+    }
+
+    public final String dirType;
+
+    LiveMigrationDirType(String dirType)
+    {
+        this.dirType = dirType;
+    }
+
+    public static LiveMigrationDirType find(String dirType)
+    {
+        return DIR_TYPE_MAP.get(dirType);
+    }
+}
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandler.java b/server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandler.java
new file mode 100644
index 00000000..902223fd
--- /dev/null
+++ b/server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandler.java
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.handlers.livemigration;
+
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Inject;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.net.SocketAddress;
+import io.vertx.ext.auth.authorization.Authorization;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.handler.HttpException;
+import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.config.LiveMigrationConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.handlers.AbstractHandler;
+import org.apache.cassandra.sidecar.handlers.AccessProtected;
+import org.apache.cassandra.sidecar.handlers.FileStreamHandler;
+import org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil;
+import org.apache.cassandra.sidecar.utils.CassandraInputValidator;
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_INDEX_PARAM;
+import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_TYPE_PARAM;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.replacePlaceholder;
+
+/**
+ * Handler that allows Cassandra instance files to be downloaded during LiveMigration. This handler
+ * doesn't stream the file but relies on {@link FileStreamHandler} to do so. This handler doesn't allow
+ * using "/.." in the path to access files. This handler does not serve files which are excluded in
+ * Live Migration configuration.
+ */
+public class LiveMigrationFileStreamHandler extends AbstractHandler<Void> implements AccessProtected
+{
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(LiveMigrationFileStreamHandler.class);
+
+    private final Map<Integer, List<PathMatcher>> fileExclusionsByInstanceId = new ConcurrentHashMap<>();
+    private final Map<Integer, List<PathMatcher>> dirExclusionsByInstanceId = new ConcurrentHashMap<>();
+    private final LiveMigrationConfiguration liveMigrationConfiguration;
+
+    @Inject
+    public LiveMigrationFileStreamHandler(InstanceMetadataFetcher metadataFetcher,
+                                          ExecutorPools executorPools,
+                                          CassandraInputValidator validator,
+                                          SidecarConfiguration sidecarConfiguration)
+    {
+        super(metadataFetcher, executorPools, validator);
+        this.liveMigrationConfiguration = sidecarConfiguration.liveMigrationConfiguration();
+    }
+
+    @Override
+    protected Void extractParamsOrThrow(RoutingContext context)
+    {
+        String dirType = context.pathParam(DIR_TYPE_PARAM);
+        if (null == dirType || dirType.isEmpty() || null == LiveMigrationDirType.find(dirType))
+        {
+            throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directory type: " + dirType);
+        }
+
+        String dirIndex = context.pathParam(DIR_INDEX_PARAM);
+        int index;
+        try
+        {
+            index = Integer.parseInt(dirIndex);
+        }
+        catch (NumberFormatException formatException)
+        {
+            throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directoryIndex: " + dirIndex);
+        }
+        if (index < 0)
+        {
+            throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directoryIndex: " + dirIndex);
+        }
+
+        // Path params are not used further, hence returning null.
+        return null;
+    }
+
+    @Override
+    protected void handleInternal(RoutingContext rc,
+                                  HttpServerRequest httpRequest,
+                                  @NotNull String host,
+                                  SocketAddress remoteAddress,
+                                  @Nullable Void request)
+    {
+        String reqPath = URLDecoder.decode(rc.request().path(), StandardCharsets.UTF_8);
+
+        if (reqPath.contains("/../") || reqPath.endsWith("/.."))
+        {
+            LOGGER.warn("Tried to access file using relative path({}). Rejecting the request.", reqPath);
+            rc.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end();
+            return;
+        }
+
+        InstanceMetadata instanceMeta = metadataFetcher.instance(host);
+        String normalizedPath = rc.normalizedPath();
+        String localFile;
+
+        try
+        {
+            localFile = LiveMigrationInstanceMetadataUtil.localPath(normalizedPath, instanceMeta);
+        }
+        catch (IllegalArgumentException e)
+        {
+            LOGGER.warn("Invalid path", e);
+            rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
+            return;
+        }
+
+        Path path = Paths.get(localFile);
+
+        executorPools.service()
+                     .executeBlocking(() -> isInvalidPath(rc, path, reqPath, instanceMeta))
+                     .onSuccess(invalid -> {
+                         if (!invalid)
+                         {
+                             rc.put(FileStreamHandler.FILE_PATH_CONTEXT_KEY, localFile);
+                             rc.next();
+                         }
+                     });
+    }
+
+    private boolean isInvalidPath(RoutingContext rc, Path path, String reqPath, InstanceMetadata instanceMeta)
+    {
+        if (!Files.exists(path))
+        {
+            LOGGER.info("Requested file is not found. file={}", path);
+            rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
+            return true;
+        }
+        if (Files.isDirectory(path))
+        {
+            LOGGER.info("Cannot transfer directory. path={}.", reqPath);
+            rc.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end();
+            return true;
+        }
+        if (isExcluded(path, instanceMeta))
+        {
+            LOGGER.debug("Requested path or one of its parent directories is excluded from Live Migration. " +
+                         "path={}", reqPath);
+            rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isExcluded(Path localFile, InstanceMetadata instanceMetadata)
+    {
+        return isFileExcluded(localFile, instanceMetadata) || isDirExcluded(localFile.getParent(), instanceMetadata);
+    }
+
+    private boolean isFileExcluded(Path localFile, InstanceMetadata instanceMetadata)
+    {
+        List<PathMatcher> fileExclusionMatchers = getPathMatchers(fileExclusionsByInstanceId,
+                                                                  instanceMetadata,
+                                                                  LiveMigrationConfiguration::filesToExclude);
+
+        return isMatch(localFile, fileExclusionMatchers);
+    }
+
+    private boolean isDirExcluded(Path dir, InstanceMetadata instanceMetadata)
+    {
+        List<PathMatcher> dirExclusionMatchers = getPathMatchers(dirExclusionsByInstanceId,
+                                                                 instanceMetadata,
+                                                                 LiveMigrationConfiguration::directoriesToExclude);
+
+        // Recursively check all parent directories to see if they are excluded or not.
+        while (dir != null)
+        {
+            if (isMatch(dir, dirExclusionMatchers))
+            {
+                return true;
+            }
+            dir = dir.getParent();
+        }
+
+        return false;
+    }
+
+    private List<PathMatcher> getPathMatchers(Map<Integer, List<PathMatcher>> map,
+                                              InstanceMetadata instanceMetadata,
+                                              Function<LiveMigrationConfiguration, Set<String>> exclusionProvider)
+    {
+        return map.computeIfAbsent(instanceMetadata.id(), id -> {
+            Set<String> exclusions = exclusionProvider.apply(liveMigrationConfiguration);
+            List<PathMatcher> matchers = new ArrayList<>(exclusions.size());
+            for (String placeholderPattern : exclusions)
+            {
+                Set<String> filePatterns = replacePlaceholder(placeholderPattern, instanceMetadata);
+                filePatterns.forEach(filePattern -> matchers.add(FileSystems.getDefault().getPathMatcher(filePattern)));
+            }
+            return matchers;
+        });
+    }
+
+    private boolean isMatch(Path localFile, List<PathMatcher> pathMatchers)
+    {
+        if (null == pathMatchers || pathMatchers.isEmpty())
+        {
+            return false;
+        }
+
+        for (PathMatcher pathMatcher : pathMatchers)
+        {
+            if (pathMatcher.matches(localFile))
+            {
+                LOGGER.debug("Requested file is excluded from Live Migration. file={}", localFile);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public Set<Authorization> requiredAuthorizations()
+    {
+        return Set.of(BasicPermissions.STREAM_FILES.toAuthorization());
+    }
+}
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtil.java b/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtil.java
index ae2d814f..826ed1fe 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtil.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtil.java
@@ -31,14 +31,15 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
 import org.jetbrains.annotations.NotNull;
 
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_CDC_RAW_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_HINTS_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
+import static org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationDirType.CDC_RAW_DIR;
+import static org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationDirType.COMMIT_LOG_DIR;
+import static org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationDirType.DATA_FIlE_DIR;
+import static org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationDirType.HINTS_DIR;
+import static org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationDirType.LOCAL_SYSTEM_DATA_FILE_DIR;
+import static org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationDirType.SAVED_CACHES_DIR;
 import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.CDC_RAW_DIR_PLACEHOLDER;
 import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.COMMITLOG_DIR_PLACEHOLDER;
 import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.DATA_FILE_DIR_PLACEHOLDER;
@@ -54,6 +55,13 @@ import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholde
 public class LiveMigrationInstanceMetadataUtil
 {
 
+    public static final String LIVE_MIGRATION_CDC_RAW_DIR_PATH = ApiEndpointsV1.LIVE_MIGRATION_FILES_API + "/" + CDC_RAW_DIR.dirType;
+    public static final String LIVE_MIGRATION_COMMITLOG_DIR_PATH = ApiEndpointsV1.LIVE_MIGRATION_FILES_API + "/" + COMMIT_LOG_DIR.dirType;
+    public static final String LIVE_MIGRATION_DATA_FILE_DIR_PATH = ApiEndpointsV1.LIVE_MIGRATION_FILES_API + "/" + DATA_FIlE_DIR.dirType;
+    public static final String LIVE_MIGRATION_HINTS_DIR_PATH = ApiEndpointsV1.LIVE_MIGRATION_FILES_API + "/" + HINTS_DIR.dirType;
+    public static final String LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH = ApiEndpointsV1.LIVE_MIGRATION_FILES_API
+                                                                                + "/" + LOCAL_SYSTEM_DATA_FILE_DIR.dirType;
+    public static final String LIVE_MIGRATION_SAVED_CACHES_DIR_PATH = ApiEndpointsV1.LIVE_MIGRATION_FILES_API + "/" + SAVED_CACHES_DIR.dirType;
     private static final Logger LOGGER = LoggerFactory.getLogger(LiveMigrationInstanceMetadataUtil.class);
 
     /**
@@ -144,6 +152,44 @@ public class LiveMigrationInstanceMetadataUtil
         return Collections.unmodifiableMap(placeholderMap);
     }
 
+    /**
+     * Returns a map of placeholder and its directories based on given {@link InstanceMetadata}.
+     */
+    public static Map<String, Set<String>> placeholderDirsMap(InstanceMetadata instanceMetadata)
+    {
+        Map<String, Set<String>> placeholderDirsMap = new HashMap<>();
+
+        placeholderDirsMap.put(HINTS_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.hintsDir()));
+        placeholderDirsMap.put(COMMITLOG_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.commitlogDir()));
+
+        if (instanceMetadata.savedCachesDir() != null)
+        {
+            placeholderDirsMap.put(SAVED_CACHES_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.savedCachesDir()));
+        }
+
+        if (instanceMetadata.cdcDir() != null)
+        {
+            placeholderDirsMap.put(CDC_RAW_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.cdcDir()));
+        }
+
+        if (instanceMetadata.localSystemDataFileDir() != null)
+        {
+            placeholderDirsMap.put(LOCAL_SYSTEM_DATA_FILE_DIR_PLACEHOLDER,
+                                  Collections.singleton(instanceMetadata.localSystemDataFileDir()));
+        }
+
+        placeholderDirsMap.put(DATA_FILE_DIR_PLACEHOLDER, Set.copyOf(instanceMetadata.dataDirs()));
+
+        List<String> dataDirs = instanceMetadata.dataDirs();
+        for (int i = 0; i < dataDirs.size(); i++)
+        {
+            placeholderDirsMap.put(DATA_FILE_DIR_PLACEHOLDER + "_" + i, Collections.singleton(dataDirs.get(i)));
+        }
+
+        return placeholderDirsMap;
+    }
+
+
     /**
      * Converts given live migration file download URL to local path.
      *
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtil.java b/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtil.java
index a421f7d4..84bdc747 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtil.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtil.java
@@ -18,10 +18,14 @@
 
 package org.apache.cassandra.sidecar.livemigration;
 
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -61,12 +65,48 @@ public class LiveMigrationPlaceholderUtil
         }
         if (placeholders.contains(placeholder))
         {
-            return input.replace("${" + placeholder + "}", value);
+            return replacePlaceholder(input, placeholder, value);
         }
 
         return null;
     }
 
+    /**
+     * Replaces placeholder present in given {@code input} with directory based on instance metadata.
+     *
+     * @param input            Input string to replace placeholder
+     * @param instanceMetadata Metadata of instance to pick the directory for placeholder in input
+     * @return input after replacing placeholder with corresponding directories. If input does not
+     * have a placeholder, then input returned as Set.
+     */
+    public static Set<String> replacePlaceholder(String input, InstanceMetadata instanceMetadata)
+    {
+        if (!hasAnyPlaceholder(input))
+        {
+            return Collections.singleton(input);
+        }
+        String placeholder = getPlaceholder(input);
+        Map<String, Set<String>> placeholderDirsMap = LiveMigrationInstanceMetadataUtil.placeholderDirsMap(instanceMetadata);
+
+        if (!placeholderDirsMap.containsKey(placeholder))
+        {
+            throw new IllegalArgumentException("Unknown placeholder specified in " + input);
+        }
+
+        Set<String> dirs = placeholderDirsMap.get(placeholder);
+        Set<String> output = new HashSet<>(dirs.size());
+        for (String dir : dirs)
+        {
+            output.add(replacePlaceholder(input, placeholder, dir));
+        }
+        return output;
+    }
+
+    private static String replacePlaceholder(String input, String placeholder, String value)
+    {
+        return input.replace("${" + placeholder + "}", value);
+    }
+
     /**
      * Checks whether given input has any placeholder init.
      * Placeholders can be specified using {@link #PLACEHOLDER_PATTERN} pattern.
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/modules/LiveMigrationModule.java b/server/src/main/java/org/apache/cassandra/sidecar/modules/LiveMigrationModule.java
index 3e4f86f3..b2eeef8e 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/modules/LiveMigrationModule.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/modules/LiveMigrationModule.java
@@ -20,7 +20,9 @@ package org.apache.cassandra.sidecar.modules;
 
 import com.google.inject.AbstractModule;
 import com.google.inject.multibindings.ProvidesIntoMap;
+import org.apache.cassandra.sidecar.handlers.FileStreamHandler;
 import org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationApiEnableDisableHandler;
+import org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationFileStreamHandler;
 import org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationListInstanceFilesHandler;
 import org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationMap;
 import org.apache.cassandra.sidecar.handlers.livemigration.LiveMigrationMapSidecarConfigImpl;
@@ -42,6 +44,20 @@ public class LiveMigrationModule extends AbstractModule
     }
 
 
+    @ProvidesIntoMap
+    @KeyClassMapKey(VertxRouteMapKeys.LiveMigrationFileStreamHandlerRouteKey.class)
+    VertxRoute downloadFileRoute(RouteBuilder.Factory factory,
+                                 LiveMigrationApiEnableDisableHandler liveMigrationApiEnableDisableHandler,
+                                 LiveMigrationFileStreamHandler liveMigrationFileStreamHandler,
+                                 FileStreamHandler fileStreamHandler)
+    {
+        return factory.builderForRoute()
+                      .handler(liveMigrationApiEnableDisableHandler::isSource)
+                      .handler(liveMigrationFileStreamHandler)
+                      .handler(fileStreamHandler)
+                      .build();
+    }
+
     @ProvidesIntoMap
     @KeyClassMapKey(VertxRouteMapKeys.LiveMigrationListInstanceFilesRouteKey.class)
     VertxRoute listInstanceFiles(RouteBuilder.Factory factory,
diff --git a/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java b/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
index 1706cc92..429ffb0c 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
@@ -192,6 +192,11 @@ public interface VertxRouteMapKeys
         HttpMethod HTTP_METHOD = HttpMethod.GET;
         String ROUTE_URI = ApiEndpointsV1.SNAPSHOTS_ROUTE;
     }
+    interface LiveMigrationFileStreamHandlerRouteKey extends RouteClassKey
+    {
+        HttpMethod HTTP_METHOD = HttpMethod.GET;
+        String ROUTE_URI = ApiEndpointsV1.LIVE_MIGRATION_FILE_TRANSFER_API;
+    }
     interface LiveMigrationListInstanceFilesRouteKey extends RouteClassKey
     {
         HttpMethod HTTP_METHOD = HttpMethod.GET;
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandlerTest.java b/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandlerTest.java
new file mode 100644
index 00000000..f5c363d7
--- /dev/null
+++ b/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandlerTest.java
@@ -0,0 +1,439 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.handlers.livemigration;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.util.Modules;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.Vertx;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.codec.BodyCodec;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.HelperTestModules;
+import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadataImpl;
+import org.apache.cassandra.sidecar.config.LiveMigrationConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
+import org.apache.cassandra.sidecar.metrics.MetricRegistryFactory;
+import org.apache.cassandra.sidecar.modules.SidecarModules;
+import org.apache.cassandra.sidecar.server.Server;
+
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_CDC_RAW_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
+import static org.apache.cassandra.sidecar.utils.TestFileUtils.createFile;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(VertxExtension.class)
+class LiveMigrationFileStreamHandlerTest
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(LiveMigrationFileStreamHandlerTest.class);
+
+    private static final int FIRST_ID = 1000110;
+    private static final String FIRST_INSTANCE_IP = "127.0.0.1";
+    private static final int SECOND_ID = 1000111;
+    private static final String SECOND_INSTANCE_IP = "127.0.0.2";
+    private static final int THIRD_ID = 1000112;
+    private static final String THIRD_INSTANCE_IP = "127.0.0.3";
+    private static final String DUMMY_CONTENT = "data";
+
+    private static final MetricRegistryFactory REGISTRY_FACTORY =
+    new MetricRegistryFactory("cassandra_sidecar_" + UUID.randomUUID(), Collections.emptyList(), Collections.emptyList());
+    private final Vertx vertx = Vertx.vertx();
+    @TempDir
+    Path tempDir;
+    Server server;
+    List<String> firstInstanceDataDirs;
+    List<String> secondInstanceDataDirs;
+    List<String> thirdInstanceDataDirs;
+    private Injector injector;
+
+    @BeforeEach
+    public void setup() throws InterruptedException
+    {
+        InstanceMetadata firstInstanceMeta = getInstanceMetadata(FIRST_INSTANCE_IP, FIRST_ID);
+        firstInstanceDataDirs = firstInstanceMeta.dataDirs();
+        InstanceMetadata secondInstanceMeta = getInstanceMetadata(SECOND_INSTANCE_IP, SECOND_ID);
+        secondInstanceDataDirs = secondInstanceMeta.dataDirs();
+        InstanceMetadata thirdInstanceMeta = getInstanceMetadata(THIRD_INSTANCE_IP, THIRD_ID);
+        thirdInstanceDataDirs = thirdInstanceMeta.dataDirs();
+        FileStreamHandlerTestModule handlerTestModule = new FileStreamHandlerTestModule(
+        Arrays.asList(firstInstanceMeta, secondInstanceMeta, thirdInstanceMeta));
+        injector = Guice.createInjector(Modules.override(SidecarModules.all())
+                                               .with(Modules.override(new TestModule())
+                                                            .with(handlerTestModule)));
+
+        server = injector.getInstance(Server.class);
+        VertxTestContext context = new VertxTestContext();
+        server.start()
+              .onSuccess(s -> context.completeNow())
+              .onFailure(context::failNow);
+        context.awaitCompletion(15, TimeUnit.SECONDS);
+    }
+
+    private InstanceMetadata getInstanceMetadata(String instanceIp,
+                                                 int instanceId)
+    {
+        String root = tempDir.resolve(String.valueOf(instanceId)).toString();
+        List<String> dataDirs = Arrays.asList(root + "/d1/data", root + "/d2/data");
+        MetricRegistry instanceSpecificRegistry = REGISTRY_FACTORY.getOrCreate(instanceId);
+
+        return InstanceMetadataImpl.builder()
+                                   .id(instanceId)
+                                   .host(instanceIp)
+                                   .port(9042)
+                                   .dataDirs(dataDirs)
+                                   .hintsDir(root + "/hints")
+                                   .commitlogDir(root + "/commitlog")
+                                   .savedCachesDir(root + "/saved_caches")
+                                   .stagingDir(root + "/staging")
+                                   .metricRegistry(instanceSpecificRegistry)
+                                   .build();
+    }
+
+    @AfterEach
+    public void tearDown() throws InterruptedException
+    {
+        CountDownLatch closeLatch = new CountDownLatch(1);
+        server.close().onSuccess(res -> closeLatch.countDown());
+        if (closeLatch.await(60, TimeUnit.SECONDS))
+            LOGGER.info("Close event received before timeout.");
+        else
+            LOGGER.error("Close event timed out.");
+    }
+
+    @Test
+    public void testRouteSucceeds(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0" + filePath;
+        shouldSucceed(context, testRoute, FIRST_INSTANCE_IP, SECOND_INSTANCE_IP, FIRST_INSTANCE_IP);
+    }
+
+    @Test
+    public void testRouteUnConfiguredDirFile(final VertxTestContext context)
+    {
+        // CDC dir not configured while constructing dummy InstanceMetadata in getInstanceMetadata()
+        // Yet requesting a dummy file to see if the endpoint returns proper error
+
+        final String testRoute = LIVE_MIGRATION_CDC_RAW_DIR_PATH + "/0/Commitlog-7-1.db";
+        shouldThrowError(context, testRoute, FIRST_INSTANCE_IP, SECOND_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+    }
+
+    @Test
+    public void testRouteDoubleDotAtTheEndAfterDirIndex(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/data/0/..",
+                         FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+    }
+
+    @Test
+    public void testRouteDoubleDotAtTheEnd(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0/ks/tb-1234/ks-tb-1234-Data.db/..",
+                         FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+    }
+
+    @Test
+    public void testRouteDoubleDotAfterDirIndex(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0/../ks/tb-1234/ks-tb-1234-Data.db",
+                         FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+    }
+
+    @Test
+    public void testRequestUsingEncodedDots(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        // Using escape character of '.' i.e. %2E
+        shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0/ks/tb-1234/%2E%2E/%2E%2E/secrets",
+                         FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+    }
+
+
+    @Test
+    public void testRequestInvalidPathUsingEncodedDots(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        // Vertx is stopping routes having three "%2E%2E" in the path and returning 404
+        shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0/%2E%2E/%2E%2E/%2E%2E/secrets",
+                         FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+    }
+
+
+    @Test
+    public void testRequestDirectory(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        // Requesting directory, it should not succeed.
+        shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0/ks/tb-1234",
+                         FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+    }
+
+    @Test
+    public void testRouteDirtyRouteThatDoesNotMatchPath(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0/../../.." + filePath,
+                         FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+    }
+
+    @Test
+    public void testRouteWithoutDirIndex(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        // This route doesn't have data file dir index init. It should fail.
+        shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + filePath,
+                         FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+    }
+
+    @Test
+    public void testRouteHavingNegativeDirIndex(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        // This route has negative dataHomeDir which is invalid. It should fail.
+        shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/-1" + filePath,
+                         FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 400);
+    }
+
+    @Test
+    public void testRouteHavingDirIndexThatDoesNotExist(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        // This route has 100 as dataHomeDir index which is greater than instance data home dir count. It should fail.
+        shouldThrowError(context, LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/100" + filePath,
+                         FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+    }
+
+    @Test
+    public void testRouteFailsForDestinationInstance(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0" + filePath;
+        shouldThrowError(context, testRoute, FIRST_INSTANCE_IP, THIRD_INSTANCE_IP, THIRD_INSTANCE_IP, 404);
+    }
+
+    @Test
+    public void testRouteFailsForNonLiveMigratingInstance(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/ks-tb-1234-Data.db";
+        createFile(DUMMY_CONTENT, secondInstanceDataDirs.get(0) + filePath);
+
+        final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0" + filePath;
+        // In this test second instance is neither source nor destination
+        shouldThrowError(context, testRoute, "127.0.0.4", "127.0.0.5", SECOND_INSTANCE_IP, 404);
+    }
+
+
+    @Test
+    public void testRouteFailsExcludedDataDirFile(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/test.txt";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0" + filePath;
+        mockFileExclusion(Collections.singleton("glob:${DATA_FILE_DIR}" + filePath));
+
+        shouldThrowError(context, testRoute, FIRST_INSTANCE_IP, SECOND_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+    }
+
+    @Test
+    public void testRouteFailsExcludedFileSecondDataDirWildcardExclusion(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/test.txt";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(1) + filePath);
+
+        final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/1" + filePath;
+        mockFileExclusion(Collections.singleton("glob:${DATA_FILE_DIR_1}/ks/**"));
+
+        shouldThrowError(context, testRoute, FIRST_INSTANCE_IP, SECOND_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+    }
+
+
+    @Test
+    public void testRouteFailsWhenParentDirExcluded(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/test.txt";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0" + filePath;
+        mockDirExclusion(Collections.singleton("glob:${DATA_FILE_DIR}/ks"));
+
+        shouldThrowError(context, testRoute, FIRST_INSTANCE_IP, SECOND_INSTANCE_IP, FIRST_INSTANCE_IP, 404);
+    }
+
+    @Test
+    public void testRouteSucceedsWhenOtherDirExcluded(final VertxTestContext context) throws IOException
+    {
+        String filePath = "/ks/tb-1234/test.txt";
+        createFile(DUMMY_CONTENT, firstInstanceDataDirs.get(0) + filePath);
+
+        final String testRoute = LIVE_MIGRATION_DATA_FILE_DIR_PATH + "/0" + filePath;
+        mockDirExclusion(Collections.singleton("glob:${DATA_FILE_DIR}/ks2"));
+
+        shouldSucceed(context, testRoute, FIRST_INSTANCE_IP, SECOND_INSTANCE_IP, FIRST_INSTANCE_IP);
+    }
+
+
+    @SuppressWarnings("SameParameterValue")
+    void shouldSucceed(final VertxTestContext context,
+                       final String testRoute,
+                       final String source,
+                       final String destination,
+                       String requestHost)
+    {
+        mockLiveMigrationMap(source, destination);
+
+        final WebClient client = WebClient.create(vertx);
+        client.get(server.actualPort(), requestHost, testRoute)
+              .as(BodyCodec.buffer())
+              .send(context.succeeding(resp -> context.verify(() -> {
+                  assertThat(resp.statusCode()).isEqualTo(HttpResponseStatus.OK.code());
+                  assertThat(resp.bodyAsString()).isEqualTo(DUMMY_CONTENT);
+                  client.close();
+                  context.completeNow();
+              })));
+    }
+
+    void shouldThrowError(final VertxTestContext context,
+                          final String testRoute,
+                          final String source,
+                          final String destination,
+                          String requestHost,
+                          final int expectedStatusCode)
+    {
+        mockLiveMigrationMap(source, destination);
+
+        final WebClient client = WebClient.create(vertx);
+        final String url = String.format("http://%s:%d%s", requestHost, server.actualPort(), testRoute);
+        client.getAbs(url)
+              .as(BodyCodec.buffer())
+              .send(context.succeeding(resp -> context.verify(() -> {
+                  assertThat(resp.statusCode()).isEqualTo(expectedStatusCode);
+                  client.close();
+                  context.completeNow();
+              })));
+    }
+
+    void mockLiveMigrationMap(final String source, final String destination)
+    {
+        LiveMigrationConfiguration liveMigrationConfig = injector.getInstance(SidecarConfiguration.class)
+                                                                 .liveMigrationConfiguration();
+        when(liveMigrationConfig.migrationMap()).thenReturn(Map.of(source, destination));
+    }
+
+    void mockFileExclusion(Set<String> fileExclusions)
+    {
+        LiveMigrationConfiguration configuration = injector.getInstance(SidecarConfiguration.class)
+                                                           .liveMigrationConfiguration();
+        when(configuration.filesToExclude()).thenReturn(fileExclusions);
+    }
+
+    void mockDirExclusion(Set<String> dirExclusions)
+    {
+        LiveMigrationConfiguration configuration = injector.getInstance(SidecarConfiguration.class)
+                                                           .liveMigrationConfiguration();
+        when(configuration.directoriesToExclude()).thenReturn(dirExclusions);
+    }
+
+
+    private static class FileStreamHandlerTestModule extends AbstractModule
+    {
+
+        private final List<InstanceMetadata> instanceMetaList;
+
+        public FileStreamHandlerTestModule(List<InstanceMetadata> instanceMetaList)
+        {
+            this.instanceMetaList = instanceMetaList;
+        }
+
+        @Override
+        protected void configure()
+        {
+
+            LiveMigrationConfiguration mockLiveMigrationConfiguration = mock(LiveMigrationConfiguration.class);
+            when(mockLiveMigrationConfiguration.filesToExclude())
+            .thenReturn(Collections.emptySet());
+            when(mockLiveMigrationConfiguration.directoriesToExclude())
+            .thenReturn(Collections.singleton("glob:${DATA_FILE_DIR}/*/*/snapshots"));
+            when(mockLiveMigrationConfiguration.migrationMap())
+            .thenReturn(Collections.emptyMap());
+
+            SidecarConfiguration sidecarConfiguration = SidecarConfigurationImpl.builder()
+                                                                                .liveMigrationConfiguration(mockLiveMigrationConfiguration)
+                                                                                .build();
+
+            bind(SidecarConfiguration.class).toInstance(sidecarConfiguration);
+            install(new HelperTestModules.InstanceMetadataTestModule(instanceMetaList));
+        }
+    }
+}
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationListInstanceFilesHandlerTest.java b/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationListInstanceFilesHandlerTest.java
index 98fa8168..28297dc3 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationListInstanceFilesHandlerTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationListInstanceFilesHandlerTest.java
@@ -67,12 +67,12 @@ import org.apache.cassandra.sidecar.metrics.MetricRegistryFactory;
 import org.apache.cassandra.sidecar.modules.SidecarModules;
 import org.apache.cassandra.sidecar.server.Server;
 
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
 import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_FILES_API;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_HINTS_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
 import static org.apache.cassandra.sidecar.livemigration.InstanceFileInfoTestUtil.findInstanceFileInfo;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_HINTS_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
 import static org.apache.cassandra.sidecar.utils.TestFileUtils.createFile;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/livemigration/CassandraInstanceFilesImplTest.java b/server/src/test/java/org/apache/cassandra/sidecar/livemigration/CassandraInstanceFilesImplTest.java
index b77043c4..d3d58ee8 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/livemigration/CassandraInstanceFilesImplTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/livemigration/CassandraInstanceFilesImplTest.java
@@ -33,14 +33,14 @@ import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
 import org.apache.cassandra.sidecar.common.response.InstanceFileInfo;
 import org.apache.cassandra.sidecar.config.LiveMigrationConfiguration;
 
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_CDC_RAW_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_HINTS_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
 import static org.apache.cassandra.sidecar.common.response.InstanceFileInfo.FileType.DIRECTORY;
 import static org.apache.cassandra.sidecar.livemigration.InstanceFileInfoTestUtil.findInstanceFileInfo;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_CDC_RAW_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_HINTS_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
 import static org.apache.cassandra.sidecar.utils.TestFileUtils.createFile;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtilTest.java b/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtilTest.java
index 6f8fb050..e5dc85b9 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtilTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationInstanceMetadataUtilTest.java
@@ -33,12 +33,12 @@ import org.junit.jupiter.api.io.TempDir;
 import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
 import org.mockito.Mockito;
 
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_CDC_RAW_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_HINTS_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH;
-import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_CDC_RAW_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_COMMITLOG_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_DATA_FILE_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_HINTS_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.LIVE_MIGRATION_SAVED_CACHES_DIR_PATH;
 import static org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil.localPath;
 import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.CDC_RAW_DIR_PLACEHOLDER;
 import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.COMMITLOG_DIR_PLACEHOLDER;
@@ -204,6 +204,58 @@ class LiveMigrationInstanceMetadataUtilTest
                                     .doesNotContainKey(cassandraHomeDir + "/" + LOCAL_SYSTEM_DIR);
     }
 
+    @Test
+    public void testPlaceholderDirsMap()
+    {
+        String cassandraHomeDir = tempDir.resolve("testPlaceholderDirsMap").toString();
+        InstanceMetadata instanceMetadata = getInstanceMetadata(cassandraHomeDir);
+        List<String> dataDirs = new ArrayList<>(2);
+        String dataDir2 = DATA_DIR + "2";
+        dataDirs.add(cassandraHomeDir + "/" + DATA_DIR);
+        dataDirs.add(cassandraHomeDir + "/" + dataDir2);
+        when(instanceMetadata.dataDirs()).thenReturn(dataDirs);
+
+        Map<String, Set<String>> placeholderDirsMap = LiveMigrationInstanceMetadataUtil.placeholderDirsMap(instanceMetadata);
+
+        Set<String> dirsToCopy = new HashSet<>(LiveMigrationInstanceMetadataUtil.dirsToCopy(instanceMetadata));
+        Set<String> dirsInMap = new HashSet<>();
+        placeholderDirsMap.forEach((k, v) -> dirsInMap.addAll(v));
+
+        // Placeholders should have been defined for all dirs that can be copied
+        assertThat(dirsInMap).isEqualTo(dirsToCopy);
+
+        assertThat(placeholderDirsMap).contains(
+        entry(HINTS_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.hintsDir())),
+        entry(COMMITLOG_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.commitlogDir())),
+        entry(SAVED_CACHES_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.savedCachesDir())),
+        entry(CDC_RAW_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.cdcDir())),
+        entry(LOCAL_SYSTEM_DATA_FILE_DIR_PLACEHOLDER, Collections.singleton(instanceMetadata.localSystemDataFileDir())),
+        entry(DATA_FILE_DIR_PLACEHOLDER, new HashSet<>(instanceMetadata.dataDirs())),
+        entry(DATA_FILE_DIR_PLACEHOLDER + "_" + 0, Collections.singleton(instanceMetadata.dataDirs().get(0))),
+        entry(DATA_FILE_DIR_PLACEHOLDER + "_" + 1, Collections.singleton(instanceMetadata.dataDirs().get(1)))
+        );
+    }
+
+    @Test
+    public void testPlaceholderDirsMapFewDirsNotConfigured()
+    {
+        String cassandraHomeDir = tempDir.resolve("testPlaceholderDirsMapFewDirsNotConfigured").toString();
+        InstanceMetadata instanceMetadata = getInstanceMetadata(cassandraHomeDir);
+        when(instanceMetadata.cdcDir()).thenReturn(null);
+        when(instanceMetadata.localSystemDataFileDir()).thenReturn(null);
+
+        Map<String, Set<String>> placeholderDirsMap = LiveMigrationInstanceMetadataUtil.placeholderDirsMap(instanceMetadata);
+
+        Set<String> dirsToCopy = new HashSet<>(LiveMigrationInstanceMetadataUtil.dirsToCopy(instanceMetadata));
+        Set<String> dirsInMap = new HashSet<>();
+        placeholderDirsMap.forEach((k, v) -> dirsInMap.addAll(v));
+
+        // Placeholders should have been defined for all dirs that can be copied
+        assertThat(dirsInMap).isEqualTo(dirsToCopy);
+
+        assertThat(placeholderDirsMap).doesNotContainKey(CDC_RAW_DIR_PLACEHOLDER)
+                                      .doesNotContainKey(LOCAL_SYSTEM_DATA_FILE_DIR_PLACEHOLDER);
+    }
 
     @Test
     public void testLocalPath()
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtilTest.java b/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtilTest.java
index e800dcf6..68480e25 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtilTest.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/livemigration/LiveMigrationPlaceholderUtilTest.java
@@ -18,16 +18,43 @@
 
 package org.apache.cassandra.sidecar.livemigration;
 
+import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.mockito.Mockito;
+
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.CDC_RAW_DIR_PLACEHOLDER;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.COMMITLOG_DIR_PLACEHOLDER;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.DATA_FILE_DIR_PLACEHOLDER;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.HINTS_DIR_PLACEHOLDER;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.LOCAL_SYSTEM_DATA_FILE_DIR_PLACEHOLDER;
+import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.SAVED_CACHES_DIR_PLACEHOLDER;
 import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.hasAnyPlaceholder;
 import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.replacePlaceholder;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.Mockito.when;
 
 class LiveMigrationPlaceholderUtilTest
 {
+
+    private static final String DATA_DIR = "data";
+    private static final String HINTS_DIR = "hints";
+    private static final String COMMITLOG_DIR = "commitlog";
+    private static final String SAVED_CACHES_DIR = "saved_caches";
+    private static final String CDC_RAW_DIR = "cdc_raw";
+    private static final String LOCAL_SYSTEM_DIR = "local_system_data";
+    private static final String STAGING_DIR = "sstable-staging";
+
+    @TempDir
+    Path tempDir;
+
     @Test
     public void testReplacePlaceholder()
     {
@@ -70,4 +97,72 @@ class LiveMigrationPlaceholderUtilTest
 
         assertThat(hasAnyPlaceholder("glob:${DATA_FILE_DIR}/ks/t1", Collections.singleton("DATA_FILE_DIR_0"))).isFalse();
     }
+
+    @Test
+    public void testReplacePlaceholderUsingInstanceMetadata()
+    {
+        String cassandraHomeDir = tempDir.resolve("testReplacePlaceholderUsingInstanceMetadata").toString();
+        InstanceMetadata instanceMetadata = getInstanceMetadata(cassandraHomeDir);
+        List<String> dataDirs = new ArrayList<>(2);
+        String dataDir2 = DATA_DIR + "2";
+        dataDirs.add(cassandraHomeDir + "/" + DATA_DIR);
+        dataDirs.add(cassandraHomeDir + "/" + dataDir2);
+        when(instanceMetadata.dataDirs()).thenReturn(dataDirs);
+
+        assertThat(replacePlaceholder("glob:${" + HINTS_DIR_PLACEHOLDER + "}", instanceMetadata))
+        .hasSize(1)
+        .contains("glob:" + instanceMetadata.hintsDir());
+
+        assertThat(replacePlaceholder("regex:${" + COMMITLOG_DIR_PLACEHOLDER + "}/Commitlog-7-1.log", instanceMetadata))
+        .hasSize(1)
+        .contains("regex:" + instanceMetadata.commitlogDir() + "/Commitlog-7-1.log");
+
+        assertThat(replacePlaceholder("glob:${" + SAVED_CACHES_DIR_PLACEHOLDER + "}/cache.bin", instanceMetadata))
+        .hasSize(1)
+        .contains("glob:" + instanceMetadata.savedCachesDir() + "/cache.bin");
+
+        assertThat(replacePlaceholder("glob:${" + DATA_FILE_DIR_PLACEHOLDER + "}/*/*/snapshots", instanceMetadata))
+        .hasSize(2)
+        .contains("glob:" + instanceMetadata.dataDirs().get(0) + "/*/*/snapshots")
+        .contains("glob:" + instanceMetadata.dataDirs().get(1) + "/*/*/snapshots");
+
+        assertThat(replacePlaceholder("glob:${" + DATA_FILE_DIR_PLACEHOLDER + "_0}/ks1/*", instanceMetadata))
+        .hasSize(1)
+        .contains("glob:" + instanceMetadata.dataDirs().get(0) + "/ks1/*");
+
+        assertThat(replacePlaceholder("glob:${" + DATA_FILE_DIR_PLACEHOLDER + "_1}/k*/*", instanceMetadata))
+        .hasSize(1)
+        .contains("glob:" + instanceMetadata.dataDirs().get(1) + "/k*/*");
+
+        assertThat(replacePlaceholder("glob:${" + CDC_RAW_DIR_PLACEHOLDER + "}/**", instanceMetadata))
+        .hasSize(1)
+        .contains("glob:" + instanceMetadata.cdcDir() + "/**");
+
+        assertThat(replacePlaceholder("glob:${" + LOCAL_SYSTEM_DATA_FILE_DIR_PLACEHOLDER + "}/", instanceMetadata))
+        .hasSize(1)
+        .contains("glob:" + instanceMetadata.localSystemDataFileDir() + "/");
+
+        // Trying to replace text not having any placeholder
+        assertThat(replacePlaceholder("glob:/home/usr/data/*", instanceMetadata))
+        .hasSize(1)
+        .contains("glob:/home/usr/data/*");
+
+        //Unknown placeholder
+        assertThatIllegalArgumentException()
+        .isThrownBy(() -> replacePlaceholder("glob:${UNKNOWN_PLACE_HOLDER}/0", instanceMetadata));
+    }
+
+    InstanceMetadata getInstanceMetadata(String cassandraHomeDir)
+    {
+        InstanceMetadata instanceMetadata = Mockito.mock(InstanceMetadata.class);
+        when(instanceMetadata.dataDirs()).thenReturn(Collections.singletonList(cassandraHomeDir + "/" + DATA_DIR));
+        when(instanceMetadata.cdcDir()).thenReturn(cassandraHomeDir + "/" + CDC_RAW_DIR);
+        when(instanceMetadata.commitlogDir()).thenReturn(cassandraHomeDir + "/" + COMMITLOG_DIR);
+        when(instanceMetadata.hintsDir()).thenReturn(cassandraHomeDir + "/" + HINTS_DIR);
+        when(instanceMetadata.savedCachesDir()).thenReturn(cassandraHomeDir + "/" + SAVED_CACHES_DIR);
+        when(instanceMetadata.localSystemDataFileDir()).thenReturn(cassandraHomeDir + "/" + LOCAL_SYSTEM_DIR);
+        when(instanceMetadata.stagingDir()).thenReturn(cassandraHomeDir + "/" + STAGING_DIR);
+
+        return instanceMetadata;
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org