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

epugh pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_10x by this push:
     new 70cfa89836c SOLR-15701: Complete configsets api (#4264)
70cfa89836c is described below

commit 70cfa89836ca5be019c9875273ebc120d46590bc
Author: Eric Pugh <[email protected]>
AuthorDate: Fri Apr 24 07:48:57 2026 -0400

    SOLR-15701: Complete configsets api (#4264)
    
    Add Download and GetFile and PutFile to ConfigSets API and SolrJ and 
removed overwrite parameter from the V1 single file upload.
---
 .../SOLR-15701_complete_configsets_api.yml         |   7 +
 .../solr/client/api/endpoint/ConfigsetsApi.java    |  86 +++++-
 .../org/apache/solr/core/ConfigSetService.java     |  26 +-
 .../solr/core/FileSystemConfigSetService.java      |   7 +-
 .../solr/handler/admin/ConfigSetsHandler.java      |  26 +-
 .../solr/handler/configsets/CloneConfigSet.java    |   6 +-
 .../solr/handler/configsets/ConfigSetAPIBase.java  |  16 +-
 .../solr/handler/configsets/DeleteConfigSet.java   |   6 +-
 .../solr/handler/configsets/DownloadConfigSet.java | 133 ++++++++
 .../solr/handler/configsets/GetConfigSetFile.java  |  75 +++++
 .../solr/handler/configsets/UploadConfigSet.java   |  26 +-
 .../cloud/MockScriptUpdateProcessorFactory.java    |   7 +-
 .../org/apache/solr/cloud/TestConfigSetsAPI.java   | 340 ++++++++++-----------
 .../solr/cloud/TestConfigSetsAPIExclusivity.java   |   2 -
 .../configsets/DownloadConfigSetAPITest.java       |  94 ++++++
 .../configsets/GetConfigSetFileAPITest.java        | 197 ++++++++++++
 .../handler/configsets/UploadConfigSetAPITest.java | 307 +++++++++++++++++++
 .../configsets/UploadConfigSetFileAPITest.java     | 101 ++++++
 .../configuration-guide/pages/config-sets.adoc     |   2 +-
 .../configuration-guide/pages/configsets-api.adoc  | 208 ++++++++++---
 .../pages/major-changes-in-solr-10.adoc            |   7 +
 21 files changed, 1397 insertions(+), 282 deletions(-)

diff --git a/changelog/unreleased/SOLR-15701_complete_configsets_api.yml 
b/changelog/unreleased/SOLR-15701_complete_configsets_api.yml
new file mode 100644
index 00000000000..3d79059b1e2
--- /dev/null
+++ b/changelog/unreleased/SOLR-15701_complete_configsets_api.yml
@@ -0,0 +1,7 @@
+title: Add ConfigSets.Download and ConfigSets.GetFile to SolrJ
+type: added
+authors:
+  - name: Eric Pugh
+links:
+  - name: SOLR-15701
+    url: https://issues.apache.org/jira/browse/SOLR-15701
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigsetsApi.java 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigsetsApi.java
index 4bc812043e9..f7fd006ef44 100644
--- a/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigsetsApi.java
+++ b/solr/api/src/java/org/apache/solr/client/api/endpoint/ConfigsetsApi.java
@@ -16,7 +16,12 @@
  */
 package org.apache.solr.client.api.endpoint;
 
+import static 
org.apache.solr.client.api.util.Constants.GENERIC_ENTITY_PROPERTY;
+import static org.apache.solr.client.api.util.Constants.RAW_OUTPUT_PROPERTY;
+
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
 import io.swagger.v3.oas.annotations.parameters.RequestBody;
 import jakarta.ws.rs.DELETE;
 import jakarta.ws.rs.GET;
@@ -24,7 +29,11 @@ import jakarta.ws.rs.POST;
 import jakarta.ws.rs.PUT;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
 import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.StreamingOutput;
 import java.io.IOException;
 import java.io.InputStream;
 import org.apache.solr.client.api.model.CloneConfigsetRequestBody;
@@ -71,31 +80,92 @@ public interface ConfigsetsApi {
         throws Exception;
   }
 
+  /** V2 API definition for downloading an existing configset as a ZIP 
archive. */
+  @Path("/configsets/{configSetName}")
+  interface Download {
+    @GET
+    @Path("/files")
+    @Operation(
+        summary = "Download a configset as a ZIP archive.",
+        tags = {"configsets"},
+        extensions = {
+          @Extension(properties = {@ExtensionProperty(name = 
RAW_OUTPUT_PROPERTY, value = "true")})
+        })
+    @Produces("application/zip")
+    Response downloadConfigSet(@PathParam("configSetName") String 
configSetName) throws Exception;
+  }
+
   /**
-   * V2 API definitions for uploading a configset, in whole or part.
+   * V2 API definition for reading a single file from an existing configset.
+   *
+   * <p>Returns the raw bytes of the file, suitable for both text and binary 
files.
+   *
+   * <p>Equivalent to GET /api/configsets/{configSetName}/files/{filePath}
+   */
+  @Path("/configsets/{configSetName}")
+  interface GetFile {
+    @GET
+    @Path("/files/{filePath:.+}")
+    @Produces(MediaType.APPLICATION_OCTET_STREAM)
+    @Operation(
+        summary = "Get the raw contents of a file in a configset.",
+        tags = {"configsets"},
+        extensions = {
+          @Extension(properties = {@ExtensionProperty(name = 
RAW_OUTPUT_PROPERTY, value = "true")})
+        })
+    StreamingOutput getConfigSetFile(
+        @PathParam("configSetName") String configSetName, 
@PathParam("filePath") String filePath)
+        throws Exception;
+  }
+
+  /**
+   * V2 API definition for uploading an entire configset as a ZIP archive.
    *
    * <p>Equivalent to the existing v1 API /admin/configs?action=UPLOAD
    */
   @Path("/configsets/{configSetName}")
   interface Upload {
     @PUT
-    @Operation(summary = "Create a new configset.", tags = "configsets")
+    @Operation(summary = "Upload a configset as a ZIP archive.", tags = 
"configsets")
     SolrJerseyResponse uploadConfigSet(
         @PathParam("configSetName") String configSetName,
         @QueryParam("overwrite") Boolean overwrite,
         @QueryParam("cleanup") Boolean cleanup,
-        @RequestBody(required = true) InputStream requestBody)
+        @RequestBody(
+                required = true,
+                extensions = {
+                  @Extension(
+                      properties = {
+                        @ExtensionProperty(name = GENERIC_ENTITY_PROPERTY, 
value = "true")
+                      })
+                })
+            InputStream requestBody)
         throws IOException;
+  }
 
+  /**
+   * V2 API definition for putting a single file to an existing configset.
+   *
+   * <p>This endpoint allows updating individual configuration files without 
re-uploading the entire
+   * configset. The file path is specified as part of the URL path.
+   */
+  @Path("/configsets/{configSetName}")
+  interface PutFile {
     @PUT
-    @Path("{filePath:.+}")
-    @Operation(summary = "Create a new configset.", tags = "configsets")
+    @Path("/files/{filePath:.+}")
+    @Operation(summary = "Upload a single file to a configset.", tags = 
"configsets")
     SolrJerseyResponse uploadConfigSetFile(
         @PathParam("configSetName") String configSetName,
         @PathParam("filePath") String filePath,
-        @QueryParam("overwrite") Boolean overwrite,
-        @QueryParam("cleanup") Boolean cleanup,
-        @RequestBody(required = true) InputStream requestBody)
+        @RequestBody(
+                required = true,
+                extensions = {
+                  @Extension(
+                      properties = {
+                        @ExtensionProperty(name = GENERIC_ENTITY_PROPERTY, 
value = "true")
+                      })
+                })
+            InputStream requestBody)
         throws IOException;
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/core/ConfigSetService.java 
b/solr/core/src/java/org/apache/solr/core/ConfigSetService.java
index 99d237f9e38..d88b15e0c44 100644
--- a/solr/core/src/java/org/apache/solr/core/ConfigSetService.java
+++ b/solr/core/src/java/org/apache/solr/core/ConfigSetService.java
@@ -381,8 +381,8 @@ public abstract class ConfigSetService {
   public abstract void uploadConfig(String configName, Path dir) throws 
IOException;
 
   /**
-   * Upload a file to config If file does not exist, it will be uploaded If 
overwriteOnExists is set
-   * to true then file will be overwritten
+   * Upload a file to config. If file does not exist, it will be uploaded. If 
overwriteOnExists is
+   * set to true then the file will be overwritten.
    *
    * @param configName the name to give the config
    * @param fileName the name of the file with '/' used as the file path 
separator
@@ -395,7 +395,7 @@ public abstract class ConfigSetService {
       throws IOException;
 
   /**
-   * Download all files from this config to the filesystem at dir
+   * Download all files from this config to the filesystem at dir.
    *
    * @param configName the config to download
    * @param dir the {@link Path} to write files under
@@ -403,7 +403,7 @@ public abstract class ConfigSetService {
   public abstract void downloadConfig(String configName, Path dir) throws 
IOException;
 
   /**
-   * Download a file from config If the file does not exist, it returns null
+   * Download a file from config. If the file does not exist, it returns null.
    *
    * @param configName the name of the config
    * @param filePath the file to download with '/' as the separator
@@ -413,7 +413,7 @@ public abstract class ConfigSetService {
       throws IOException;
 
   /**
-   * Copy a config
+   * Copy a config.
    *
    * @param fromConfig the config to copy from
    * @param toConfig the config to copy to
@@ -421,7 +421,7 @@ public abstract class ConfigSetService {
   public abstract void copyConfig(String fromConfig, String toConfig) throws 
IOException;
 
   /**
-   * Check whether a config exists
+   * Check whether a config exists.
    *
    * @param configName the config to check if it exists
    * @return whether the config exists or not
@@ -429,14 +429,14 @@ public abstract class ConfigSetService {
   public abstract boolean checkConfigExists(String configName) throws 
IOException;
 
   /**
-   * Delete a config (recursively deletes its files if not empty)
+   * Delete a config (recursively deletes its files if not empty).
    *
    * @param configName the config to delete
    */
   public abstract void deleteConfig(String configName) throws IOException;
 
   /**
-   * Delete files in config
+   * Delete files in config.
    *
    * @param configName the name of the config
    * @param filesToDelete a list of file paths to delete using '/' as file 
path separator
@@ -445,8 +445,8 @@ public abstract class ConfigSetService {
       throws IOException;
 
   /**
-   * Set the config metadata If config does not exist, it will be created and 
set metadata on it
-   * Else metadata will be replaced with the provided metadata
+   * Set the config metadata. If config does not exist, it will be created and 
set metadata on it.
+   * Else metadata will be replaced with the provided metadata.
    *
    * @param configName the config name
    * @param data the metadata to be set on config
@@ -455,7 +455,7 @@ public abstract class ConfigSetService {
       throws IOException;
 
   /**
-   * Get the config metadata (mutable, non-null)
+   * Get the config metadata (mutable, non-null).
    *
    * @param configName the config name
    * @return the config metadata
@@ -463,7 +463,7 @@ public abstract class ConfigSetService {
   public abstract Map<String, Object> getConfigMetadata(String configName) 
throws IOException;
 
   /**
-   * List the names of configs (non-null)
+   * List the names of configs (non-null).
    *
    * @return list of config names
    */
@@ -471,7 +471,7 @@ public abstract class ConfigSetService {
 
   /**
    * Get the names of the files in config including dirs (mutable, non-null) 
sorted
-   * lexicographically e.g. solrconfig.xml, lang/, lang/stopwords_en.txt
+   * lexicographically e.g. solrconfig.xml, lang/, lang/stopwords_en.txt.
    *
    * @param configName the config name
    * @return list of file name paths in the config with '/' uses as file path 
separators
diff --git 
a/solr/core/src/java/org/apache/solr/core/FileSystemConfigSetService.java 
b/solr/core/src/java/org/apache/solr/core/FileSystemConfigSetService.java
index 496e9fa86a4..0baeadd727a 100644
--- a/solr/core/src/java/org/apache/solr/core/FileSystemConfigSetService.java
+++ b/solr/core/src/java/org/apache/solr/core/FileSystemConfigSetService.java
@@ -42,7 +42,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * FileSystem ConfigSetService impl.
+ * File system based ConfigSetService implementation.
  *
  * <p>Loads a ConfigSet defined by the core's configSet property, looking for 
a directory named for
  * the configSet property value underneath a base directory. If no configSet 
property is set, loads
@@ -182,6 +182,11 @@ public class FileSystemConfigSetService extends 
ConfigSetService {
     }
 
     if (overwriteOnExists || !Files.exists(configsetFilePath)) {
+      // Create parent directories if they don't exist (similar to ZK's 
makePath)
+      Path parent = configsetFilePath.getParent();
+      if (parent != null && !Files.exists(parent)) {
+        Files.createDirectories(parent);
+      }
       Files.write(configsetFilePath, data);
     }
   }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/ConfigSetsHandler.java 
b/solr/core/src/java/org/apache/solr/handler/admin/ConfigSetsHandler.java
index edcdc0b1088..50159bf1abd 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ConfigSetsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ConfigSetsHandler.java
@@ -18,7 +18,6 @@ package org.apache.solr.handler.admin;
 
 import static org.apache.solr.common.params.CommonParams.NAME;
 
-import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -39,21 +38,20 @@ import org.apache.solr.handler.api.V2ApiUtils;
 import org.apache.solr.handler.configsets.CloneConfigSet;
 import org.apache.solr.handler.configsets.ConfigSetAPIBase;
 import org.apache.solr.handler.configsets.DeleteConfigSet;
+import org.apache.solr.handler.configsets.DownloadConfigSet;
+import org.apache.solr.handler.configsets.GetConfigSetFile;
 import org.apache.solr.handler.configsets.ListConfigSets;
 import org.apache.solr.handler.configsets.UploadConfigSet;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.security.AuthorizationContext;
 import org.apache.solr.security.PermissionNameProvider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** A {@link org.apache.solr.request.SolrRequestHandler} for ConfigSets API 
requests. */
 public class ConfigSetsHandler extends RequestHandlerBase implements 
PermissionNameProvider {
   // TODO refactor into o.a.s.handler.configsets package to live alongside 
actual API logic
   public static final String DEFAULT_CONFIGSET_NAME = "_default";
   public static final String AUTOCREATED_CONFIGSET_SUFFIX = ".AUTOCREATED";
-  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
   protected final CoreContainer coreContainer;
   public static long CONFIG_SET_TIMEOUT = 300 * 1000;
 
@@ -106,10 +104,15 @@ public class ConfigSetsHandler extends RequestHandlerBase 
implements PermissionN
           uploadResponse =
               uploadApi.uploadConfigSet(configSetName, overwrite, cleanup, 
configSetData);
         } else { // Uploading a single file
+          // Single file uploads do not support cleanup or overwrite parameters
+          if (cleanup) {
+            throw new SolrException(
+                ErrorCode.BAD_REQUEST,
+                "ConfigSet uploads do not allow cleanup=true when filePath is 
used.");
+          }
+          // Note: overwrite parameter is ignored for single file uploads 
(always overwrites)
           final var filePath = req.getParams().get(ConfigSetParams.FILE_PATH);
-          uploadResponse =
-              uploadApi.uploadConfigSetFile(
-                  configSetName, filePath, overwrite, cleanup, configSetData);
+          uploadResponse = uploadApi.uploadConfigSetFile(configSetName, 
filePath, configSetData);
         }
         V2ApiUtils.squashIntoSolrResponseWithoutHeader(rsp, uploadResponse);
         break;
@@ -119,7 +122,7 @@ public class ConfigSetsHandler extends RequestHandlerBase 
implements PermissionN
         break;
       case CREATE:
         final String newConfigSetName = req.getParams().get(NAME);
-        if (newConfigSetName == null || newConfigSetName.length() == 0) {
+        if (newConfigSetName == null || newConfigSetName.isEmpty()) {
           throw new SolrException(ErrorCode.BAD_REQUEST, "ConfigSet name not 
specified");
         }
 
@@ -187,7 +190,12 @@ public class ConfigSetsHandler extends RequestHandlerBase 
implements PermissionN
   @Override
   public Collection<Class<? extends JerseyResource>> getJerseyResources() {
     return List.of(
-        ListConfigSets.class, CloneConfigSet.class, DeleteConfigSet.class, 
UploadConfigSet.class);
+        ListConfigSets.class,
+        CloneConfigSet.class,
+        DeleteConfigSet.class,
+        UploadConfigSet.class,
+        DownloadConfigSet.class,
+        GetConfigSetFile.class);
   }
 
   @Override
diff --git 
a/solr/core/src/java/org/apache/solr/handler/configsets/CloneConfigSet.java 
b/solr/core/src/java/org/apache/solr/handler/configsets/CloneConfigSet.java
index 0ceb5a830be..c90a3636563 100644
--- a/solr/core/src/java/org/apache/solr/handler/configsets/CloneConfigSet.java
+++ b/solr/core/src/java/org/apache/solr/handler/configsets/CloneConfigSet.java
@@ -35,7 +35,11 @@ import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 
-/** V2 API implementation for ConfigsetsApi.Clone */
+/**
+ * V2 API implementation for creating a new configset form an existing one.
+ *
+ * <p>This API (GET /v2/configsets) is analogous to the v1 
/admin/configs?action=CREATE command.
+ */
 public class CloneConfigSet extends ConfigSetAPIBase implements 
ConfigsetsApi.Clone {
 
   @Inject
diff --git 
a/solr/core/src/java/org/apache/solr/handler/configsets/ConfigSetAPIBase.java 
b/solr/core/src/java/org/apache/solr/handler/configsets/ConfigSetAPIBase.java
index 0e780cf343d..5f291cab954 100644
--- 
a/solr/core/src/java/org/apache/solr/handler/configsets/ConfigSetAPIBase.java
+++ 
b/solr/core/src/java/org/apache/solr/handler/configsets/ConfigSetAPIBase.java
@@ -51,7 +51,7 @@ import org.slf4j.LoggerFactory;
  * Parent class for all APIs that manipulate configsets
  *
  * <p>Contains utilities for tasks common in configset manipulation, including 
running configset
- * "commands" and checking configset "trusted-ness".
+ * "commands".
  */
 public class ConfigSetAPIBase extends JerseyResource {
 
@@ -112,20 +112,6 @@ public class ConfigSetAPIBase extends JerseyResource {
     return contentStreamsIterator.next().getStream();
   }
 
-  protected void createBaseNode(
-      ConfigSetService configSetService,
-      boolean overwritesExisting,
-      boolean requestIsTrusted,
-      String configName)
-      throws IOException {
-    if (overwritesExisting) {
-      if (!requestIsTrusted) {
-        throw new SolrException(
-            SolrException.ErrorCode.BAD_REQUEST, "Trying to make an untrusted 
ConfigSet update");
-      }
-    }
-  }
-
   private void sendToOverseer(
       SolrQueryResponse rsp, ConfigSetParams.ConfigSetAction action, 
Map<String, Object> result)
       throws KeeperException, InterruptedException {
diff --git 
a/solr/core/src/java/org/apache/solr/handler/configsets/DeleteConfigSet.java 
b/solr/core/src/java/org/apache/solr/handler/configsets/DeleteConfigSet.java
index 1a4b363a833..3b26c5e2fc2 100644
--- a/solr/core/src/java/org/apache/solr/handler/configsets/DeleteConfigSet.java
+++ b/solr/core/src/java/org/apache/solr/handler/configsets/DeleteConfigSet.java
@@ -32,7 +32,11 @@ import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
 
-/** V2 API implementation for ConfigsetsApi.Delete */
+/**
+ * V2 API implementation for deleting a configset
+ *
+ * <p>This API (GET /v2/configsets) is analogous to the v1 
/admin/configs?action=DELETE command.
+ */
 public class DeleteConfigSet extends ConfigSetAPIBase implements 
ConfigsetsApi.Delete {
 
   @Inject
diff --git 
a/solr/core/src/java/org/apache/solr/handler/configsets/DownloadConfigSet.java 
b/solr/core/src/java/org/apache/solr/handler/configsets/DownloadConfigSet.java
new file mode 100644
index 00000000000..729aaf00d91
--- /dev/null
+++ 
b/solr/core/src/java/org/apache/solr/handler/configsets/DownloadConfigSet.java
@@ -0,0 +1,133 @@
+/*
+ * 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.solr.handler.configsets;
+
+import static 
org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.StreamingOutput;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.apache.commons.io.file.PathUtils;
+import org.apache.solr.client.api.endpoint.ConfigsetsApi;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.StrUtils;
+import org.apache.solr.core.ConfigSetService;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/** V2 API implementation for downloading a configset as a zip file. */
+public class DownloadConfigSet extends ConfigSetAPIBase implements 
ConfigsetsApi.Download {
+
+  @Inject
+  public DownloadConfigSet(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
+
+  @Override
+  @PermissionName(CONFIG_READ_PERM)
+  public Response downloadConfigSet(String configSetName) throws Exception {
+    if (StrUtils.isNullOrEmpty(configSetName)) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST, "No configset name provided to 
download");
+    }
+    if (!configSetService.checkConfigExists(configSetName)) {
+      throw new SolrException(
+          SolrException.ErrorCode.NOT_FOUND, "ConfigSet " + configSetName + " 
not found!");
+    }
+    return buildZipResponse(configSetService, configSetName);
+  }
+
+  /**
+   * Build a ZIP download {@link Response} for the given configset.
+   *
+   * @param configSetService the service to use for downloading the configset 
files
+   * @param configSetName the name of the configset to download
+   */
+  public static Response buildZipResponse(ConfigSetService configSetService, 
String configSetName)
+      throws IOException {
+    final byte[] zipBytes = zipConfigSet(configSetService, configSetName);
+    return Response.ok((StreamingOutput) outputStream -> 
outputStream.write(zipBytes))
+        .type("application/zip")
+        .build();
+  }
+
+  /**
+   * Download the named configset from {@link ConfigSetService} and return its 
contents as a ZIP
+   * archive byte array.
+   */
+  public static byte[] zipConfigSet(ConfigSetService configSetService, String 
configSetName)
+      throws IOException {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    Path tmpDirectory = Files.createTempDirectory("configset-download-");
+    try {
+      configSetService.downloadConfig(configSetName, tmpDirectory);
+      try (ZipOutputStream zipOut = new ZipOutputStream(baos)) {
+        Files.walkFileTree(
+            tmpDirectory,
+            new SimpleFileVisitor<>() {
+              @Override
+              public FileVisitResult preVisitDirectory(Path dir, 
BasicFileAttributes attrs)
+                  throws IOException {
+                if (Files.isHidden(dir)) {
+                  return FileVisitResult.SKIP_SUBTREE;
+                }
+                String dirName = tmpDirectory.relativize(dir).toString();
+                if (!dirName.isEmpty()) {
+                  if (!dirName.endsWith("/")) {
+                    dirName += "/";
+                  }
+                  zipOut.putNextEntry(new ZipEntry(dirName));
+                  zipOut.closeEntry();
+                }
+                return FileVisitResult.CONTINUE;
+              }
+
+              @Override
+              public FileVisitResult visitFile(Path file, BasicFileAttributes 
attrs)
+                  throws IOException {
+                if (!Files.isHidden(file)) {
+                  try (InputStream fis = Files.newInputStream(file)) {
+                    ZipEntry zipEntry = new 
ZipEntry(tmpDirectory.relativize(file).toString());
+                    zipOut.putNextEntry(zipEntry);
+                    fis.transferTo(zipOut);
+                  }
+                }
+                return FileVisitResult.CONTINUE;
+              }
+            });
+      }
+    } finally {
+      PathUtils.deleteDirectory(tmpDirectory);
+    }
+    return baos.toByteArray();
+  }
+}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/configsets/GetConfigSetFile.java 
b/solr/core/src/java/org/apache/solr/handler/configsets/GetConfigSetFile.java
new file mode 100644
index 00000000000..75fdaa9272a
--- /dev/null
+++ 
b/solr/core/src/java/org/apache/solr/handler/configsets/GetConfigSetFile.java
@@ -0,0 +1,75 @@
+/*
+ * 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.solr.handler.configsets;
+
+import static 
org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.core.StreamingOutput;
+import java.io.IOException;
+import org.apache.solr.client.api.endpoint.ConfigsetsApi;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.StrUtils;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.jersey.PermissionName;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/** V2 API implementation for reading the contents of a single file from an 
existing configset. */
+public class GetConfigSetFile extends ConfigSetAPIBase implements 
ConfigsetsApi.GetFile {
+
+  @Inject
+  public GetConfigSetFile(
+      CoreContainer coreContainer,
+      SolrQueryRequest solrQueryRequest,
+      SolrQueryResponse solrQueryResponse) {
+    super(coreContainer, solrQueryRequest, solrQueryResponse);
+  }
+
+  @Override
+  @PermissionName(CONFIG_READ_PERM)
+  public StreamingOutput getConfigSetFile(String configSetName, String 
filePath) throws Exception {
+    if (StrUtils.isNullOrEmpty(configSetName)) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No 
configset name provided");
+    }
+    if (StrUtils.isNullOrEmpty(filePath)) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No file 
path provided");
+    }
+    if (!configSetService.checkConfigExists(configSetName)) {
+      throw new SolrException(
+          SolrException.ErrorCode.NOT_FOUND, "ConfigSet '" + configSetName + 
"' not found");
+    }
+
+    // Return StreamingOutput that writes raw bytes (supports both text and 
binary files)
+    return output -> {
+      try {
+        final byte[] data = 
configSetService.downloadFileFromConfig(configSetName, filePath);
+        if (data == null) {
+          throw new SolrException(
+              SolrException.ErrorCode.NOT_FOUND,
+              "File '" + filePath + "' not found in configset '" + 
configSetName + "'");
+        }
+        output.write(data);
+      } catch (IOException e) {
+        throw new SolrException(
+            SolrException.ErrorCode.NOT_FOUND,
+            "File '" + filePath + "' not found in configset '" + configSetName 
+ "'",
+            e);
+      }
+    };
+  }
+}
diff --git 
a/solr/core/src/java/org/apache/solr/handler/configsets/UploadConfigSet.java 
b/solr/core/src/java/org/apache/solr/handler/configsets/UploadConfigSet.java
index fee660a87f5..6728b17ef10 100644
--- a/solr/core/src/java/org/apache/solr/handler/configsets/UploadConfigSet.java
+++ b/solr/core/src/java/org/apache/solr/handler/configsets/UploadConfigSet.java
@@ -40,7 +40,13 @@ import org.apache.solr.util.FileTypeMagicUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class UploadConfigSet extends ConfigSetAPIBase implements 
ConfigsetsApi.Upload {
+/**
+ * V2 API implementation for uploading a configset as a zip file.
+ *
+ * <p>This API (GET /v2/configsets) is analogous to the v1 
/admin/configs?action=UPLOAD command.
+ */
+public class UploadConfigSet extends ConfigSetAPIBase
+    implements ConfigsetsApi.Upload, ConfigsetsApi.PutFile {
 
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
@@ -104,26 +110,19 @@ public class UploadConfigSet extends ConfigSetAPIBase 
implements ConfigsetsApi.U
   @Override
   @PermissionName(CONFIG_EDIT_PERM)
   public SolrJerseyResponse uploadConfigSetFile(
-      String configSetName,
-      String filePath,
-      Boolean overwrite,
-      Boolean cleanup,
-      InputStream requestBody)
-      throws IOException {
+      String configSetName, String filePath, InputStream requestBody) throws 
IOException {
     final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
     ensureConfigSetUploadEnabled();
     SolrIdentifierValidator.validateConfigSetName(configSetName);
 
-    boolean overwritesExisting = 
configSetService.checkConfigExists(configSetName);
+    boolean overwrite = true;
 
     // Get upload parameters
 
     String singleFilePath = filePath != null ? filePath : "";
-    if (overwrite == null) overwrite = true;
-    if (cleanup == null) cleanup = false;
 
     String fixedSingleFilePath = singleFilePath;
-    if (fixedSingleFilePath.charAt(0) == '/') {
+    if (!fixedSingleFilePath.isEmpty() && fixedSingleFilePath.charAt(0) == 
'/') {
       fixedSingleFilePath = fixedSingleFilePath.substring(1);
     }
     byte[] data = requestBody.readAllBytes();
@@ -138,11 +137,6 @@ public class UploadConfigSet extends ConfigSetAPIBase 
implements ConfigsetsApi.U
           "The file type provided for upload, '"
               + singleFilePath
               + "', is forbidden for use in configSets.");
-    } else if (cleanup) {
-      // Cleanup is not allowed while using singleFilePath upload
-      throw new SolrException(
-          SolrException.ErrorCode.BAD_REQUEST,
-          "ConfigSet uploads do not allow cleanup=true when file path is 
used.");
     } else {
       // Create a node for the configuration in config
       // For creating the baseNode, the cleanup parameter is only allowed to 
be true when
diff --git 
a/solr/core/src/test/org/apache/solr/cloud/MockScriptUpdateProcessorFactory.java
 
b/solr/core/src/test/org/apache/solr/cloud/MockScriptUpdateProcessorFactory.java
index de6be081bf4..87a444453fc 100644
--- 
a/solr/core/src/test/org/apache/solr/cloud/MockScriptUpdateProcessorFactory.java
+++ 
b/solr/core/src/test/org/apache/solr/cloud/MockScriptUpdateProcessorFactory.java
@@ -25,10 +25,9 @@ import 
org.apache.solr.update.processor.UpdateRequestProcessor;
 import org.apache.solr.update.processor.UpdateRequestProcessorFactory;
 
 /**
- * The scripting update processor capability is something that is only allowed 
by a trusted
- * configSet. The actual code lives in the /modules/scripting project, however 
the test for trusted
- * configsets lives in TestConfigSetsAPI. This class is meant to simulate the
- * ScriptUpdateProcessorFactory for this test.
+ * Mock implementation of ScriptUpdateProcessorFactory for testing purposes. 
The actual code lives
+ * in the /modules/scripting project, however the test lives in 
TestConfigSetsAPI. This class is
+ * meant to simulate the ScriptUpdateProcessorFactory for this test.
  */
 public class MockScriptUpdateProcessorFactory extends 
UpdateRequestProcessorFactory {
 
diff --git a/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPI.java 
b/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPI.java
index d0e6a70e66d..6736af93b68 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPI.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPI.java
@@ -19,7 +19,6 @@ package org.apache.solr.cloud;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.apache.solr.common.params.CommonParams.NAME;
 import static org.apache.solr.core.ConfigSetProperties.DEFAULT_FILENAME;
-import static org.hamcrest.CoreMatchers.containsString;
 
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.http.HttpServletRequest;
@@ -67,6 +66,7 @@ import 
org.apache.solr.client.solrj.request.ConfigSetAdminRequest;
 import org.apache.solr.client.solrj.request.ConfigSetAdminRequest.Create;
 import org.apache.solr.client.solrj.request.ConfigSetAdminRequest.Delete;
 import org.apache.solr.client.solrj.request.ConfigSetAdminRequest.Upload;
+import org.apache.solr.client.solrj.request.ConfigsetsApi;
 import org.apache.solr.client.solrj.request.GenericSolrRequest;
 import org.apache.solr.client.solrj.request.schema.SchemaRequest;
 import org.apache.solr.client.solrj.response.CollectionAdminResponse;
@@ -592,15 +592,9 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
     }
   }
 
-  @Test
-  public void testSingleFileOverwriteV1() throws Exception {
-    testSingleFileOverwrite(false);
-  }
+  // Single file uploads do not support overwrite parameter (always overwrites)
 
-  @Test
-  public void testSingleFileOverwriteV2() throws Exception {
-    testSingleFileOverwrite(true);
-  }
+  // V2 API not tested: single file uploads always overwrite (no overwrite 
parameter)
 
   public void testSingleFileOverwrite(boolean v2) throws Exception {
     String configsetName = "regular";
@@ -743,19 +737,12 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
             zkClient, configsetName, configsetSuffix, 
"test/upload/path/solrconfig.xml"));
   }
 
+  // Single file uploads do not support cleanup parameter
+  // V2 API not tested: single file uploads do not support cleanup parameter
   @Test
-  public void testSingleWithCleanupV1() throws Exception {
-    testSingleWithCleanup(false);
-  }
-
-  @Test
-  public void testSingleWithCleanupV2() throws Exception {
-    testSingleWithCleanup(true);
-  }
-
-  public void testSingleWithCleanup(boolean v2) throws Exception {
+  public void testSingleWithCleanup() throws Exception {
     String configsetName = "regular";
-    String configsetSuffix = "testSinglePathCleanup-1-" + v2;
+    String configsetSuffix = "testSinglePathCleanup-1";
     uploadConfigSetWithAssertions(configsetName, configsetSuffix, null);
     try (SolrZkClient zkClient =
         new SolrZkClient.Builder()
@@ -774,7 +761,7 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
               "/test/upload/path/solrconfig.xml",
               true,
               true,
-              v2));
+              false));
       assertFalse(
           "New file should not exist, since the trust check did not succeed.",
           zkClient.exists(
@@ -831,19 +818,13 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
     }
   }
 
-  @Test
-  public void testSingleFileUntrustedV1() throws Exception {
-    testSingleFileUntrusted(false);
-  }
+  // Single file uploads do not support cleanup parameter
 
-  @Test
-  public void testSingleFileUntrustedV2() throws Exception {
-    testSingleFileUntrusted(true);
-  }
+  // V2 API not tested: single file uploads do not support cleanup parameter
 
-  public void testSingleFileUntrusted(boolean v2) throws Exception {
+  public void testSingleFileUpload(boolean v2) throws Exception {
     String configsetName = "regular";
-    String configsetSuffix = "testSinglePathUntrusted-1-" + v2;
+    String configsetSuffix = "testSinglePathUpload-1-" + v2;
     uploadConfigSetWithAssertions(configsetName, configsetSuffix, null);
 
     try (SolrZkClient zkClient =
@@ -852,8 +833,7 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
             .withTimeout(AbstractZkTestCase.TIMEOUT, TimeUnit.MILLISECONDS)
             .withConnTimeOut(45000, TimeUnit.MILLISECONDS)
             .build()) {
-      // New file with trusted request
-
+      // Upload new file to first path
       assertEquals(
           0,
           uploadSingleConfigSetFile(
@@ -872,13 +852,13 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
               zkClient, configsetName, configsetSuffix, 
"test/upload/path/solrconfig.xml"));
       assertConfigsetFiles(configsetName, configsetSuffix, zkClient);
 
-      // New file with untrusted request
+      // Upload new file to different path
       assertEquals(
           0,
           uploadSingleConfigSetFile(
               configsetName,
               configsetSuffix,
-              null,
+              "solr",
               "solr/configsets/upload/regular/solrconfig.xml",
               "/test/different/path/solrconfig.xml",
               false,
@@ -891,7 +871,7 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
               zkClient, configsetName, configsetSuffix, 
"test/different/path/solrconfig.xml"));
       assertConfigsetFiles(configsetName, configsetSuffix, zkClient);
 
-      // Overwrite with trusted request
+      // Overwrite existing file
       int extraFileZkVersion =
           getConfigZNodeVersion(
               zkClient, configsetName, configsetSuffix, 
"test/different/path/solrconfig.xml");
@@ -913,29 +893,7 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
                   zkClient, configsetName, configsetSuffix, 
"test/different/path/solrconfig.xml"));
       assertConfigsetFiles(configsetName, configsetSuffix, zkClient);
 
-      // Overwrite with untrusted request
-      extraFileZkVersion =
-          getConfigZNodeVersion(
-              zkClient, configsetName, configsetSuffix, 
"test/upload/path/solrconfig.xml");
-      assertEquals(
-          0,
-          uploadSingleConfigSetFile(
-              configsetName,
-              configsetSuffix,
-              null,
-              "solr/configsets/upload/regular/solrconfig.xml",
-              "/test/upload/path/solrconfig.xml",
-              true,
-              false,
-              v2));
-      assertTrue(
-          "Expecting version bump",
-          extraFileZkVersion
-              < getConfigZNodeVersion(
-                  zkClient, configsetName, configsetSuffix, 
"test/upload/path/solrconfig.xml"));
-      assertConfigsetFiles(configsetName, configsetSuffix, zkClient);
-
-      // Make sure that cleanup flag does not result in configSet being 
trusted.
+      // Make sure that cleanup flag validation works correctly.
       ignoreException("ConfigSet uploads do not allow cleanup=true when 
filePath is used.");
       extraFileZkVersion =
           getConfigZNodeVersion(
@@ -973,20 +931,19 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
 
   public void testSingleFileNewConfig(boolean v2) throws Exception {
     String configsetName = "regular";
-    String configsetSuffixTrusted = "testSinglePathNewConfig-1-" + v2;
-    String configsetSuffixUntrusted = "testSinglePathNewConfig-2-" + v2;
+    String configsetSuffix = "testSinglePathNewConfig-" + v2;
     try (SolrZkClient zkClient =
         new SolrZkClient.Builder()
             .withUrl(cluster.getZkServer().getZkAddress())
             .withTimeout(AbstractZkTestCase.TIMEOUT, TimeUnit.MILLISECONDS)
             .withConnTimeOut(45000, TimeUnit.MILLISECONDS)
             .build()) {
-      // New file with trusted request
+      // Upload single file to create new configset
       assertEquals(
           0,
           uploadSingleConfigSetFile(
               configsetName,
-              configsetSuffixTrusted,
+              configsetSuffix,
               "solr",
               "solr/configsets/upload/regular/solrconfig.xml",
               "solrconfig.xml",
@@ -996,35 +953,10 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
       assertEquals(
           "Expecting first version of new file",
           0,
-          getConfigZNodeVersion(zkClient, configsetName, 
configsetSuffixTrusted, "solrconfig.xml"));
+          getConfigZNodeVersion(zkClient, configsetName, configsetSuffix, 
"solrconfig.xml"));
       List<String> children =
           zkClient.getChildren(
-              String.format(Locale.ROOT, "/configs/%s%s", configsetName, 
configsetSuffixTrusted),
-              null);
-      assertEquals("The configSet should only have one file uploaded.", 1, 
children.size());
-      assertEquals("Incorrect file uploaded.", "solrconfig.xml", 
children.get(0));
-
-      // New file with trusted request
-      assertEquals(
-          0,
-          uploadSingleConfigSetFile(
-              configsetName,
-              configsetSuffixUntrusted,
-              null,
-              "solr/configsets/upload/regular/solrconfig.xml",
-              "solrconfig.xml",
-              false,
-              false,
-              v2));
-      assertEquals(
-          "Expecting first version of new file",
-          0,
-          getConfigZNodeVersion(
-              zkClient, configsetName, configsetSuffixUntrusted, 
"solrconfig.xml"));
-      children =
-          zkClient.getChildren(
-              String.format(Locale.ROOT, "/configs/%s%s", configsetName, 
configsetSuffixUntrusted),
-              null);
+              String.format(Locale.ROOT, "/configs/%s%s", configsetName, 
configsetSuffix), null);
       assertEquals("The configSet should only have one file uploaded.", 1, 
children.size());
       assertEquals("Incorrect file uploaded.", "solrconfig.xml", 
children.get(0));
     }
@@ -1043,7 +975,7 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
 
   @Test
   public void testUpload() throws Exception {
-    String suffix = "-untrusted";
+    String suffix = "-test";
     uploadConfigSetWithAssertions("regular", suffix, null);
     // try to create a collection with the uploaded configset
     createCollection("newcollection", "regular" + suffix, 1, 1, 
cluster.getSolrClient());
@@ -1054,37 +986,12 @@ public class TestConfigSetsAPI extends SolrCloudTestCase 
{
     Assume.assumeNotNull((new 
ScriptEngineManager()).getEngineByExtension("js"));
     Assume.assumeNotNull((new 
ScriptEngineManager()).getEngineByName("JavaScript"));
 
-    // Authorization off
-    final String untrustedSuffix = "-untrusted";
-    uploadConfigSetWithAssertions("with-script-processor", untrustedSuffix, 
null);
-    // try to create a collection with the uploaded configset
-    ignoreException("uploaded without any authentication in place");
-    Throwable thrown =
-        expectThrows(
-            RemoteSolrException.class,
-            () -> {
-              createCollection(
-                  "newcollection2",
-                  "with-script-processor" + untrustedSuffix,
-                  1,
-                  1,
-                  cluster.getSolrClient());
-            });
-    unIgnoreException("uploaded without any authentication in place");
-
-    assertThat(thrown.getMessage(), containsString("Underlying core creation 
failed"));
-
-    // Authorization on
-    final String trustedSuffix = "-trusted";
-    uploadConfigSetWithAssertions("with-script-processor", trustedSuffix, 
"solr");
-    // try to create a collection with the uploaded configset
+    // Upload configset with script processor and create collection
+    final String suffix = "-test";
+    uploadConfigSetWithAssertions("with-script-processor", suffix, "solr");
     CollectionAdminResponse resp =
         createCollection(
-            "newcollection2",
-            "with-script-processor" + trustedSuffix,
-            1,
-            1,
-            cluster.getSolrClient());
+            "newcollection2", "with-script-processor" + suffix, 1, 1, 
cluster.getSolrClient());
     scriptRequest("newcollection2");
   }
 
@@ -1095,6 +1002,89 @@ public class TestConfigSetsAPI extends SolrCloudTestCase 
{
     assertEquals(400, res);
   }
 
+  @Test
+  public void testGetFile() throws Exception {
+    String configSetName = "regular";
+    String configSetSuffix = "testGetFile";
+
+    // First upload a configset
+    uploadConfigSetWithAssertions(configSetName, configSetSuffix, null);
+
+    try (SolrZkClient zkClient =
+        new SolrZkClient.Builder()
+            .withUrl(cluster.getZkServer().getZkAddress())
+            .withTimeout(AbstractZkTestCase.TIMEOUT, TimeUnit.MILLISECONDS)
+            .withConnTimeOut(45000, TimeUnit.MILLISECONDS)
+            .build()) {
+
+      // Verify files exist in ZK
+      assertTrue(
+          zkClient.exists("/configs/" + configSetName + configSetSuffix + 
"/solrconfig.xml"));
+      assertTrue(
+          zkClient.exists("/configs/" + configSetName + configSetSuffix + 
"/managed-schema.xml"));
+
+      // Test getting a root-level file via V2 API
+      String baseUrl = 
cluster.getJettySolrRunners().get(0).getBaseURLV2().toString();
+      String getFileUrl =
+          baseUrl + "/configsets/" + configSetName + configSetSuffix + 
"/files/solrconfig.xml";
+
+      HttpClient httpClient = 
cluster.getJettySolrRunners().get(0).getSolrClient().getHttpClient();
+      ContentResponse response = 
httpClient.newRequest(getFileUrl).method(HttpMethod.GET).send();
+
+      byte[] responseBytes = response.getContent();
+      String content = new String(responseBytes, UTF_8);
+
+      assertNotNull(content);
+      assertTrue("Content should contain config XML", 
content.contains("<config"));
+
+      // Test getting a nested file
+      getFileUrl =
+          baseUrl + "/configsets/" + configSetName + configSetSuffix + 
"/files/managed-schema.xml";
+      response = 
httpClient.newRequest(getFileUrl).method(HttpMethod.GET).send();
+
+      responseBytes = response.getContent();
+      content = new String(responseBytes, UTF_8);
+
+      assertNotNull(content);
+      assertTrue("Content should contain schema XML", 
content.contains("schema"));
+
+      // Test binary file preservation by uploading a small binary file
+      byte[] binaryData =
+          new byte[] {
+            (byte) 0x89,
+            0x50,
+            0x4E,
+            0x47,
+            0x0D,
+            0x0A,
+            0x1A,
+            0x0A, // PNG signature
+            (byte) 0xFF,
+            (byte) 0xFE,
+            0x00,
+            0x01,
+            (byte) 0x80
+          };
+
+      // Upload binary file to ZK directly
+      zkClient.makePath(
+          "/configs/" + configSetName + configSetSuffix + "/test.bin",
+          binaryData,
+          CreateMode.PERSISTENT,
+          null);
+
+      // Retrieve it via API
+      getFileUrl = baseUrl + "/configsets/" + configSetName + configSetSuffix 
+ "/files/test.bin";
+      response = 
httpClient.newRequest(getFileUrl).method(HttpMethod.GET).send();
+
+      byte[] retrievedBytes = response.getContent();
+
+      // Binary data should be preserved exactly
+      assertArrayEquals(
+          "Binary file should be preserved byte-for-byte", binaryData, 
retrievedBytes);
+    }
+  }
+
   private static String getSecurityJson() {
     return "{\n"
         + "  'authentication':{\n"
@@ -1135,8 +1125,6 @@ public class TestConfigSetsAPI extends SolrCloudTestCase {
     assertTrue(
         "solrconfig.xml file should have been uploaded",
         zkClient.exists("/configs/" + configSetName + suffix + 
"/solrconfig.xml"));
-    byte data[] = zkClient.getData("/configs/" + configSetName + suffix, null, 
null);
-    // assertEquals("{\"trusted\": false}", new String(data, 
StandardCharsets.UTF_8));
     assertArrayEquals(
         "solrconfig.xml file contents on zookeeper are not exactly same as 
that of the file uploaded in config",
         zkClient.getData("/configs/" + configSetName + suffix + 
"/solrconfig.xml", null, null),
@@ -1189,21 +1177,20 @@ public class TestConfigSetsAPI extends 
SolrCloudTestCase {
       throws Exception {
 
     if (v2) {
-      // TODO: switch to using V2Request
-
-      final ByteBuffer fileBytes = 
TestSolrConfigHandler.getFileContent(file.toString(), false);
-      final String uriEnding =
-          "/configsets/"
-              + configSetName
-              + suffix
-              + (!overwrite ? "?overwrite=false" : "")
-              + (cleanup ? "?cleanup=true" : "");
-      final boolean usePut = true;
-      JettySolrRunner jetty = cluster.getJettySolrRunners().getFirst();
-      Map<?, ?> map =
-          postDataAndGetResponse(
-              jetty, jetty.getBaseURLV2() + uriEnding, fileBytes, username, 
usePut);
-      return getStatusCode(map);
+      // Use generated v2 SolrJ client (ConfigsetsApi)
+      try (InputStream fileBytes = new FileInputStream(file.toFile())) {
+        // Create and execute the upload request using generated client
+        final var uploadRequest =
+            new ConfigsetsApi.UploadConfigSet(configSetName + suffix, 
fileBytes);
+        uploadRequest.setOverwrite(overwrite);
+        uploadRequest.setCleanup(cleanup);
+
+        final var response = uploadRequest.process(cluster.getSolrClient());
+        return response.responseHeader.status;
+      } catch (RemoteSolrException e) {
+        // Convert exception to status code for consistent error handling with 
v1
+        return e.code();
+      }
     } // else "not" a V2 request...
 
     try {
@@ -1236,44 +1223,51 @@ public class TestConfigSetsAPI extends 
SolrCloudTestCase {
     final Path file = SolrTestCaseJ4.getFile(localFilePath);
 
     if (v2) {
-      // TODO: switch to use V2Request
-
-      final ByteBuffer sampleConfigFile =
-          TestSolrConfigHandler.getFileContent(file.toString(), false);
-      if (uploadPath != null && !uploadPath.startsWith("/")) {
-        uploadPath = "/" + uploadPath;
-      }
-      final String uriEnding =
-          "/configsets/"
-              + configSetName
-              + suffix
-              + uploadPath
-              + (!overwrite ? "?overwrite=false" : "")
-              + (cleanup ? "?cleanup=true" : "");
-      final boolean usePut = true;
+      // Use generated v2 SolrJ client (ConfigsetsApi)
+      try {
+        final ByteBuffer sampleConfigFileBuffer =
+            TestSolrConfigHandler.getFileContent(file.toString(), false);
+
+        // Convert ByteBuffer to InputStream for the generated client
+        final InputStream sampleConfigFile =
+            new ByteArrayInputStream(
+                sampleConfigFileBuffer.array(),
+                sampleConfigFileBuffer.arrayOffset(),
+                sampleConfigFileBuffer.limit());
+
+        if (uploadPath.startsWith("/")) {
+          uploadPath = uploadPath.substring(1);
+        }
 
-      JettySolrRunner jetty = cluster.getJettySolrRunners().getFirst();
-      Map<?, ?> map =
-          postDataAndGetResponse(
-              jetty, jetty.getBaseURLV2() + uriEnding, sampleConfigFile, 
username, usePut);
-      return getStatusCode(map);
-    } // else "not" a V2 request...
+        // Create and execute the upload request using generated client
+        final var uploadRequest =
+            new ConfigsetsApi.UploadConfigSetFile(
+                configSetName + suffix, uploadPath, sampleConfigFile);
 
-    try {
-      return (new Upload())
-          .setConfigSetName(configSetName + suffix)
-          .setFilePath(uploadPath)
-          // NOTE: server doesn't actually care, and test plumbing doesn't 
tell us
-          .setUploadFile(file, "application/octet-stream")
-          .setOverwrite(overwrite ? true : null) // expect server default to 
be 'false'
-          .setCleanup(cleanup ? true : null) // expect server default to be 
'false'
-          .setBasicAuthCredentials(username, username) // for our 
MockAuthenticationPlugin
-          .process(cluster.getSolrClient())
-          .getStatus();
-    } catch (SolrServerException e1) {
-      throw new AssertionError("Server error uploading file to configset: " + 
e1, e1);
-    } catch (SolrException e2) {
-      return e2.code();
+        final var response = uploadRequest.process(cluster.getSolrClient());
+        return response.responseHeader.status;
+      } catch (RemoteSolrException e) {
+        // Convert exception to status code for consistent error handling with 
v1
+        return e.code();
+      }
+    } else { // else "not" a V2 request...
+
+      try {
+        return (new Upload())
+            .setConfigSetName(configSetName + suffix)
+            .setFilePath(uploadPath)
+            // NOTE: server doesn't actually care, and test plumbing doesn't 
tell us
+            .setUploadFile(file, "application/octet-stream")
+            .setOverwrite(overwrite ? true : null) // expect server default to 
be 'false'
+            .setCleanup(cleanup ? true : null) // expect server default to be 
'false'
+            .setBasicAuthCredentials(username, username) // for our 
MockAuthenticationPlugin
+            .process(cluster.getSolrClient())
+            .getStatus();
+      } catch (SolrServerException e1) {
+        throw new AssertionError("Server error uploading file to configset: " 
+ e1, e1);
+      } catch (SolrException e2) {
+        return e2.code();
+      }
     }
   }
 
diff --git 
a/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPIExclusivity.java 
b/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPIExclusivity.java
index 6cc865c2fb9..722ed01c1fa 100644
--- a/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPIExclusivity.java
+++ b/solr/core/src/test/org/apache/solr/cloud/TestConfigSetsAPIExclusivity.java
@@ -91,8 +91,6 @@ public class TestConfigSetsAPIExclusivity extends 
SolrTestCaseJ4 {
 
   private void setupBaseConfigSet(String baseConfigSetName) throws Exception {
     solrCluster.uploadConfigSet(configset("configset-2"), baseConfigSetName);
-    // Make configset untrusted
-    solrCluster.getZkClient();
   }
 
   private Exception getFirstExceptionOrNull(List<Exception> list) {
diff --git 
a/solr/core/src/test/org/apache/solr/handler/configsets/DownloadConfigSetAPITest.java
 
b/solr/core/src/test/org/apache/solr/handler/configsets/DownloadConfigSetAPITest.java
new file mode 100644
index 00000000000..10325b278ee
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/configsets/DownloadConfigSetAPITest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.solr.handler.configsets;
+
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import jakarta.ws.rs.core.Response;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.FileSystemConfigSetService;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/** Unit tests for {@link DownloadConfigSet}. */
+public class DownloadConfigSetAPITest extends SolrTestCase {
+
+  private CoreContainer mockCoreContainer;
+  private FileSystemConfigSetService configSetService;
+  private Path configSetBase;
+
+  @BeforeClass
+  public static void ensureWorkingMockito() {
+    assumeWorkingMockito();
+  }
+
+  @Before
+  public void initConfigSetService() {
+    configSetBase = createTempDir("configsets");
+    // Use an anonymous subclass to access the protected testing constructor
+    configSetService = new FileSystemConfigSetService(configSetBase) {};
+    mockCoreContainer = mock(CoreContainer.class);
+    when(mockCoreContainer.getConfigSetService()).thenReturn(configSetService);
+  }
+
+  /** Creates a configset directory with a single file so the API can find and 
zip it. */
+  private void createConfigSet(String name, String fileName, String content) 
throws Exception {
+    Path dir = configSetBase.resolve(name);
+    Files.createDirectories(dir);
+    Files.writeString(dir.resolve(fileName), content, StandardCharsets.UTF_8);
+  }
+
+  @Test
+  @SuppressWarnings("resource") // Response never created when exception is 
thrown
+  public void testMissingConfigSetNameThrowsBadRequest() {
+    final var api = new DownloadConfigSet(mockCoreContainer, null, null);
+    final var ex = assertThrows(SolrException.class, () -> 
api.downloadConfigSet(null));
+    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
+
+    final var ex2 = assertThrows(SolrException.class, () -> 
api.downloadConfigSet(""));
+    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex2.code());
+  }
+
+  @Test
+  @SuppressWarnings("resource") // Response never created when exception is 
thrown
+  public void testNonExistentConfigSetThrowsNotFound() {
+    // "missing" was never created in configSetBase, so checkConfigExists 
returns false
+    final var api = new DownloadConfigSet(mockCoreContainer, null, null);
+    final var ex = assertThrows(SolrException.class, () -> 
api.downloadConfigSet("missing"));
+    assertEquals(SolrException.ErrorCode.NOT_FOUND.code, ex.code());
+  }
+
+  @Test
+  public void testSuccessfulDownloadReturnsZipResponse() throws Exception {
+    createConfigSet("myconfig", "solrconfig.xml", "<config/>");
+
+    final var api = new DownloadConfigSet(mockCoreContainer, null, null);
+    try (final Response response = api.downloadConfigSet("myconfig")) {
+      assertEquals(200, response.getStatus());
+      assertEquals("application/zip", response.getMediaType().toString());
+    }
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/configsets/GetConfigSetFileAPITest.java
 
b/solr/core/src/test/org/apache/solr/handler/configsets/GetConfigSetFileAPITest.java
new file mode 100644
index 00000000000..2237930f89b
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/configsets/GetConfigSetFileAPITest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.solr.handler.configsets;
+
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import jakarta.ws.rs.core.StreamingOutput;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.FileSystemConfigSetService;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/** Unit tests for {@link GetConfigSetFile}. */
+public class GetConfigSetFileAPITest extends SolrTestCase {
+
+  private CoreContainer mockCoreContainer;
+  private FileSystemConfigSetService configSetService;
+  private Path configSetBase;
+
+  @BeforeClass
+  public static void ensureWorkingMockito() {
+    assumeWorkingMockito();
+  }
+
+  @Before
+  public void initConfigSetService() {
+    configSetBase = createTempDir("configsets");
+    // Use an anonymous subclass to access the protected testing constructor
+    configSetService = new FileSystemConfigSetService(configSetBase) {};
+    mockCoreContainer = mock(CoreContainer.class);
+    when(mockCoreContainer.getConfigSetService()).thenReturn(configSetService);
+  }
+
+  /** Creates a configset directory with one file. */
+  private void createConfigSetWithFile(String configSetName, String filePath, 
String content)
+      throws Exception {
+    Path dir = configSetBase.resolve(configSetName);
+    Files.createDirectories(dir);
+    Files.writeString(dir.resolve(filePath), content, StandardCharsets.UTF_8);
+  }
+
+  @Test
+  public void testMissingConfigSetNameThrowsBadRequest() {
+    final var api = new GetConfigSetFile(mockCoreContainer, null, null);
+    final var ex =
+        assertThrows(SolrException.class, () -> api.getConfigSetFile(null, 
"schema.xml"));
+    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
+
+    final var ex2 = assertThrows(SolrException.class, () -> 
api.getConfigSetFile("", "schema.xml"));
+    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex2.code());
+  }
+
+  @Test
+  public void testMissingFilePathThrowsBadRequest() {
+    final var api = new GetConfigSetFile(mockCoreContainer, null, null);
+    final var ex = assertThrows(SolrException.class, () -> 
api.getConfigSetFile("myconfig", null));
+    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
+
+    final var ex2 = assertThrows(SolrException.class, () -> 
api.getConfigSetFile("myconfig", ""));
+    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex2.code());
+  }
+
+  @Test
+  public void testNonExistentConfigSetThrowsNotFound() {
+    // "missing" was never created in configSetBase, so checkConfigExists 
returns false
+    final var api = new GetConfigSetFile(mockCoreContainer, null, null);
+    final var ex =
+        assertThrows(SolrException.class, () -> 
api.getConfigSetFile("missing", "schema.xml"));
+    assertEquals(SolrException.ErrorCode.NOT_FOUND.code, ex.code());
+  }
+
+  @Test
+  public void testSuccessfulFileRead() throws Exception {
+    final String configSetName = "myconfig";
+    final String filePath = "schema.xml";
+    final String fileContent = "<schema/>";
+    createConfigSetWithFile(configSetName, filePath, fileContent);
+
+    final var api = new GetConfigSetFile(mockCoreContainer, null, null);
+    final StreamingOutput streamingOutput = 
api.getConfigSetFile(configSetName, filePath);
+
+    assertNotNull(streamingOutput);
+
+    // Read the streamed bytes
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    streamingOutput.write(baos);
+    String actualContent = baos.toString(StandardCharsets.UTF_8);
+
+    assertEquals(fileContent, actualContent);
+  }
+
+  @Test
+  public void testFileNotFoundInConfigSetThrowsNotFound() throws Exception {
+    final String configSetName = "myconfig";
+    // Create the configset directory but do NOT add the requested file
+    Files.createDirectories(configSetBase.resolve(configSetName));
+
+    final var api = new GetConfigSetFile(mockCoreContainer, null, null);
+    final StreamingOutput streamingOutput = 
api.getConfigSetFile(configSetName, "missing.xml");
+
+    // Exception is thrown when we try to write the output
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    final var ex = assertThrows(SolrException.class, () -> 
streamingOutput.write(baos));
+    assertEquals(SolrException.ErrorCode.NOT_FOUND.code, ex.code());
+  }
+
+  @Test
+  public void testEmptyFileReturnsEmptyContent() throws Exception {
+    final String configSetName = "myconfig";
+    final String filePath = "empty.xml";
+    createConfigSetWithFile(configSetName, filePath, "");
+
+    final var api = new GetConfigSetFile(mockCoreContainer, null, null);
+    final StreamingOutput streamingOutput = 
api.getConfigSetFile(configSetName, filePath);
+
+    assertNotNull(streamingOutput);
+
+    // Read the streamed bytes
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    streamingOutput.write(baos);
+
+    assertEquals(0, baos.size());
+  }
+
+  @Test
+  public void testBinaryFilePreservedWithStreamingOutput() throws Exception {
+    // This test demonstrates that binary files are now correctly handled
+    // by returning raw bytes via StreamingOutput instead of UTF-8 String.
+    final String configSetName = "binarytest";
+    final String filePath = "logo.png";
+
+    // Create a file with binary data simulating a small PNG image header
+    // PNG signature: 89 50 4E 47 0D 0A 1A 0A (first 8 bytes of any PNG)
+    // Plus some additional binary data that would corrupt as UTF-8
+    byte[] binaryData =
+        new byte[] {
+          (byte) 0x89,
+          0x50,
+          0x4E,
+          0x47,
+          0x0D,
+          0x0A,
+          0x1A,
+          0x0A, // PNG signature
+          (byte) 0xFF,
+          (byte) 0xFE,
+          0x00,
+          0x01,
+          (byte) 0x80,
+          (byte) 0xDE,
+          (byte) 0xAD,
+          (byte) 0xBE,
+          (byte) 0xEF // Binary data
+        };
+
+    Path configDir = configSetBase.resolve(configSetName);
+    Files.createDirectories(configDir);
+    Files.write(configDir.resolve(filePath), binaryData);
+
+    final var api = new GetConfigSetFile(mockCoreContainer, null, null);
+    final StreamingOutput streamingOutput = 
api.getConfigSetFile(configSetName, filePath);
+
+    assertNotNull(streamingOutput);
+
+    // Read the streamed bytes
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    streamingOutput.write(baos);
+    byte[] responseBytes = baos.toByteArray();
+
+    assertArrayEquals(
+        "Binary file content should be preserved byte-for-byte", binaryData, 
responseBytes);
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/configsets/UploadConfigSetAPITest.java
 
b/solr/core/src/test/org/apache/solr/handler/configsets/UploadConfigSetAPITest.java
new file mode 100644
index 00000000000..b376a2592b1
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/configsets/UploadConfigSetAPITest.java
@@ -0,0 +1,307 @@
+/*
+ * 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.solr.handler.configsets;
+
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.FileSystemConfigSetService;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/** Unit tests for {@link UploadConfigSet#uploadConfigSet} (Upload interface). 
*/
+public class UploadConfigSetAPITest extends SolrTestCase {
+
+  private CoreContainer mockCoreContainer;
+  private FileSystemConfigSetService configSetService;
+  private Path configSetBase;
+
+  @BeforeClass
+  public static void ensureWorkingMockito() {
+    assumeWorkingMockito();
+  }
+
+  @Before
+  public void initConfigSetService() {
+    configSetBase = createTempDir("configsets");
+    // Use an anonymous subclass to access the protected testing constructor
+    configSetService = new FileSystemConfigSetService(configSetBase) {};
+    mockCoreContainer = mock(CoreContainer.class);
+    when(mockCoreContainer.getConfigSetService()).thenReturn(configSetService);
+  }
+
+  /** Creates an in-memory ZIP file with the specified files. */
+  @SuppressWarnings("try") // ZipOutputStream must be closed to finalize ZIP 
format
+  private InputStream createZipStream(String... filePathAndContent) throws 
Exception {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    try (ZipOutputStream zos = new ZipOutputStream(baos)) {
+      for (int i = 0; i < filePathAndContent.length; i += 2) {
+        String filePath = filePathAndContent[i];
+        String content = filePathAndContent[i + 1];
+        zos.putNextEntry(new ZipEntry(filePath));
+        zos.write(content.getBytes(StandardCharsets.UTF_8));
+        zos.closeEntry();
+      }
+    }
+    return new ByteArrayInputStream(baos.toByteArray());
+  }
+
+  /** Creates an empty ZIP file. */
+  @SuppressWarnings("try") // ZipOutputStream must be closed even with no 
entries
+  private InputStream createEmptyZipStream() throws Exception {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    try (ZipOutputStream zos = new ZipOutputStream(baos)) {
+      // No entries
+    }
+    return new ByteArrayInputStream(baos.toByteArray());
+  }
+
+  /** Creates a configset with files on disk for testing overwrites and 
cleanup. */
+  private void createExistingConfigSet(String configSetName, String... 
filePathAndContent)
+      throws Exception {
+    Path configDir = configSetBase.resolve(configSetName);
+    Files.createDirectories(configDir);
+    for (int i = 0; i < filePathAndContent.length; i += 2) {
+      String filePath = filePathAndContent[i];
+      String content = filePathAndContent[i + 1];
+      Path fullPath = configDir.resolve(filePath);
+      Files.createDirectories(fullPath.getParent());
+      Files.writeString(fullPath, content, StandardCharsets.UTF_8);
+    }
+  }
+
+  @Test
+  public void testSuccessfulZipUpload() throws Exception {
+    final String configSetName = "newconfig";
+    InputStream zipStream = createZipStream("solrconfig.xml", "<config/>");
+
+    final var api = new UploadConfigSet(mockCoreContainer, null, null);
+    final var response = api.uploadConfigSet(configSetName, true, false, 
zipStream);
+
+    assertNotNull(response);
+    assertTrue(
+        "ConfigSet should exist after upload", 
configSetService.checkConfigExists(configSetName));
+
+    // Verify the file was uploaded
+    byte[] uploadedData = 
configSetService.downloadFileFromConfig(configSetName, "solrconfig.xml");
+    assertEquals("<config/>", new String(uploadedData, 
StandardCharsets.UTF_8));
+  }
+
+  @Test
+  public void testSuccessfulZipUploadWithMultipleFiles() throws Exception {
+    final String configSetName = "multifile";
+    InputStream zipStream =
+        createZipStream(
+            "solrconfig.xml", "<config/>",
+            "schema.xml", "<schema/>",
+            "stopwords.txt", "a\nthe");
+
+    final var api = new UploadConfigSet(mockCoreContainer, null, null);
+    final var response = api.uploadConfigSet(configSetName, true, false, 
zipStream);
+
+    assertNotNull(response);
+    assertTrue(configSetService.checkConfigExists(configSetName));
+
+    // Verify all files were uploaded
+    byte[] solrconfig = configSetService.downloadFileFromConfig(configSetName, 
"solrconfig.xml");
+    assertEquals("<config/>", new String(solrconfig, StandardCharsets.UTF_8));
+
+    byte[] schema = configSetService.downloadFileFromConfig(configSetName, 
"schema.xml");
+    assertEquals("<schema/>", new String(schema, StandardCharsets.UTF_8));
+
+    byte[] stopwords = configSetService.downloadFileFromConfig(configSetName, 
"stopwords.txt");
+    assertEquals("a\nthe", new String(stopwords, StandardCharsets.UTF_8));
+  }
+
+  @Test
+  public void testEmptyZipThrowsBadRequest() throws Exception {
+    try (InputStream emptyZip = createEmptyZipStream()) {
+
+      final var api = new UploadConfigSet(mockCoreContainer, null, null);
+      final var ex =
+          assertThrows(
+              SolrException.class, () -> api.uploadConfigSet("newconfig", 
true, false, emptyZip));
+
+      assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
+      assertTrue(
+          "Error message should mention empty zip",
+          ex.getMessage().contains("empty zipped data") || 
ex.getMessage().contains("non-zipped"));
+    }
+  }
+
+  @Test
+  public void testNonZipDataThrowsBadRequest() {
+    // Send plain text instead of a ZIP
+    InputStream notAZip =
+        new ByteArrayInputStream("this is not a zip 
file".getBytes(StandardCharsets.UTF_8));
+
+    final var api = new UploadConfigSet(mockCoreContainer, null, null);
+    // This should fail either as bad ZIP or as empty ZIP
+    assertThrows(Exception.class, () -> api.uploadConfigSet("newconfig", true, 
false, notAZip));
+  }
+
+  @Test
+  public void testOverwriteExistingConfigSet() throws Exception {
+    final String configSetName = "existing";
+    // Create existing configset with old content
+    createExistingConfigSet(configSetName, "solrconfig.xml", "<old-config/>");
+
+    // Upload new content with overwrite=true
+    InputStream zipStream = createZipStream("solrconfig.xml", "<new-config/>");
+    final var api = new UploadConfigSet(mockCoreContainer, null, null);
+    final var response = api.uploadConfigSet(configSetName, true, false, 
zipStream);
+
+    assertNotNull(response);
+
+    // Verify the file was overwritten
+    byte[] uploadedData = 
configSetService.downloadFileFromConfig(configSetName, "solrconfig.xml");
+    assertEquals("<new-config/>", new String(uploadedData, 
StandardCharsets.UTF_8));
+  }
+
+  @Test
+  public void testOverwriteFalseThrowsExceptionWhenExists() throws Exception {
+    final String configSetName = "existing";
+    createExistingConfigSet(configSetName, "solrconfig.xml", "<old-config/>");
+
+    try (InputStream zipStream = createZipStream("solrconfig.xml", 
"<new-config/>")) {
+      final var api = new UploadConfigSet(mockCoreContainer, null, null);
+
+      final var ex =
+          assertThrows(
+              SolrException.class,
+              () -> api.uploadConfigSet(configSetName, false, false, 
zipStream));
+
+      assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
+      assertTrue(
+          "Error message should mention config already exists",
+          ex.getMessage().contains("already"));
+    }
+  }
+
+  @Test
+  public void testCleanupRemovesUnusedFiles() throws Exception {
+    final String configSetName = "cleanuptest";
+    // Create existing configset with multiple files
+    createExistingConfigSet(
+        configSetName,
+        "solrconfig.xml",
+        "<old-config/>",
+        "schema.xml",
+        "<old-schema/>",
+        "old-file.txt",
+        "to be deleted");
+
+    // Upload new ZIP with only one file and cleanup=true
+    InputStream zipStream = createZipStream("solrconfig.xml", "<new-config/>");
+    final var api = new UploadConfigSet(mockCoreContainer, null, null);
+    final var response = api.uploadConfigSet(configSetName, true, true, 
zipStream);
+
+    assertNotNull(response);
+
+    // Verify solrconfig.xml was updated
+    byte[] solrconfig = configSetService.downloadFileFromConfig(configSetName, 
"solrconfig.xml");
+    assertEquals("<new-config/>", new String(solrconfig, 
StandardCharsets.UTF_8));
+
+    // Verify old files were deleted (should throw or return null)
+    try {
+      byte[] oldSchema = 
configSetService.downloadFileFromConfig(configSetName, "schema.xml");
+      if (oldSchema != null) {
+        fail("schema.xml should have been deleted during cleanup");
+      }
+    } catch (Exception e) {
+      // Expected - file should not exist
+    }
+  }
+
+  @Test
+  public void testCleanupFalseKeepsExistingFiles() throws Exception {
+    final String configSetName = "nocleanup";
+    // Create existing configset with multiple files
+    createExistingConfigSet(
+        configSetName, "solrconfig.xml", "<old-config/>", "schema.xml", 
"<old-schema/>");
+
+    // Upload new ZIP with only one file and cleanup=false
+    InputStream zipStream = createZipStream("solrconfig.xml", "<new-config/>");
+    final var api = new UploadConfigSet(mockCoreContainer, null, null);
+    final var response = api.uploadConfigSet(configSetName, true, false, 
zipStream);
+
+    assertNotNull(response);
+
+    // Verify solrconfig.xml was updated
+    byte[] solrconfig = configSetService.downloadFileFromConfig(configSetName, 
"solrconfig.xml");
+    assertEquals("<new-config/>", new String(solrconfig, 
StandardCharsets.UTF_8));
+
+    // Verify schema.xml still exists
+    byte[] schema = configSetService.downloadFileFromConfig(configSetName, 
"schema.xml");
+    assertEquals("<old-schema/>", new String(schema, StandardCharsets.UTF_8));
+  }
+
+  @Test
+  public void testDefaultParametersWhenNull() throws Exception {
+    final String configSetName = "defaults";
+    InputStream zipStream = createZipStream("solrconfig.xml", "<config/>");
+
+    final var api = new UploadConfigSet(mockCoreContainer, null, null);
+    // Pass null for overwrite and cleanup - should use defaults 
(overwrite=true, cleanup=false)
+    final var response = api.uploadConfigSet(configSetName, null, null, 
zipStream);
+
+    assertNotNull(response);
+    assertTrue(configSetService.checkConfigExists(configSetName));
+  }
+
+  @Test
+  public void testZipWithDirectoryEntries() throws Exception {
+    final String configSetName = "withdirs";
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    try (ZipOutputStream zos = new ZipOutputStream(baos)) {
+      // Add directory entry
+      zos.putNextEntry(new ZipEntry("conf/"));
+      zos.closeEntry();
+
+      // Add file in directory
+      zos.putNextEntry(new ZipEntry("conf/solrconfig.xml"));
+      zos.write("<config/>".getBytes(StandardCharsets.UTF_8));
+      zos.closeEntry();
+    }
+    InputStream zipStream = new ByteArrayInputStream(baos.toByteArray());
+
+    final var api = new UploadConfigSet(mockCoreContainer, null, null);
+    final var response = api.uploadConfigSet(configSetName, true, false, 
zipStream);
+
+    assertNotNull(response);
+    assertTrue(configSetService.checkConfigExists(configSetName));
+
+    // Directory entries should be skipped, but file should be uploaded
+    byte[] uploadedData =
+        configSetService.downloadFileFromConfig(configSetName, 
"conf/solrconfig.xml");
+    assertEquals("<config/>", new String(uploadedData, 
StandardCharsets.UTF_8));
+  }
+}
diff --git 
a/solr/core/src/test/org/apache/solr/handler/configsets/UploadConfigSetFileAPITest.java
 
b/solr/core/src/test/org/apache/solr/handler/configsets/UploadConfigSetFileAPITest.java
new file mode 100644
index 00000000000..c58305b5fd3
--- /dev/null
+++ 
b/solr/core/src/test/org/apache/solr/handler/configsets/UploadConfigSetFileAPITest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.solr.handler.configsets;
+
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import org.apache.solr.SolrTestCase;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.FileSystemConfigSetService;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/** Unit tests for {@link UploadConfigSet#uploadConfigSetFile} */
+public class UploadConfigSetFileAPITest extends SolrTestCase {
+
+  private CoreContainer mockCoreContainer;
+  private FileSystemConfigSetService configSetService;
+  private Path configSetBase;
+
+  @BeforeClass
+  public static void ensureWorkingMockito() {
+    assumeWorkingMockito();
+  }
+
+  @Before
+  public void initConfigSetService() {
+    configSetBase = createTempDir("configsets");
+    // Use an anonymous subclass to access the protected testing constructor
+    configSetService = new FileSystemConfigSetService(configSetBase) {};
+    mockCoreContainer = mock(CoreContainer.class);
+    when(mockCoreContainer.getConfigSetService()).thenReturn(configSetService);
+  }
+
+  @Test
+  public void testSingleFileUploadSuccess() throws Exception {
+    final String configSetName = "singlefile";
+    final String filePath = "solrconfig.xml";
+    final String content = "<config/>";
+    InputStream fileStream = new 
ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+
+    final var api = new UploadConfigSet(mockCoreContainer, null, null);
+    api.uploadConfigSetFile(configSetName, filePath, fileStream);
+
+    // Verify the file was uploaded
+    byte[] uploadedData = 
configSetService.downloadFileFromConfig(configSetName, filePath);
+    assertEquals(content, new String(uploadedData, StandardCharsets.UTF_8));
+  }
+
+  @Test
+  public void testSingleFileWithEmptyPathThrowsBadRequest() {
+    final String configSetName = "emptypath";
+    InputStream fileStream = new 
ByteArrayInputStream("<config/>".getBytes(StandardCharsets.UTF_8));
+
+    final var api = new UploadConfigSet(mockCoreContainer, null, null);
+
+    // Test with empty filePath
+    final var ex =
+        assertThrows(
+            SolrException.class, () -> api.uploadConfigSetFile(configSetName, 
"", fileStream));
+    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
+    assertTrue("Error should mention invalid path", 
ex.getMessage().contains("not valid"));
+  }
+
+  @Test
+  public void testSingleFileUploadWithNestedPath() throws Exception {
+    final String configSetName = "nested";
+    final String filePath = "lang/stopwords_en.txt";
+    final String content = "a\nthe\nis";
+    InputStream fileStream = new 
ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+
+    final var api = new UploadConfigSet(mockCoreContainer, null, null);
+    api.uploadConfigSetFile(configSetName, filePath, fileStream);
+
+    // Verify the file was uploaded with correct path
+    byte[] uploadedData = 
configSetService.downloadFileFromConfig(configSetName, filePath);
+    assertEquals(content, new String(uploadedData, StandardCharsets.UTF_8));
+  }
+}
diff --git 
a/solr/solr-ref-guide/modules/configuration-guide/pages/config-sets.adoc 
b/solr/solr-ref-guide/modules/configuration-guide/pages/config-sets.adoc
index 2b79b985aba..5e22aa3200a 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/config-sets.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/config-sets.adoc
@@ -36,7 +36,7 @@ If you don't, then the `_default` will be copied and given a 
unique name for use
 A configset can be uploaded to ZooKeeper either via the 
xref:configsets-api.adoc[] or more directly via 
xref:deployment-guide:solr-control-script-reference.adoc#upload-a-configuration-set[`bin/solr
 zk upconfig`].
 The Configsets API has some other operations as well, and likewise, so does 
the CLI.
 
-To upload a file to a configset already stored on ZooKeeper, you can use 
xref:deployment-guide:solr-control-script-reference.adoc#copy-between-local-files-and-zookeeper-znodes[`bin/solr
 zk cp`].
+To upload a file to a configset already stored on ZooKeeper, you can use 
xref:deployment-guide:solr-control-script-reference.adoc#copy-between-local-files-and-zookeeper-znodes[`bin/solr
 zk cp`] or the xref:configsets-api.adoc#configsets-upload-file[`API`].
 
 CAUTION: By default, ZooKeeper's file size limit is 1MB.
 If your files are larger than this, you'll need to either 
xref:deployment-guide:zookeeper-ensemble.adoc#increasing-the-file-size-limit[increase
 the ZooKeeper file size limit] or store them xref:libs.adoc[on the filesystem] 
of every node in a cluster.
diff --git 
a/solr/solr-ref-guide/modules/configuration-guide/pages/configsets-api.adoc 
b/solr/solr-ref-guide/modules/configuration-guide/pages/configsets-api.adoc
index 74f2c6751a5..a8cfecfe430 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/configsets-api.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/configsets-api.adoc
@@ -82,27 +82,61 @@ The output will look like:
 }
 ----
 
+[[configsets-download]]
+== Download a Configset
+
+The v2 API allows configsets to be downloaded as a single zipped file.
+This is useful for backing up configsets, sharing them between environments, 
or examining their contents.
+
+The `download` command takes the following parameters:
+
+`name`::
++
+[%autowidth,frame=none]
+|===
+s|Required |Default: none
+|===
++
+The name of the configset to download.
+
+The response will be a ZIP file containing all files from the configset.
+
+To download a configset named "myConfigSet":
+
+[tabs#downloadconfigset]
+======
+V1 API::
++
+====
+The v1 API does not currently support downloading configsets.
+Use the v2 API instead.
+====
+
+V2 API::
++
+====
+With the v2 API, use a `GET` request to `/configsets/__config_name__/files`:
+
+[source,bash]
+----
+curl -X GET -o myConfigSet.zip 
"http://localhost:8983/api/configsets/myConfigSet/files";
+----
+
+The configset will be downloaded as a ZIP file that can be extracted and used 
locally, or re-uploaded to another Solr instance.
+====
+======
+
 [[configsets-upload]]
 == Upload a Configset
 
-Upload a configset, which is sent as a zipped file.
-A single, non-zipped file can also be uploaded with the `filePath` parameter.
+You can upload an entire configset as a ZIP archive, which is useful for 
creating new configsets or replacing all files in an existing configset.
 
 This functionality is enabled by default, but can be disabled via a runtime 
parameter `-Dsolr.configset.upload.enabled=false`.
 Disabling this feature is advisable if you want to expose Solr installation to 
untrusted users (even though you should never do that!).
 
-A configset is uploaded in a "trusted" mode if authentication is enabled and 
the upload operation is performed as an authenticated request.
-Without authentication, a configset is uploaded in an "untrusted" mode.
-Upon creation of a collection using an "untrusted" configset, the following 
functionality will not work:
-
-* The XSLT transformer (`tr` parameter) cannot be used at request processing 
time.
-* If specified in the configset, the ScriptUpdateProcessorFactory will not 
initialize.
-
-If you use any of these parameters or features, you must have enabled security 
features in your Solr installation and you must upload the configset as an 
authenticated user.
-
-Not all file types are supported for use in configSets. Please see 
xref:configuration-guide:config-sets.adoc#forbidden-file-types[] for more 
information.
+Not all file types are supported for use in configsets. Please see 
xref:configuration-guide:config-sets.adoc#forbidden-file-types[forbidden file 
types] for more information.
 
-The `upload` command takes the following parameters:
+The following parameters are supported when uploading a configset:
 
 `name`::
 +
@@ -121,8 +155,6 @@ The configset to be created when the upload is complete.
 |===
 +
 If set to `true`, Solr will overwrite an existing configset with the same name 
(if false, the request will fail).
-If `filePath` is provided, then this option specifies whether the specified 
file within the configset should be overwritten if it already exists.
-Default is `false` when using the v1 API, but `true` when using the v2 API.
 
 `cleanup`::
 +
@@ -131,20 +163,8 @@ Default is `false` when using the v1 API, but `true` when 
using the v2 API.
 |Optional |Default: `false`
 |===
 When overwriting an existing configset (`overwrite=true`), this parameter 
tells Solr to delete the files in ZooKeeper that existed in the old configset 
but not in the one being uploaded.
-This parameter cannot be set to true when `filePath` is used.
 
-`filePath`::
-+
-[%autowidth,frame=none]
-|===
-|Optional |Default: none
-|===
-+
-This parameter allows the uploading of a single, non-zipped file to the given 
path under the configset in ZooKeeper.
-This functionality respects the `overwrite` parameter, so a request will fail 
if the given file path already exists in the configset and overwrite is set to 
`false`.
-The `cleanup` parameter cannot be set to true when `filePath` is used.
-
-If uploading an entire configset, the body of the request should be a zip file 
that contains the configset.
+When uploading an entire configset, the body of the request should be a zip 
file that contains the configset.
 The zip file must be created from within the `conf` directory (i.e., 
`solrconfig.xml` must be the top level entry in the zip file).
 
 Here is an example on how to create the zip file named "myconfig.zip" and 
upload it as a configset named "myConfigSet":
@@ -189,6 +209,33 @@ This behavior can be disabled with the parameter 
`overwrite=false`, in which cas
 ====
 ======
 
+[[configsets-upload-file]]
+== Update a Single File in a Configset
+
+This API lets you modify a specific file in an existing configset.
+This is useful for making targeted changes without re-uploading the entire 
configset.
+
+**URL Path Parameters:**
+
+`configSetName`::
++
+[%autowidth,frame=none]
+|===
+s|Required |Default: none
+|===
++
+The name of the configset containing the file.
+
+`filePath`::
++
+[%autowidth,frame=none]
+|===
+s|Required |Default: none
+|===
++
+The path to the file within the configset, specified as part of the URL path 
(e.g., `solrconfig.xml` or `lang/stopwords_en.txt`).
+For nested paths, use slashes as shown in the examples below.
+
 Here is an example on how to upload a single file to a configset named 
"myConfigSet":
 
 [tabs#uploadsinglefile]
@@ -203,35 +250,120 @@ The filename to upload is provided via the `filePath` 
parameter:
 ----
 curl -X POST --header "Content-Type:application/octet-stream"
     --data-binary 
@solr/server/solr/configsets/sample_techproducts_configs/conf/solrconfig.xml
-    
"http://localhost:8983/solr/admin/configs?action=UPLOAD&name=myConfigSet&filePath=solrconfig.xml&overwrite=true";
+    
"http://localhost:8983/solr/admin/configs?action=UPLOAD&name=myConfigSet&filePath=solrconfig.xml";
 ----
 ====
 
 V2 API::
 +
 ====
-With the v2 API, the name of the configset and file are both provided in the 
URL.
-They can be substituted in `/configsets/__config_name__/__file_name__`.
-The filename may be nested and include `/` characters.
+With the v2 API, the file path is part of the URL path.
+Use a `PUT` request to `/api/configsets/{configSetName}/files/{filePath}`:
+
 
 [source,bash]
 ----
 curl -X PUT --header "Content-Type:application/octet-stream"
     --data-binary 
@solr/server/solr/configsets/sample_techproducts_configs/conf/solrconfig.xml
-    "http://localhost:8983/api/configsets/myConfigSet/solrconfig.xml";
+    "http://localhost:8983/api/configsets/myConfigSet/files/solrconfig.xml";
 ----
 
-With this API, the default behavior is to overwrite the file if it already 
exists within the configset.
-This behavior can be disabled with the parameter `overwrite=false`, in which 
case the request will fail if the file already exists within the configset.
+For nested files within the configset:
+
+[source,bash]
+----
+curl -X PUT --header "Content-Type:application/octet-stream"
+    --data-binary 
@solr/server/solr/configsets/sample_techproducts_configs/conf/lang/stopwords_en.txt
+    
"http://localhost:8983/api/configsets/myConfigSet/files/lang/stopwords_en.txt";
+----
 ====
 ======
 
+[[configsets-get-file]]
+== Get a Single File from a Configset
+
+This API retrieves the raw contents of a single file from an existing 
configset.
+This is useful for inspecting individual configuration files without 
downloading the entire configset.
+
+This endpoint mirrors the upload API structure, with the file path specified 
as part of the URL path.
+The response contains the raw bytes of the file, supporting both text and 
binary files.
+
+**URL Path Parameters:**
+
+`configSetName`::
++
+[%autowidth,frame=none]
+|===
+s|Required |Default: none
+|===
++
+The name of the configset containing the file.
+
+`filePath`::
++
+[%autowidth,frame=none]
+|===
+s|Required |Default: none
+|===
++
+The path to the file within the configset, specified as part of the URL path 
(e.g., `solrconfig.xml` or `lang/stopwords_en.txt`).
+For nested paths, use slashes as shown in the examples below.
+
+The response is the raw file content as `application/octet-stream`.
+This format supports both text files (XML, properties, etc.) and binary files 
(images, class files, etc.).
+
+To retrieve the `solrconfig.xml` file from a configset named "myConfigSet":
+
+[tabs#getfileconfigset]
+======
+V1 API::
++
+====
+The v1 API does not currently support retrieving individual configset files.
+Use the v2 API instead.
+====
+
+V2 API::
++
+====
+With the v2 API, the file path is part of the URL path.
+Use a `GET` request to `/api/configsets/{configSetName}/files/{filePath}`:
+
+[source,bash]
+----
+curl -X GET 
"http://localhost:8983/api/configsets/myConfigSet/files/solrconfig.xml";
+----
+
+For nested files within the configset:
+
+[source,bash]
+----
+curl -X GET 
"http://localhost:8983/api/configsets/myConfigSet/files/lang/stopwords_en.txt";
+----
+====
+======
+
+*Example Output*
+
+The response is the raw file content. For a text file like `solrconfig.xml`:
+
+[source,xml]
+----
+<?xml version="1.0" encoding="UTF-8" ?>
+<config>
+  ...
+</config>
+----
+
+For binary files, the raw bytes are returned directly, preserving the binary 
content.
+
+
 [[configsets-create]]
 == Create a Configset
 
-The `create` command creates a new configset based on a configset that has 
been previously uploaded.
+The `create` command creates a new configset based on a configset that has 
been previously been created.
 
-If you have not yet uploaded any configsets, see the <<Upload a Configset>> 
command above.
+If you have not yet created any configsets, see the <<Upload a Configset>> 
command above.
 
 The following parameters are supported when creating a configset.
 
@@ -347,7 +479,7 @@ The name of the configset to delete is provided as a path 
parameter:
 
 [source,bash]
 ----
-curl -X DELETE http://localhost:8983/api/configsets/myConfigSet?omitHeader=true
+curl -X DELETE http://localhost:8983/api/configsets/myConfigSet
 ----
 ====
 ======
diff --git 
a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc 
b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc
index 4954860be11..fe6217dd9dd 100644
--- 
a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc
+++ 
b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc
@@ -332,6 +332,13 @@ Older segments will continue to be readable.
 WARNING: After upgrading to Solr 10.1, downgrading to an earlier Solr 10.0.x 
version may fail because the older version does not include the `Lucene104` 
codec needed to read the newly written segments.
 If you require the ability to roll back, back up your indexes before upgrading.
 
+=== API Changes
+
+Solr 10.1 changes the V1 API signature in the ConfigSets API for uploading a 
single file
+Previously the default for `over write` is false when using the v1 API, but 
true when using the v2 API.
+Overwrite makes sense when uploading a complete configset and you want to 
eliminate any remnants configset files from the previous one when you replaced 
it with a new one.  
+However for a single file upload it has no bearing and only existed due to a 
bad V1 API design choice.
+
 === Docker
 
 The `gosu` binary is no longer installed in the Solr Docker image. See 
https://github.com/tianon/gosu[gosu github page] for alternatives, such as 
`runuser`, `setpriv` or `chroot`.

Reply via email to