Skip to content

Commit 9d08a46

Browse files
authored
Handle root as a special file in generic-http repo (#2458)
1 parent 6d22b6c commit 9d08a46

File tree

3 files changed

+144
-38
lines changed

3 files changed

+144
-38
lines changed

api/src/main/java/org/commonjava/indy/content/IndyPathGenerator.java

+9-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package org.commonjava.indy.content;
1717

18-
import org.apache.commons.codec.digest.DigestUtils;
1918
import org.commonjava.indy.model.core.PathStyle;
2019
import org.commonjava.indy.model.core.StoreKey;
2120
import org.commonjava.indy.model.galley.KeyedLocation;
@@ -32,11 +31,11 @@
3231
import javax.enterprise.inject.Default;
3332
import javax.enterprise.inject.Instance;
3433
import javax.inject.Inject;
35-
import java.io.File;
3634
import java.util.HashSet;
3735
import java.util.Set;
3836
import java.util.concurrent.atomic.AtomicReference;
3937

38+
import static org.commonjava.indy.content.DownloadManager.ROOT_PATH;
4039
import static org.commonjava.indy.model.core.PathStyle.base64url;
4140
import static org.commonjava.indy.model.core.PathStyle.hashed;
4241

@@ -97,13 +96,19 @@ public String getPath( final ConcreteResource resource )
9796
PathStyle pathStyle = kl.getAttribute(LocationUtils.PATH_STYLE, PathStyle.class);
9897
if ( hashed == pathStyle || base64url == pathStyle)
9998
{
100-
path = defaultPathGenerator.getStyledPath( path, pathStyle );
99+
final String rawPath = path;
100+
path = defaultPathGenerator.getStyledPath( rawPath, pathStyle );
101+
// Add special handling for generic-http stores' root by removing the trailing '/' from the hashed path
102+
if ( ROOT_PATH.equals( rawPath ) && path.endsWith( ROOT_PATH ) )
103+
{
104+
path = path.substring( 0, path.length() - 1 );
105+
}
106+
logger.trace( "Get styled path: {}, rawPath: {}", path, rawPath );
101107
}
102108
else
103109
{
104110
AtomicReference<String> pathref = new AtomicReference<>( path );
105111
pathCalculators.forEach( c -> pathref.set( c.calculateStoragePath( key, pathref.get() ) ) );
106-
107112
path = pathref.get();
108113
}
109114

bindings/jaxrs/src/main/java/org/commonjava/indy/core/bind/jaxrs/GenericContentAccessResource.java

+48-34
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import io.swagger.annotations.ApiParam;
2121
import io.swagger.annotations.ApiResponse;
2222
import io.swagger.annotations.ApiResponses;
23-
import org.commonjava.indy.bind.jaxrs.IndyDeployment;
2423
import org.commonjava.indy.bind.jaxrs.util.REST;
2524
import org.commonjava.maven.galley.event.EventMetadata;
2625
import org.slf4j.Logger;
@@ -42,17 +41,16 @@
4241
import javax.ws.rs.core.UriInfo;
4342

4443
import static org.commonjava.indy.IndyContentConstants.CHECK_CACHE_ONLY;
44+
import static org.commonjava.indy.content.DownloadManager.ROOT_PATH;
4545
import static org.commonjava.indy.model.core.GenericPackageTypeDescriptor.GENERIC_CONTENT_REST_BASE_PATH;
4646
import static org.commonjava.indy.model.core.GenericPackageTypeDescriptor.GENERIC_PKG_KEY;
47-
import static org.commonjava.indy.pkg.maven.model.MavenPackageTypeDescriptor.MAVEN_PKG_KEY;
4847

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

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

7270
@Override
7371
@ApiOperation( "Store content under the given artifact store (type/name) and path." )
74-
@ApiResponses( { @ApiResponse( code = 201, message = "Content was stored successfully" ), @ApiResponse( code = 400,
75-
message = "No appropriate storage location was found in the specified store (this store, or a member if a group is specified)." ) } )
72+
@ApiResponses( { @ApiResponse( code = 201, message = "Content was stored successfully" ),
73+
@ApiResponse( code = 400, message = "No appropriate storage location found in the specified store." ) } )
7674
@PUT
7775
@Path( "/{path: (.+)?}" )
7876
public Response doCreate(
79-
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" )
80-
String type, final @ApiParam( required = true ) @PathParam( "name" ) String name,
77+
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" ) String type,
78+
final @ApiParam( required = true ) @PathParam( "name" ) String name,
8179
final @PathParam( "path" ) String path, final @Context UriInfo uriInfo,
8280
final @Context HttpServletRequest request )
8381
{
@@ -89,10 +87,21 @@ public Response doCreate(
8987
.build( GENERIC_PKG_KEY, type, name ) );
9088
}
9189

90+
@ApiOperation( "Store '/' in the given artifact store by handling the '/' as special filepath" )
91+
@PUT
92+
@Path( "/" )
93+
public Response doCreate(
94+
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" ) String type,
95+
final @ApiParam( required = true ) @PathParam( "name" ) String name, final @Context UriInfo uriInfo,
96+
final @Context HttpServletRequest request )
97+
{
98+
return doCreate( type, name, ROOT_PATH, uriInfo, request );
99+
}
100+
92101
@Override
93102
@ApiOperation( "Delete content under the given store (type/name) and path." )
94103
@ApiResponses( { @ApiResponse( code = 404, message = "Content is not available" ),
95-
@ApiResponse( code = 204, message = "Content was deleted successfully" ) } )
104+
@ApiResponse( code = 204, message = "Content was deleted successfully" ) } )
96105
@DELETE
97106
@Path( "/{path: (.*)}" )
98107
public Response doDelete(
@@ -105,55 +114,60 @@ public Response doDelete(
105114
}
106115

107116
@Override
108-
@ApiOperation( "Store content under the given store (type/name) and path." )
109-
@ApiResponses( { @ApiResponse( code = 404, message = "Content is not available" ), @ApiResponse( code = 200,
110-
message = "Header metadata for content (or rendered listing when path ends with '/index.html' or '/'" ), } )
117+
@ApiOperation( "Check content under the given store and path." )
118+
@ApiResponses( { @ApiResponse( code = 404, message = "Content is not available" ),
119+
@ApiResponse( code = 200, message = "Get header metadata for content" ), } )
111120
@HEAD
112121
@Path( "/{path: (.*)}" )
113122
public Response doHead(
114-
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" )
115-
String type, final @ApiParam( required = true ) @PathParam( "name" ) String name,
116-
final @PathParam( "path" ) String path, @QueryParam( CHECK_CACHE_ONLY ) final Boolean cacheOnly,
123+
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" ) String type,
124+
final @ApiParam( required = true ) @PathParam( "name" ) String name,
125+
final @PathParam( "path" ) String path, final @QueryParam( CHECK_CACHE_ONLY ) Boolean cacheOnly,
117126
@Context final UriInfo uriInfo, @Context final HttpServletRequest request )
118127
{
119128
final String baseUri = uriInfo.getBaseUriBuilder().path( GENERIC_CONTENT_REST_BASE_PATH ).build().toString();
120-
return handler.doHead( GENERIC_PKG_KEY, type, name, path, cacheOnly, baseUri, request,
121-
new EventMetadata() );
129+
return handler.doHead( GENERIC_PKG_KEY, type, name, path, cacheOnly, baseUri, request, new EventMetadata(),
130+
null, false );
131+
}
132+
133+
@ApiOperation( "Check '/' in the given artifact store by handling the '/' as special filepath" )
134+
@HEAD
135+
@Path( "/" )
136+
public Response doHead(
137+
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" ) String type,
138+
final @ApiParam( required = true ) @PathParam( "name" ) String name,
139+
final @QueryParam( CHECK_CACHE_ONLY ) Boolean cacheOnly, @Context final UriInfo uriInfo,
140+
@Context final HttpServletRequest request )
141+
{
142+
return doHead( type, name, ROOT_PATH, cacheOnly, uriInfo, request );
122143
}
123144

124145
@Override
125146
@ApiOperation( "Retrieve Maven artifact content under the given artifact store (type/name) and path." )
126147
@ApiResponses( { @ApiResponse( code = 404, message = "Content is not available" ),
127-
@ApiResponse( code = 200, response = String.class,
128-
message = "Rendered content listing (when path ends with '/index.html' or '/')" ),
129-
@ApiResponse( code = 200, response = StreamingOutput.class, message = "Content stream" ), } )
148+
@ApiResponse( code = 200, response = StreamingOutput.class, message = "Content stream" ), } )
130149
@GET
131150
@Path( "/{path: (.*)}" )
132151
public Response doGet(
133-
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" )
134-
String type, final @ApiParam( required = true ) @PathParam( "name" ) String name,
152+
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" ) String type,
153+
final @ApiParam( required = true ) @PathParam( "name" ) String name,
135154
final @PathParam( "path" ) String path, @Context final UriInfo uriInfo,
136155
@Context final HttpServletRequest request )
137156
{
138157
final String baseUri = uriInfo.getBaseUriBuilder().path( GENERIC_CONTENT_REST_BASE_PATH ).build().toString();
139-
140-
return handler.doGet( GENERIC_PKG_KEY, type, name, path, baseUri, request, new EventMetadata() );
158+
return handler.doGet( GENERIC_PKG_KEY, type, name, path, baseUri, request, new EventMetadata(), null, false );
141159
}
142160

143161
@Override
144-
@ApiOperation( "Retrieve root listing under the given artifact store (type/name)." )
145-
@ApiResponses( { @ApiResponse( code = 200, response = String.class, message = "Rendered root content listing" ),
146-
@ApiResponse( code = 200, response = StreamingOutput.class, message = "Content stream" ), } )
162+
@ApiOperation( "Retrieve '/' in the given artifact store by handling the '/' as special filepath" )
147163
@GET
148164
@Path( "/" )
149165
public Response doGet(
150-
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" )
151-
String type, final @ApiParam( required = true ) @PathParam( "name" ) String name,
152-
@Context final UriInfo uriInfo, @Context final HttpServletRequest request )
166+
final @ApiParam( allowableValues = "hosted,group,remote", required = true ) @PathParam( "type" ) String type,
167+
final @ApiParam( required = true ) @PathParam( "name" ) String name, @Context final UriInfo uriInfo,
168+
@Context final HttpServletRequest request )
153169
{
154-
final String baseUri = uriInfo.getBaseUriBuilder().path( GENERIC_CONTENT_REST_BASE_PATH ).build().toString();
155-
156-
return handler.doGet( GENERIC_PKG_KEY, type, name, "", baseUri, request, new EventMetadata() );
170+
return doGet( type, name, ROOT_PATH, uriInfo, request );
157171
}
158172

159173
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Copyright (C) 2011-2023 Red Hat, Inc. (https://github.com/Commonjava/indy)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.commonjava.indy.ftest.core.content;
17+
18+
import org.commonjava.indy.ftest.core.AbstractContentManagementTest;
19+
import org.commonjava.indy.model.core.HostedRepository;
20+
import org.commonjava.indy.model.core.PathStyle;
21+
import org.commonjava.indy.model.core.RemoteRepository;
22+
import org.junit.Test;
23+
24+
import java.io.ByteArrayInputStream;
25+
26+
import static org.commonjava.indy.pkg.PackageTypeConstants.PKG_TYPE_GENERIC_HTTP;
27+
import static org.hamcrest.CoreMatchers.equalTo;
28+
import static org.hamcrest.MatcherAssert.assertThat;
29+
30+
/**
31+
* Root '/' is handled as a special file in generic-http repo. Some build downloads from root of remote site
32+
* and store the file by hashing the '/' to a special path.
33+
*
34+
* This test stores the '/' file and retrieves it. See also {@link org.commonjava.indy.content.IndyPathGenerator}
35+
* and {@link org.commonjava.indy.core.bind.jaxrs.GenericContentAccessResource}
36+
*/
37+
public class StoreFileAndVerifyRootFileInGenericRepoTest
38+
extends AbstractContentManagementTest
39+
{
40+
@Override
41+
protected boolean createStandardTestStructures()
42+
{
43+
return false;
44+
}
45+
46+
private final String expected = "This is a test: " + System.nanoTime();
47+
48+
private final String rootPath = "/";
49+
50+
@Test
51+
public void getRootFileAndVerifyOnRemote() throws Exception
52+
{
53+
// CASE 1: store and retrieve on remote repo by the "root" path.
54+
55+
final String remoteUrl = server.formatUrl( rootPath );
56+
server.expect( remoteUrl, 200, expected );
57+
58+
// Create remote repo
59+
RemoteRepository remote1 = new RemoteRepository( PKG_TYPE_GENERIC_HTTP, "repo1", remoteUrl );
60+
remote1.setPathStyle( PathStyle.hashed );
61+
remote1 = client.stores()
62+
.create( remote1, "add generic-http remote repo with hashed path-style",
63+
RemoteRepository.class );
64+
65+
// Get and verify
66+
assertThat( client.content().exists( remote1.getKey(), rootPath ), equalTo( true ) );
67+
assertContent( remote1, rootPath, expected );
68+
}
69+
70+
@Test
71+
public void storeRootFileAndVerifyOnHosted() throws Exception
72+
{
73+
// CASE 2: store and retrieve on hosted repo by the "root" path.
74+
75+
HostedRepository hosted1 = new HostedRepository( PKG_TYPE_GENERIC_HTTP, STORE );
76+
hosted1.setPathStyle( PathStyle.hashed );
77+
hosted1 = this.client.stores()
78+
.create( hosted1, "add generic-http hosted repo with hashed path-style",
79+
HostedRepository.class );
80+
81+
// Store and verify
82+
client.content().store( hosted1.getKey(), rootPath, new ByteArrayInputStream( expected.getBytes() ) );
83+
84+
assertThat( client.content().exists( hosted1.getKey(), rootPath ), equalTo( true ) );
85+
assertContent( hosted1, rootPath, expected );
86+
}
87+
}

0 commit comments

Comments
 (0)