Skip to content

Commit

Permalink
Handle root as a special file in generic-http repo (#2458)
Browse files Browse the repository at this point in the history
  • Loading branch information
ruhan1 authored Sep 5, 2024
1 parent 6d22b6c commit 9d08a46
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package org.commonjava.indy.content;

import org.apache.commons.codec.digest.DigestUtils;
import org.commonjava.indy.model.core.PathStyle;
import org.commonjava.indy.model.core.StoreKey;
import org.commonjava.indy.model.galley.KeyedLocation;
Expand All @@ -32,11 +31,11 @@
import javax.enterprise.inject.Default;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

import static org.commonjava.indy.content.DownloadManager.ROOT_PATH;
import static org.commonjava.indy.model.core.PathStyle.base64url;
import static org.commonjava.indy.model.core.PathStyle.hashed;

Expand Down Expand Up @@ -97,13 +96,19 @@ public String getPath( final ConcreteResource resource )
PathStyle pathStyle = kl.getAttribute(LocationUtils.PATH_STYLE, PathStyle.class);
if ( hashed == pathStyle || base64url == pathStyle)
{
path = defaultPathGenerator.getStyledPath( path, pathStyle );
final String rawPath = path;
path = defaultPathGenerator.getStyledPath( rawPath, pathStyle );
// Add special handling for generic-http stores' root by removing the trailing '/' from the hashed path
if ( ROOT_PATH.equals( rawPath ) && path.endsWith( ROOT_PATH ) )
{
path = path.substring( 0, path.length() - 1 );
}
logger.trace( "Get styled path: {}, rawPath: {}", path, rawPath );
}
else
{
AtomicReference<String> pathref = new AtomicReference<>( path );
pathCalculators.forEach( c -> pathref.set( c.calculateStoragePath( key, pathref.get() ) ) );

path = pathref.get();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.commonjava.indy.bind.jaxrs.IndyDeployment;
import org.commonjava.indy.bind.jaxrs.util.REST;
import org.commonjava.maven.galley.event.EventMetadata;
import org.slf4j.Logger;
Expand All @@ -42,17 +41,16 @@
import javax.ws.rs.core.UriInfo;

import static org.commonjava.indy.IndyContentConstants.CHECK_CACHE_ONLY;
import static org.commonjava.indy.content.DownloadManager.ROOT_PATH;
import static org.commonjava.indy.model.core.GenericPackageTypeDescriptor.GENERIC_CONTENT_REST_BASE_PATH;
import static org.commonjava.indy.model.core.GenericPackageTypeDescriptor.GENERIC_PKG_KEY;
import static org.commonjava.indy.pkg.maven.model.MavenPackageTypeDescriptor.MAVEN_PKG_KEY;

@Api( value = "Maven Content Access and Storage",
description = "Handles retrieval and management of Maven artifact content. This is the main point of access for Maven/Gradle users." )
@Api( value = "generic-http Content Access and Storage" )
@Path( "/api/content/generic-http/{type: (hosted|group|remote)}/{name}" )
@ApplicationScoped
@REST
public class GenericContentAccessResource
implements PackageContentAccessResource
implements PackageContentAccessResource
{

private final Logger logger = LoggerFactory.getLogger( getClass() );
Expand All @@ -71,13 +69,13 @@ public GenericContentAccessResource( final ContentAccessHandler handler )

@Override
@ApiOperation( "Store content under the given artifact store (type/name) and path." )
@ApiResponses( { @ApiResponse( code = 201, message = "Content was stored successfully" ), @ApiResponse( code = 400,
message = "No appropriate storage location was found in the specified store (this store, or a member if a group is specified)." ) } )
@ApiResponses( { @ApiResponse( code = 201, message = "Content was stored successfully" ),
@ApiResponse( code = 400, message = "No appropriate storage location found in the specified store." ) } )
@PUT
@Path( "/{path: (.+)?}" )
public Response doCreate(
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" )
String type, final @ApiParam( required = true ) @PathParam( "name" ) String name,
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" ) String type,
final @ApiParam( required = true ) @PathParam( "name" ) String name,
final @PathParam( "path" ) String path, final @Context UriInfo uriInfo,
final @Context HttpServletRequest request )
{
Expand All @@ -89,10 +87,21 @@ public Response doCreate(
.build( GENERIC_PKG_KEY, type, name ) );
}

@ApiOperation( "Store '/' in the given artifact store by handling the '/' as special filepath" )
@PUT
@Path( "/" )
public Response doCreate(
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" ) String type,
final @ApiParam( required = true ) @PathParam( "name" ) String name, final @Context UriInfo uriInfo,
final @Context HttpServletRequest request )
{
return doCreate( type, name, ROOT_PATH, uriInfo, request );
}

@Override
@ApiOperation( "Delete content under the given store (type/name) and path." )
@ApiResponses( { @ApiResponse( code = 404, message = "Content is not available" ),
@ApiResponse( code = 204, message = "Content was deleted successfully" ) } )
@ApiResponse( code = 204, message = "Content was deleted successfully" ) } )
@DELETE
@Path( "/{path: (.*)}" )
public Response doDelete(
Expand All @@ -105,55 +114,60 @@ public Response doDelete(
}

@Override
@ApiOperation( "Store content under the given store (type/name) and path." )
@ApiResponses( { @ApiResponse( code = 404, message = "Content is not available" ), @ApiResponse( code = 200,
message = "Header metadata for content (or rendered listing when path ends with '/index.html' or '/'" ), } )
@ApiOperation( "Check content under the given store and path." )
@ApiResponses( { @ApiResponse( code = 404, message = "Content is not available" ),
@ApiResponse( code = 200, message = "Get header metadata for content" ), } )
@HEAD
@Path( "/{path: (.*)}" )
public Response doHead(
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" )
String type, final @ApiParam( required = true ) @PathParam( "name" ) String name,
final @PathParam( "path" ) String path, @QueryParam( CHECK_CACHE_ONLY ) final Boolean cacheOnly,
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" ) String type,
final @ApiParam( required = true ) @PathParam( "name" ) String name,
final @PathParam( "path" ) String path, final @QueryParam( CHECK_CACHE_ONLY ) Boolean cacheOnly,
@Context final UriInfo uriInfo, @Context final HttpServletRequest request )
{
final String baseUri = uriInfo.getBaseUriBuilder().path( GENERIC_CONTENT_REST_BASE_PATH ).build().toString();
return handler.doHead( GENERIC_PKG_KEY, type, name, path, cacheOnly, baseUri, request,
new EventMetadata() );
return handler.doHead( GENERIC_PKG_KEY, type, name, path, cacheOnly, baseUri, request, new EventMetadata(),
null, false );
}

@ApiOperation( "Check '/' in the given artifact store by handling the '/' as special filepath" )
@HEAD
@Path( "/" )
public Response doHead(
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" ) String type,
final @ApiParam( required = true ) @PathParam( "name" ) String name,
final @QueryParam( CHECK_CACHE_ONLY ) Boolean cacheOnly, @Context final UriInfo uriInfo,
@Context final HttpServletRequest request )
{
return doHead( type, name, ROOT_PATH, cacheOnly, uriInfo, request );
}

@Override
@ApiOperation( "Retrieve Maven artifact content under the given artifact store (type/name) and path." )
@ApiResponses( { @ApiResponse( code = 404, message = "Content is not available" ),
@ApiResponse( code = 200, response = String.class,
message = "Rendered content listing (when path ends with '/index.html' or '/')" ),
@ApiResponse( code = 200, response = StreamingOutput.class, message = "Content stream" ), } )
@ApiResponse( code = 200, response = StreamingOutput.class, message = "Content stream" ), } )
@GET
@Path( "/{path: (.*)}" )
public Response doGet(
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" )
String type, final @ApiParam( required = true ) @PathParam( "name" ) String name,
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" ) String type,
final @ApiParam( required = true ) @PathParam( "name" ) String name,
final @PathParam( "path" ) String path, @Context final UriInfo uriInfo,
@Context final HttpServletRequest request )
{
final String baseUri = uriInfo.getBaseUriBuilder().path( GENERIC_CONTENT_REST_BASE_PATH ).build().toString();

return handler.doGet( GENERIC_PKG_KEY, type, name, path, baseUri, request, new EventMetadata() );
return handler.doGet( GENERIC_PKG_KEY, type, name, path, baseUri, request, new EventMetadata(), null, false );
}

@Override
@ApiOperation( "Retrieve root listing under the given artifact store (type/name)." )
@ApiResponses( { @ApiResponse( code = 200, response = String.class, message = "Rendered root content listing" ),
@ApiResponse( code = 200, response = StreamingOutput.class, message = "Content stream" ), } )
@ApiOperation( "Retrieve '/' in the given artifact store by handling the '/' as special filepath" )
@GET
@Path( "/" )
public Response doGet(
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" )
String type, final @ApiParam( required = true ) @PathParam( "name" ) String name,
@Context final UriInfo uriInfo, @Context final HttpServletRequest request )
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" ) String type,
final @ApiParam( required = true ) @PathParam( "name" ) String name, @Context final UriInfo uriInfo,
@Context final HttpServletRequest request )
{
final String baseUri = uriInfo.getBaseUriBuilder().path( GENERIC_CONTENT_REST_BASE_PATH ).build().toString();

return handler.doGet( GENERIC_PKG_KEY, type, name, "", baseUri, request, new EventMetadata() );
return doGet( type, name, ROOT_PATH, uriInfo, request );
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Copyright (C) 2011-2023 Red Hat, Inc. (https://github.com/Commonjava/indy)
*
* Licensed 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.commonjava.indy.ftest.core.content;

import org.commonjava.indy.ftest.core.AbstractContentManagementTest;
import org.commonjava.indy.model.core.HostedRepository;
import org.commonjava.indy.model.core.PathStyle;
import org.commonjava.indy.model.core.RemoteRepository;
import org.junit.Test;

import java.io.ByteArrayInputStream;

import static org.commonjava.indy.pkg.PackageTypeConstants.PKG_TYPE_GENERIC_HTTP;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;

/**
* Root '/' is handled as a special file in generic-http repo. Some build downloads from root of remote site
* and store the file by hashing the '/' to a special path.
*
* This test stores the '/' file and retrieves it. See also {@link org.commonjava.indy.content.IndyPathGenerator}
* and {@link org.commonjava.indy.core.bind.jaxrs.GenericContentAccessResource}
*/
public class StoreFileAndVerifyRootFileInGenericRepoTest
extends AbstractContentManagementTest
{
@Override
protected boolean createStandardTestStructures()
{
return false;
}

private final String expected = "This is a test: " + System.nanoTime();

private final String rootPath = "/";

@Test
public void getRootFileAndVerifyOnRemote() throws Exception
{
// CASE 1: store and retrieve on remote repo by the "root" path.

final String remoteUrl = server.formatUrl( rootPath );
server.expect( remoteUrl, 200, expected );

// Create remote repo
RemoteRepository remote1 = new RemoteRepository( PKG_TYPE_GENERIC_HTTP, "repo1", remoteUrl );
remote1.setPathStyle( PathStyle.hashed );
remote1 = client.stores()
.create( remote1, "add generic-http remote repo with hashed path-style",
RemoteRepository.class );

// Get and verify
assertThat( client.content().exists( remote1.getKey(), rootPath ), equalTo( true ) );
assertContent( remote1, rootPath, expected );
}

@Test
public void storeRootFileAndVerifyOnHosted() throws Exception
{
// CASE 2: store and retrieve on hosted repo by the "root" path.

HostedRepository hosted1 = new HostedRepository( PKG_TYPE_GENERIC_HTTP, STORE );
hosted1.setPathStyle( PathStyle.hashed );
hosted1 = this.client.stores()
.create( hosted1, "add generic-http hosted repo with hashed path-style",
HostedRepository.class );

// Store and verify
client.content().store( hosted1.getKey(), rootPath, new ByteArrayInputStream( expected.getBytes() ) );

assertThat( client.content().exists( hosted1.getKey(), rootPath ), equalTo( true ) );
assertContent( hosted1, rootPath, expected );
}
}

0 comments on commit 9d08a46

Please sign in to comment.