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

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


The following commit(s) were added to refs/heads/branch_9x by this push:
     new c5ce8dd3437 SOLR-17351: Decompose filestore "get file" API
c5ce8dd3437 is described below

commit c5ce8dd3437210c6866bc6b197afb998047cc3b2
Author: Jason Gerlowski <[email protected]>
AuthorDate: Mon Feb 17 13:16:54 2025 -0500

    SOLR-17351: Decompose filestore "get file" API
    
    This commit splits up the "get file" endpoint into a number of different
    APIs. Specifically:
    
      - metadata-fetching has been moved out to the endpoint,
        GET/api/cluster/filestore/metadata/some/path.txt
      - Filestore commands such as pushing/pulling files are now
        available at: POST /api/cluster/filestore/commands
    
    These divisions allow us to generate SolrRequest/SolrResponse classes
    representing these APIs, meaning that SolrJ users no longer need to use
    GenericSolrRequest/GenericSolrResponse.
    
    (As a 9.x backport this commit retains the original form of these APIs
    to retain backwards compatibility, but this support should be removed in
    10.0)
---
 solr/CHANGES.txt                                   |   6 +
 .../client/api/endpoint/ClusterFileStoreApis.java  | 108 ++++++++++++-
 .../client/api/endpoint/NodeFileStoreApis.java     |  12 +-
 .../model/FileStoreDirectoryListingResponse.java   |   4 -
 .../java/org/apache/solr/core/CoreContainer.java   |   1 +
 .../apache/solr/filestore/ClusterFileStore.java    | 178 ++++++++++++++++++++-
 .../org/apache/solr/filestore/NodeFileStore.java   | 166 +++++--------------
 .../pages/package-manager-internals.adoc           |  15 +-
 .../solr/client/solrj/InputStreamResponse.java     |   5 +-
 .../apache/solr/client/solrj/util/ClientUtils.java |   2 +
 .../solrj/src/resources/java-template/api.mustache |  11 ++
 11 files changed, 356 insertions(+), 152 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 9c53bd1effe..2f238ea0275 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -25,6 +25,12 @@ Improvements
   other v2 APIs.  SolrJ now offers (experimental) SolrRequest implementations 
for all v2 configset APIs in
   `org.apache.solr.client.solrj.request.ConfigsetsApi`. (Jason Gerlowski)
 
+* SOLR-17351: Solr's filestore "get-file" API has been decomposed into several 
separate endpoints.  Traditional file-fetching is now available at
+  `GET /api/cluster/filestore/files/some/path.txt` and metadata fetching (and 
directory listing) is now available at
+  `GET /api/cluster/filestore/metadata/some/path.txt`. SolrJ now offers 
request and response bindings for these APIs in
+  `org.apache.solr.client.solrj.request.FileStoreApi`. The older form of this 
endpoint (`GET /api/node/files`) is deprecated and is slated to
+  be removed. (Jason Gerlowski)
+
 Optimizations
 ---------------------
 * SOLR-17578: Remove ZkController internal core supplier, for slightly faster 
reconnection after Zookeeper session loss. (Pierre Salagnac)
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/endpoint/ClusterFileStoreApis.java
 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/ClusterFileStoreApis.java
index 11ee4e1e26d..8dbc705e7ab 100644
--- 
a/solr/api/src/java/org/apache/solr/client/api/endpoint/ClusterFileStoreApis.java
+++ 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/ClusterFileStoreApis.java
@@ -17,6 +17,8 @@
 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.OMIT_FROM_CODEGEN_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.Parameter;
@@ -24,23 +26,26 @@ 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;
+import jakarta.ws.rs.POST;
 import jakarta.ws.rs.PUT;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
 import jakarta.ws.rs.QueryParam;
 import java.io.InputStream;
 import java.util.List;
+import org.apache.solr.client.api.model.FileStoreDirectoryListingResponse;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.client.api.model.UploadToFileStoreResponse;
 
 @Path("/cluster")
 public interface ClusterFileStoreApis {
-  // TODO Better understand the purpose of the 'sig' parameter and improve 
docs here.
+
   @PUT
   @Operation(
       summary = "Upload a file to the filestore.",
       tags = {"file-store"})
-  @Path("/files{filePath:.+}")
+  @Path("/filestore/files{filePath:.+}")
   UploadToFileStoreResponse uploadFile(
       @Parameter(description = "File store path") @PathParam("filePath") 
String filePath,
       @Parameter(description = "Signature(s) for the file being uploaded") 
@QueryParam("sig")
@@ -60,7 +65,7 @@ public interface ClusterFileStoreApis {
   @Operation(
       summary = "Delete a file or directory from the filestore.",
       tags = {"file-store"})
-  @Path("/files{path:.+}")
+  @Path("/filestore/files{path:.+}")
   SolrJerseyResponse deleteFile(
       @Parameter(description = "Path to a file or directory within the 
filestore")
           @PathParam("path")
@@ -70,4 +75,101 @@ public interface ClusterFileStoreApis {
                   "Indicates whether the deletion should only be done on the 
receiving node.  For internal use only")
           @QueryParam("localDelete")
           Boolean localDelete);
+
+  @GET
+  @Operation(
+      summary = "Retrieve metadata about a file or directory in the 
filestore.",
+      tags = {"file-store"})
+  @Path("/filestore/metadata{path:.+}")
+  FileStoreDirectoryListingResponse getMetadata(
+      @Parameter(description = "Path to a file or directory within the 
filestore")
+          @PathParam("path")
+          String path);
+
+  @GET
+  @Operation(
+      summary = "Retrieve raw contents of a file in the filestore.",
+      tags = {"file-store"},
+      extensions = {
+        @Extension(properties = {@ExtensionProperty(name = 
RAW_OUTPUT_PROPERTY, value = "true")})
+      })
+  @Path("/filestore/files{filePath:.+}")
+  SolrJerseyResponse getFile(
+      @Parameter(description = "Path to a file or directory within the 
filestore")
+          @PathParam("filePath")
+          String path);
+
+  @POST
+  @Operation(
+      summary = "Fetches a filestore entry from other nodes in the cluster.",
+      tags = {"file-store"})
+  @Path("/filestore/commands/fetch{path:.+}")
+  SolrJerseyResponse fetchFile(
+      @Parameter(description = "Path to a file or directory within the 
filestore")
+          @PathParam("path")
+          String path,
+      @Parameter(description = "An optional Solr node name to fetch the file 
from")
+          @QueryParam("getFrom")
+          String getFrom);
+
+  @POST
+  @Operation(
+      summary = "Syncs a file by pushing it to other nodes in the cluster.",
+      tags = {"file-store"})
+  @Path("/filestore/commands/sync{path:.+}")
+  SolrJerseyResponse syncFile(
+      @Parameter(description = "Path to a file or directory within the 
filestore")
+          @PathParam("path")
+          String path);
+
+  ////////////////////////
+  // Deprecated API bindings for 'upload' and 'delete' APIs without the 
'/filestore' path segment.
+  // cf. 'uploadFile' and 'deleteFile' bindings above. Remove in 10.0
+  @PUT
+  @Operation(
+      summary = "Upload a file to the filestore.  Deprecated: Use 'uploadFile' 
instead",
+      tags = {"file-store"},
+      deprecated = true,
+      extensions = {
+        @Extension(
+            properties = {@ExtensionProperty(name = 
OMIT_FROM_CODEGEN_PROPERTY, value = "true")})
+      })
+  @Path("/files{filePath:.+}")
+  @Deprecated
+  UploadToFileStoreResponse uploadFileDeprecated(
+      @Parameter(description = "File store path") @PathParam("filePath") 
String filePath,
+      @Parameter(description = "Signature(s) for the file being uploaded") 
@QueryParam("sig")
+          List<String> sig,
+      @Parameter(description = "File content to be stored in the filestore")
+          @RequestBody(
+              required = true,
+              extensions = {
+                @Extension(
+                    properties = {
+                      @ExtensionProperty(name = GENERIC_ENTITY_PROPERTY, value 
= "true")
+                    })
+              })
+          InputStream requestBody);
+
+  @DELETE
+  @Operation(
+      summary =
+          "Delete a file or directory from the filestore. Deprecated: Use 
'deleteFile' instead",
+      tags = {"file-store"},
+      deprecated = true,
+      extensions = {
+        @Extension(
+            properties = {@ExtensionProperty(name = 
OMIT_FROM_CODEGEN_PROPERTY, value = "true")})
+      })
+  @Path("/files{path:.+}")
+  @Deprecated
+  SolrJerseyResponse deleteFileDeprecated(
+      @Parameter(description = "Path to a file or directory within the 
filestore")
+          @PathParam("path")
+          String path,
+      @Parameter(
+              description =
+                  "Indicates whether the deletion should only be done on the 
receiving node.  For internal use only")
+          @QueryParam("localDelete")
+          Boolean localDelete);
 }
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/endpoint/NodeFileStoreApis.java 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/NodeFileStoreApis.java
index 15f4a73cfb1..5627c385551 100644
--- 
a/solr/api/src/java/org/apache/solr/client/api/endpoint/NodeFileStoreApis.java
+++ 
b/solr/api/src/java/org/apache/solr/client/api/endpoint/NodeFileStoreApis.java
@@ -33,22 +33,28 @@ import org.apache.solr.client.api.model.SolrJerseyResponse;
  */
 @Path("/node")
 public interface NodeFileStoreApis {
+
+  /**
+   * @deprecated use {@link ClusterFileStoreApis} methods/APIs instead.
+   */
   @GET
   @Operation(
       summary = "Retrieve file contents or metadata from the filestore.",
       tags = {"file-store"},
+      deprecated = true,
       // The response of this v2 API is highly variable based on the 
parameters specified.  It can
       // return raw (potentially binary) file data, a JSON-ified 
representation of that file data,
       // metadata regarding one or multiple file store entries, etc.  This 
variability can be
       // handled on the Jersey server side, but would be prohibitively 
difficult to accommodate in
-      // our code-generation templates.  Ideally, cosmetic improvements (e.g. 
splitting it up into
-      // multiple endpoints) will make this unnecessary in the future.  But 
for now, the extension
-      // property below ensures that this endpoint is ignored entirely when 
doing code generation.
+      // our code-generation templates.  It has been split up to be more 
manageable in 10.0, but the
+      // extension property below is required in the interim to ensure that 
the endpoint is ignored
+      // entirely when doing code generation.
       extensions = {
         @Extension(
             properties = {@ExtensionProperty(name = 
OMIT_FROM_CODEGEN_PROPERTY, value = "true")})
       })
   @Path("/files{path:.+}")
+  @Deprecated
   SolrJerseyResponse getFile(
       @Parameter(description = "Path to a file or directory within the 
filestore")
           @PathParam("path")
diff --git 
a/solr/api/src/java/org/apache/solr/client/api/model/FileStoreDirectoryListingResponse.java
 
b/solr/api/src/java/org/apache/solr/client/api/model/FileStoreDirectoryListingResponse.java
index bcbc5f1f728..79e002b6cee 100644
--- 
a/solr/api/src/java/org/apache/solr/client/api/model/FileStoreDirectoryListingResponse.java
+++ 
b/solr/api/src/java/org/apache/solr/client/api/model/FileStoreDirectoryListingResponse.java
@@ -19,10 +19,6 @@ package org.apache.solr.client.api.model;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import java.util.Map;
 
-/**
- * One of several possible responses from {@link
- * org.apache.solr.client.api.endpoint.NodeFileStoreApis#getFile(String, 
Boolean, String, Boolean)}
- */
 public class FileStoreDirectoryListingResponse extends SolrJerseyResponse {
   @JsonProperty public Map<String, Object> files;
 }
diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java 
b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index b9f441801b2..1f2767302f4 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -1181,6 +1181,7 @@ public class CoreContainer {
                 protected void configure() {
                   bindFactory(new 
InjectionFactories.SingletonFactory<>(fileStore))
                       .to(DistribFileStore.class)
+                      .to(FileStore.class)
                       .in(Singleton.class);
                 }
               })
diff --git a/solr/core/src/java/org/apache/solr/filestore/ClusterFileStore.java 
b/solr/core/src/java/org/apache/solr/filestore/ClusterFileStore.java
index db422baf6bf..69df495453f 100644
--- a/solr/core/src/java/org/apache/solr/filestore/ClusterFileStore.java
+++ b/solr/core/src/java/org/apache/solr/filestore/ClusterFileStore.java
@@ -18,23 +18,33 @@
 package org.apache.solr.filestore;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.solr.handler.admin.api.ReplicationAPIBase.FILE_STREAM;
+import static org.apache.solr.response.RawResponseWriter.CONTENT;
 
 import jakarta.inject.Inject;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.invoke.MethodHandles;
 import java.nio.ByteBuffer;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.solr.api.JerseyResource;
 import org.apache.solr.client.api.endpoint.ClusterFileStoreApis;
+import org.apache.solr.client.api.model.FileStoreDirectoryListingResponse;
+import org.apache.solr.client.api.model.FileStoreEntryMetadata;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.client.api.model.UploadToFileStoreResponse;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrCore;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.pkg.PackageAPI;
 import org.apache.solr.request.SolrQueryRequest;
@@ -61,7 +71,7 @@ public class ClusterFileStore extends JerseyResource 
implements ClusterFileStore
   @Inject
   public ClusterFileStore(
       CoreContainer coreContainer,
-      DistribFileStore fileStore,
+      FileStore fileStore,
       SolrQueryRequest req,
       SolrQueryResponse rsp) {
     this.coreContainer = coreContainer;
@@ -141,6 +151,115 @@ public class ClusterFileStore extends JerseyResource 
implements ClusterFileStore
     return response;
   }
 
+  @Override
+  @PermissionName(PermissionNameProvider.Name.FILESTORE_READ_PERM)
+  public SolrJerseyResponse getFile(String path) {
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+
+    final var type = fileStore.getType(path, false);
+    if (type == FileStore.FileType.NOFILE) {
+      throw new SolrException(
+          SolrException.ErrorCode.NOT_FOUND,
+          "Requested path [" + path + "] not found in filestore");
+    } else if (type == FileStore.FileType.DIRECTORY) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "Requested path [" + path + "] is a directory and has no returnable 
contents");
+    }
+
+    attachFileToResponse(path, fileStore, req, rsp);
+    return response;
+  }
+
+  @Override
+  @PermissionName(PermissionNameProvider.Name.FILESTORE_READ_PERM)
+  public FileStoreDirectoryListingResponse getMetadata(String path) {
+    if (path == null) {
+      path = "";
+    }
+    FileStore.FileType type = fileStore.getType(path, false);
+    return getMetadata(type, path, fileStore);
+  }
+
+  public static void attachFileToResponse(
+      String path, FileStore fileStore, SolrQueryRequest req, 
SolrQueryResponse rsp) {
+    ModifiableSolrParams solrParams = new ModifiableSolrParams();
+    solrParams.add(CommonParams.WT, FILE_STREAM);
+    req.setParams(SolrParams.wrapDefaults(solrParams, req.getParams()));
+    rsp.add(
+        CONTENT,
+        (SolrCore.RawWriter)
+            os ->
+                fileStore.get(
+                    path,
+                    it -> {
+                      try {
+                        InputStream inputStream = it.getInputStream();
+                        if (inputStream != null) {
+                          inputStream.transferTo(os);
+                        }
+                      } catch (IOException e) {
+                        throw new SolrException(
+                            SolrException.ErrorCode.SERVER_ERROR, "Error 
reading file " + path);
+                      }
+                    },
+                    false));
+  }
+
+  @SuppressWarnings("fallthrough")
+  public static FileStoreDirectoryListingResponse getMetadata(
+      FileStore.FileType type, String path, FileStore fileStore) {
+    final var dirListingResponse = new FileStoreDirectoryListingResponse();
+    if (path == null) {
+      path = "";
+    }
+
+    switch (type) {
+      case NOFILE:
+        dirListingResponse.files = Collections.singletonMap(path, null);
+        break;
+      case METADATA:
+      case FILE:
+        int idx = path.lastIndexOf('/');
+        String fileName = path.substring(idx + 1);
+        String parentPath = path.substring(0, path.lastIndexOf('/'));
+        List<FileStore.FileDetails> l = fileStore.list(parentPath, s -> 
s.equals(fileName));
+
+        dirListingResponse.files =
+            Collections.singletonMap(path, l.isEmpty() ? null : 
convertToResponse(l.get(0)));
+        break;
+      case DIRECTORY:
+        final var directoryContents =
+            fileStore.list(path, null).stream()
+                .map(details -> convertToResponse(details))
+                .collect(Collectors.toList());
+        dirListingResponse.files = Collections.singletonMap(path, 
directoryContents);
+        break;
+    }
+
+    return dirListingResponse;
+  }
+
+  // TODO Modify the filestore implementation itself to return this object, so 
conversion isn't
+  // needed.
+  private static FileStoreEntryMetadata 
convertToResponse(FileStore.FileDetails details) {
+    final var entryMetadata = new FileStoreEntryMetadata();
+
+    entryMetadata.name = details.getSimpleName();
+    if (details.isDir()) {
+      entryMetadata.dir = true;
+      return entryMetadata;
+    }
+
+    entryMetadata.size = details.size();
+    entryMetadata.timestamp = details.getTimeStamp();
+    if (details.getMetaData() != null) {
+      details.getMetaData().toMap(entryMetadata.unknownProperties());
+    }
+
+    return entryMetadata;
+  }
+
   private void doLocalDelete(String filePath) {
     fileStore.deleteLocal(filePath);
   }
@@ -194,6 +313,63 @@ public class ClusterFileStore extends JerseyResource 
implements ClusterFileStore
     return response;
   }
 
+  @Override
+  @PermissionName(PermissionNameProvider.Name.FILESTORE_WRITE_PERM)
+  public SolrJerseyResponse fetchFile(String path, String getFrom) {
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    if (path == null) {
+      path = "";
+    }
+    pullFileFromNode(coreContainer, fileStore, path, getFrom);
+    return response;
+  }
+
+  @Override
+  @PermissionName(PermissionNameProvider.Name.FILESTORE_WRITE_PERM)
+  public SolrJerseyResponse syncFile(String path) {
+    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    syncToAllNodes(fileStore, path);
+    return response;
+  }
+
+  @Override
+  @PermissionName(PermissionNameProvider.Name.FILESTORE_WRITE_PERM)
+  public UploadToFileStoreResponse uploadFileDeprecated(
+      String filePath, List<String> sig, InputStream requestBody) {
+    return uploadFile(filePath, sig, requestBody);
+  }
+
+  @Override
+  @PermissionName(PermissionNameProvider.Name.FILESTORE_WRITE_PERM)
+  public SolrJerseyResponse deleteFileDeprecated(String path, Boolean 
localDelete) {
+    return deleteFile(path, localDelete);
+  }
+
+  public static void syncToAllNodes(FileStore fileStore, String path) {
+    try {
+      fileStore.syncToAllNodes(path);
+    } catch (IOException e) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error 
getting file ", e);
+    }
+  }
+
+  public static void pullFileFromNode(
+      CoreContainer coreContainer, FileStore fileStore, String path, String 
getFrom) {
+    coreContainer
+        .getUpdateShardHandler()
+        .getUpdateExecutor()
+        .submit(
+            () -> {
+              log.debug("Downloading file {}", path);
+              try {
+                fileStore.fetch(path, getFrom);
+              } catch (Exception e) {
+                log.error("Failed to download file: {}", path, e);
+              }
+              log.info("downloaded file: {}", path);
+            });
+  }
+
   private List<String> readSignatures(List<String> signatures, byte[] buf)
       throws SolrException, IOException {
     if (signatures == null || signatures.isEmpty()) return null;
diff --git a/solr/core/src/java/org/apache/solr/filestore/NodeFileStore.java 
b/solr/core/src/java/org/apache/solr/filestore/NodeFileStore.java
index e6c61419437..babcc0dc551 100644
--- a/solr/core/src/java/org/apache/solr/filestore/NodeFileStore.java
+++ b/solr/core/src/java/org/apache/solr/filestore/NodeFileStore.java
@@ -17,29 +17,19 @@
 package org.apache.solr.filestore;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.apache.solr.handler.admin.api.ReplicationAPIBase.FILE_STREAM;
-import static org.apache.solr.response.RawResponseWriter.CONTENT;
 import static 
org.apache.solr.security.PermissionNameProvider.Name.FILESTORE_READ_PERM;
 
 import jakarta.inject.Inject;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.invoke.MethodHandles;
-import java.util.Collections;
-import java.util.List;
-import java.util.stream.Collectors;
 import org.apache.solr.api.JerseyResource;
 import org.apache.solr.client.api.endpoint.NodeFileStoreApis;
-import org.apache.solr.client.api.model.FileStoreDirectoryListingResponse;
-import org.apache.solr.client.api.model.FileStoreEntryMetadata;
 import org.apache.solr.client.api.model.FileStoreJsonFileResponse;
 import org.apache.solr.client.api.model.SolrJerseyResponse;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.CommonParams;
-import org.apache.solr.common.params.ModifiableSolrParams;
-import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.core.SolrCore;
 import org.apache.solr.jersey.PermissionName;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
@@ -58,7 +48,7 @@ public class NodeFileStore extends JerseyResource implements 
NodeFileStoreApis {
   @Inject
   public NodeFileStore(
       CoreContainer coreContainer,
-      DistribFileStore fileStore,
+      FileStore fileStore,
       SolrQueryRequest req,
       SolrQueryResponse rsp) {
     this.coreContainer = coreContainer;
@@ -67,21 +57,18 @@ public class NodeFileStore extends JerseyResource 
implements NodeFileStoreApis {
     this.fileStore = fileStore;
   }
 
-  // TODO SOLR-17351 - this single "get" operation actually supports several 
different chunks of
-  //  functionality: syncing, directory listing, file-fetching, 
metadata-fetching. We should split
-  //  it up into multiple distinct APIs
+  // Has been replaced by a number of distinct ClusterFileStore endpoints
+  /**
+   * @deprecated use {@link ClusterFileStore} methods instead
+   */
   @Override
   @PermissionName(FILESTORE_READ_PERM)
+  @Deprecated
   public SolrJerseyResponse getFile(String path, Boolean sync, String getFrom, 
Boolean meta) {
-    final var response = instantiateJerseyResponse(SolrJerseyResponse.class);
+    final var clusterFileApi = new ClusterFileStore(coreContainer, fileStore, 
req, rsp);
 
     if (Boolean.TRUE.equals(sync)) {
-      try {
-        fileStore.syncToAllNodes(path);
-        return response;
-      } catch (IOException e) {
-        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error 
getting file ", e);
-      }
+      return clusterFileApi.syncFile(path);
     }
 
     if (path == null) {
@@ -89,122 +76,41 @@ public class NodeFileStore extends JerseyResource 
implements NodeFileStoreApis {
     }
     final var pathCopy = path;
     if (getFrom != null) {
-      coreContainer
-          .getUpdateShardHandler()
-          .getUpdateExecutor()
-          .submit(
-              () -> {
-                log.debug("Downloading file {}", pathCopy);
-                try {
-                  fileStore.fetch(pathCopy, getFrom);
-                } catch (Exception e) {
-                  log.error("Failed to download file: {}", pathCopy, e);
-                }
-                log.info("downloaded file: {}", pathCopy);
-              });
-      return response;
+      return clusterFileApi.fetchFile(path, getFrom);
     }
 
     FileStore.FileType type = fileStore.getType(path, false);
-    if (type == FileStore.FileType.NOFILE) {
-      final var fileMissingResponse =
-          instantiateJerseyResponse(FileStoreDirectoryListingResponse.class);
-      fileMissingResponse.files = Collections.singletonMap(path, null);
-      return fileMissingResponse;
+    if (type == FileStore.FileType.NOFILE
+        || type == FileStore.FileType.DIRECTORY
+        || (Boolean.TRUE.equals(meta) && type == FileStore.FileType.FILE)) {
+      return clusterFileApi.getMetadata(path);
     }
-    if (type == FileStore.FileType.DIRECTORY) {
-      final var directoryListingResponse =
-          instantiateJerseyResponse(FileStoreDirectoryListingResponse.class);
-      final var directoryContents =
-          fileStore.list(path, null).stream()
-              .map(details -> convertToResponse(details))
-              .collect(Collectors.toList());
-      directoryListingResponse.files = Collections.singletonMap(path, 
directoryContents);
-      return directoryListingResponse;
-    }
-    if (Boolean.TRUE.equals(meta)) {
-      if (type == FileStore.FileType.FILE) {
-        int idx = path.lastIndexOf('/');
-        String fileName = path.substring(idx + 1);
-        String parentPath = path.substring(0, path.lastIndexOf('/'));
-        List<FileStore.FileDetails> l = fileStore.list(parentPath, s -> 
s.equals(fileName));
 
-        final var fileMetaResponse =
-            instantiateJerseyResponse(FileStoreDirectoryListingResponse.class);
-        fileMetaResponse.files =
-            Collections.singletonMap(path, l.isEmpty() ? null : 
convertToResponse(l.get(0)));
-        return fileMetaResponse;
-      }
-    } else { // User wants to get the "raw" file
-      // TODO Should we be trying to json-ify otherwise "raw" files in this 
way?  It seems like a
-      // pretty sketchy idea, esp. for code with very little test coverage.  
Consider removing
-      if ("json".equals(req.getParams().get(CommonParams.WT))) {
-        final var jsonResponse = 
instantiateJerseyResponse(FileStoreJsonFileResponse.class);
-        try {
-          fileStore.get(
-              pathCopy,
-              it -> {
-                try {
-                  InputStream inputStream = it.getInputStream();
-                  if (inputStream != null) {
-                    jsonResponse.response = new 
String(inputStream.readAllBytes(), UTF_8);
-                  }
-                } catch (IOException e) {
-                  throw new SolrException(
-                      SolrException.ErrorCode.SERVER_ERROR, "Error reading 
file " + pathCopy);
+    // Support for JSON-ified response removed in 10.0, but here for the 
remainder of 9.x
+    if ("json".equals(req.getParams().get(CommonParams.WT))) {
+      final var jsonResponse = 
instantiateJerseyResponse(FileStoreJsonFileResponse.class);
+      try {
+        fileStore.get(
+            pathCopy,
+            it -> {
+              try {
+                InputStream inputStream = it.getInputStream();
+                if (inputStream != null) {
+                  jsonResponse.response = new 
String(inputStream.readAllBytes(), UTF_8);
                 }
-              },
-              false);
-          return jsonResponse;
-        } catch (IOException e) {
-          throw new SolrException(
-              SolrException.ErrorCode.SERVER_ERROR, "Error getting file from 
path " + path);
-        }
-      } else {
-        ModifiableSolrParams solrParams = new ModifiableSolrParams();
-        solrParams.add(CommonParams.WT, FILE_STREAM);
-        req.setParams(SolrParams.wrapDefaults(solrParams, req.getParams()));
-        rsp.add(
-            CONTENT,
-            (SolrCore.RawWriter)
-                os ->
-                    fileStore.get(
-                        pathCopy,
-                        it -> {
-                          try {
-                            InputStream inputStream = it.getInputStream();
-                            if (inputStream != null) {
-                              inputStream.transferTo(os);
-                            }
-                          } catch (IOException e) {
-                            throw new SolrException(
-                                SolrException.ErrorCode.SERVER_ERROR,
-                                "Error reading file " + pathCopy);
-                          }
-                        },
-                        false));
+              } catch (IOException e) {
+                throw new SolrException(
+                    SolrException.ErrorCode.SERVER_ERROR, "Error reading file 
" + pathCopy);
+              }
+            },
+            false);
+        return jsonResponse;
+      } catch (IOException e) {
+        throw new SolrException(
+            SolrException.ErrorCode.SERVER_ERROR, "Error getting file from 
path " + path);
       }
+    } else {
+      return clusterFileApi.getFile(path);
     }
-    return response;
-  }
-
-  // TODO Modify the filestore implementation itself to return this object, so 
conversion isn't
-  // needed.
-  private FileStoreEntryMetadata convertToResponse(FileStore.FileDetails 
details) {
-    final var entryMetadata = new FileStoreEntryMetadata();
-
-    entryMetadata.name = details.getSimpleName();
-    if (details.isDir()) {
-      entryMetadata.dir = true;
-      return entryMetadata;
-    }
-
-    entryMetadata.size = details.size();
-    entryMetadata.timestamp = details.getTimeStamp();
-    if (details.getMetaData() != null) {
-      details.getMetaData().toMap(entryMetadata.unknownProperties());
-    }
-
-    return entryMetadata;
   }
 }
diff --git 
a/solr/solr-ref-guide/modules/configuration-guide/pages/package-manager-internals.adoc
 
b/solr/solr-ref-guide/modules/configuration-guide/pages/package-manager-internals.adoc
index aedc7166fd2..fa308238a9d 100644
--- 
a/solr/solr-ref-guide/modules/configuration-guide/pages/package-manager-internals.adoc
+++ 
b/solr/solr-ref-guide/modules/configuration-guide/pages/package-manager-internals.adoc
@@ -88,10 +88,9 @@ The metadata file stores the sha512 and signatures of the 
jar files too.
 
 The end points are:
 
-* `PUT /api/cluster/files/{full/path/to/file}` in each node
-* `GET /api/node/files/{full/path/to/file}` to download the file
-* `GET /api/node/files/{full/path/to/file}?meta=true` get the metadata of the 
file
-* `GET /api/node/files/{full/path/to/}` get the list of files in 
`/full/path/to`
+* `PUT /api/cluster/filestore/files/{full/path/to/file}` to upload a new file 
and add it to the file store
+* `GET /api/cluster/filestore/files/{full/path/to/file}` to download a file 
already in the filestore
+* `GET /api/cluster/filestore/metadata/{full/path/to/file}` to get the 
metadata of a particular file, or a list of files available in a directory
 
 === Signing Your Artifacts
 
@@ -115,14 +114,14 @@ openssl dgst -sha1 -sign my_key.pem runtimelibs.jar | 
openssl enc -base64 | sed
 +
 [source, bash]
 ----
-curl --data-binary @runtimelibs.jar -X PUT  
http://localhost:8983/api/cluster/files/mypkg/1.0/myplugins.jar?sig=<signature-of-jar>
+curl --data-binary @runtimelibs.jar -X PUT  
http://localhost:8983/api/cluster/filestore/files/mypkg/1.0/myplugins.jar?sig=<signature-of-jar>
 ----
 
 . Verify your jar upload:
 +
 [source, bash]
 ----
-curl http://localhost:8983/api/node/files/mypkg/1.0?omitHeader=true
+curl 
http://localhost:8983/api/cluster/filestore/metadata/mypkg/1.0?omitHeader=true
 ----
 +
 [source, json]
@@ -328,14 +327,14 @@ curl -o runtimelibs3.jar   -LO 
https://github.com/apache/solr/blob/releases/solr
 
 openssl dgst -sha1 -sign my_key.pem runtimelibs.jar | openssl enc -base64 | 
sed 's/+/%2B/g' | tr -d \\n | sed
 
-curl --data-binary @runtimelibs3.jar -X PUT  
http://localhost:8983/api/cluster/files/mypkg/2.0/myplugins.jar?sig=<signature>
+curl --data-binary @runtimelibs3.jar -X PUT  
http://localhost:8983/api/cluster/filestore/files/mypkg/2.0/myplugins.jar?sig=<signature>
 ----
 
 . Verify it:
 +
 [source, bash]
 ----
-curl http://localhost:8983/api/node/files/mypkg/2.0?omitHeader=true
+curl 
http://localhost:8983/api/cluster/filestore/metadata/mypkg/2.0?omitHeader=true
 ----
 +
 [source, json]
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/InputStreamResponse.java 
b/solr/solrj/src/java/org/apache/solr/client/solrj/InputStreamResponse.java
index b86e76f13ac..a4cde5b5266 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/InputStreamResponse.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/InputStreamResponse.java
@@ -70,8 +70,8 @@ public class InputStreamResponse extends SimpleSolrResponse {
   /**
    * Access the server response as an {@link InputStream}, regardless of the 
HTTP status code
    *
-   * <p>Caller is responsible for consuming and closing the stream, and 
releasing it from the
-   * tracking done by {@link ObjectReleaseTracker}. No validation is done on 
the HTTP status code.
+   * <p>Caller is responsible for consuming and closing the stream. No 
validation is done on the
+   * HTTP status code.
    */
   public InputStream getResponseStream() {
     final NamedList<Object> resp = getResponse();
@@ -124,7 +124,6 @@ public class InputStreamResponse extends SimpleSolrResponse 
{
       inputStream.transferTo(baos);
       return baos.toString(Charset.defaultCharset());
     } finally {
-      ObjectReleaseTracker.release(inputStream);
       IOUtils.closeQuietly(baos);
       IOUtils.closeQuietly(inputStream);
     }
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java 
b/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java
index 37af22f8721..82ecb5c68a6 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java
@@ -115,6 +115,8 @@ public class ClientUtils {
     final var v2Root = SolrRequest.ApiVersion.V2.getApiPrefix();
     if (existingPath.contains(v1Root)) {
       return existingPath.replaceFirst(v1Root, v2Root);
+    } else if (existingPath.endsWith("/api")) {
+      return existingPath;
     } else if (!existingPath.contains(v2Root)) {
       return existingPath + v2Root;
     } else {
diff --git a/solr/solrj/src/resources/java-template/api.mustache 
b/solr/solrj/src/resources/java-template/api.mustache
index f276f3f0a72..0ed28650056 100644
--- a/solr/solrj/src/resources/java-template/api.mustache
+++ b/solr/solrj/src/resources/java-template/api.mustache
@@ -23,10 +23,12 @@ import java.io.OutputStream;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.response.SimpleSolrResponse;
@@ -239,6 +241,15 @@ public class {{classname}} {
               return params;
             }
 
+            @Override
+            public Set<String> getQueryParams() {
+              final var queryParams = new HashSet<String>();
+              {{#queryParams}}
+              queryParams.add("{{baseName}}");
+              {{/queryParams}}
+              return queryParams;
+            }
+
             @Override
             protected {{operationIdCamelCase}}Response 
createResponse(SolrClient client) {
               return new {{operationIdCamelCase}}Response();


Reply via email to