package org.carlspring.strongbox.controllers.maven;
import org.carlspring.maven.commons.util.ArtifactUtils;
import org.carlspring.strongbox.client.ArtifactTransportException;
import org.carlspring.strongbox.controllers.BaseArtifactController;
import org.carlspring.strongbox.io.ArtifactInputStream;
import org.carlspring.strongbox.services.ArtifactManagementService;
import org.carlspring.strongbox.storage.ArtifactResolutionException;
import org.carlspring.strongbox.storage.ArtifactStorageException;
import org.carlspring.strongbox.storage.Storage;
import org.carlspring.strongbox.storage.repository.Repository;
import org.carlspring.strongbox.utils.ArtifactControllerHelper;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.JAXBException;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import java.util.regex.Matcher;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.comparator.DirectoryFileComparator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import static org.carlspring.strongbox.utils.ArtifactControllerHelper.handlePartialDownload;
import static org.carlspring.strongbox.utils.ArtifactControllerHelper.isRangedRequest;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.NOT_FOUND;
/**
* REST API for all artifact-related processes.
* <p>
* Thanks to custom URL processing any path variable like '{path:.+}' will be processed as '**'.
*
* @author Alex Oreshkevich
* @see {@linkplain http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-config-path-matching}
*/
@RestController
@RequestMapping(path = MavenArtifactController.ROOT_CONTEXT,
headers = "user-agent=Maven/*")
public class MavenArtifactController
extends BaseArtifactController
{
private static final Logger logger = LoggerFactory.getLogger(MavenArtifactController.class);
// must be the same as @RequestMapping value on the class definition
public final static String ROOT_CONTEXT = "/storages";
@Inject
private ArtifactManagementService mavenArtifactManagementService;
@PreAuthorize("authenticated")
@RequestMapping(value = "greet",
method = RequestMethod.GET)
public ResponseEntity greet()
{
return new ResponseEntity<>("success", HttpStatus.OK);
}
@ApiOperation(value = "Used to deploy an artifact",
position = 0)
@ApiResponses(value = { @ApiResponse(code = 200,
message = "The artifact was deployed successfully."),
@ApiResponse(code = 400,
message = "An error occurred.") })
@PreAuthorize("hasAuthority('ARTIFACTS_DEPLOY')")
@RequestMapping(value = "{storageId}/{repositoryId}/{path:.+}",
method = RequestMethod.PUT)
public ResponseEntity upload(@ApiParam(value = "The storageId",
required = true)
@PathVariable(name = "storageId") String storageId,
@ApiParam(value = "The repositoryId",
required = true)
@PathVariable(name = "repositoryId") String repositoryId,
@PathVariable String path,
HttpServletRequest request)
{
try
{
getArtifactManagementService().store(storageId, repositoryId, path, request.getInputStream());
return ResponseEntity.ok("The artifact was deployed successfully.");
}
catch (Exception e)
{
logger.error(e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(e.getMessage());
}
}
@ApiOperation(value = "Used to retrieve an artifact",
position = 1)
@ApiResponses(value = { @ApiResponse(code = 200,
message = ""),
@ApiResponse(code = 400,
message = "An error occurred.") })
@PreAuthorize("hasAuthority('ARTIFACTS_RESOLVE')")
@RequestMapping(value = { "{storageId}/{repositoryId}/{path:.+}" },
method = RequestMethod.GET)
public void download(@ApiParam(value = "The storageId",
required = true)
@PathVariable String storageId,
@ApiParam(value = "The repositoryId",
required = true)
@PathVariable String repositoryId,
@RequestHeader HttpHeaders httpHeaders,
@PathVariable String path,
HttpServletRequest request,
HttpServletResponse response)
throws Exception
{
logger.debug(" repository = " + repositoryId + "\n\tpath = " + path);
Storage storage = configurationManager.getConfiguration()
.getStorage(storageId);
if (storage == null)
{
logger.error("Unable to find storage by ID " + storageId);
response.sendError(INTERNAL_SERVER_ERROR.value(), "Unable to find storage by ID " + storageId);
return;
}
Repository repository = storage.getRepository(repositoryId);
if (repository == null)
{
logger.error("Unable to find repository by ID " + repositoryId + " for storage " + storageId);
response.sendError(INTERNAL_SERVER_ERROR.value(),
"Unable to find repository by ID " + repositoryId + " for storage " + storageId);
return;
}
if (!repository.isInService())
{
logger.error("Repository is not in service...");
response.setStatus(HttpStatus.SERVICE_UNAVAILABLE.value());
return;
}
if (repository.allowsDirectoryBrowsing() && probeForDirectoryListing(repository, path))
{
try
{
generateDirectoryListing(repository, path, request, response);
}
catch (Exception e)
{
logger.error("Unable to generate directory listing for " +
"/" + storageId + "/" + repositoryId + "/" + path, e);
response.setStatus(INTERNAL_SERVER_ERROR.value());
}
return;
}
ArtifactInputStream is;
try
{
is = (ArtifactInputStream) getArtifactManagementService().resolve(storageId, repositoryId, path);
if (is == null)
{
response.setStatus(NOT_FOUND.value());
return;
}
if (isRangedRequest(httpHeaders))
{
logger.debug("Detecting range request....");
handlePartialDownload(is, httpHeaders, response);
}
copyToResponse(is, response);
}
catch (ArtifactResolutionException | ArtifactTransportException e)
{
logger.info("Unable to find artifact by path " + path, e);
response.setStatus(NOT_FOUND.value());
return;
}
setMediaTypeHeader(path, response);
response.setHeader("Accept-Ranges", "bytes");
ArtifactControllerHelper.setHeadersForChecksums(is, response);
logger.debug("Download succeeded.");
}
private void setMediaTypeHeader(String path,
HttpServletResponse response)
{
// TODO: This is far from optimal and will need to have a content type approach at some point:
if (ArtifactUtils.isChecksum(path))
{
response.setContentType(MediaType.TEXT_PLAIN_VALUE);
}
else if (ArtifactUtils.isMetadata(path))
{
response.setContentType(MediaType.APPLICATION_XML_VALUE);
}
else
{
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
}
}
private boolean probeForDirectoryListing(Repository repository,
String path)
{
String filePath = path.replaceAll("/", Matcher.quoteReplacement(File.separator));
String dir = repository.getBasedir() + File.separator + filePath;
File file = new File(dir);
// Do not allow .index and .trash directories (or any other directory starting with ".") to be browseable.
// NB: Files will still be downloadable.
if (!file.isHidden() && !path.startsWith(".") && !path.contains("/."))
{
if (file.exists() && file.isDirectory())
{
return true;
}
file = new File(dir + File.separator);
return file.exists() && file.isDirectory();
}
else
{
return false;
}
}
private void generateDirectoryListing(Repository repository,
String path,
HttpServletRequest request,
HttpServletResponse response)
{
path = path.replaceAll("/", Matcher.quoteReplacement(File.separator));
if (request == null)
{
throw new RuntimeException("Unable to retrieve HTTP request from execution context");
}
String dir = repository.getBasedir() + File.separator + path;
String requestUri = request.getRequestURI();
File file = new File(dir);
if (file.isDirectory() && !requestUri.endsWith("/"))
{
response.setLocale(new Locale(request.getRequestURI() + "/"));
response.setStatus(HttpStatus.TEMPORARY_REDIRECT.value());
}
try
{
logger.debug(" browsing: " + file.toString());
StringBuilder sb = new StringBuilder();
sb.append("<html>");
sb.append("<head>");
sb.append(
"<style>body{font-family: \"Trebuchet MS\", verdana, lucida, arial, helvetica, sans-serif;} table tr {text-align: left;}</style>");
sb.append("<title>Index of " + request.getRequestURI() + "</title>");
sb.append("</head>");
sb.append("<body>");
sb.append("<h1>Index of " + request.getRequestURI() + "</h1>");
sb.append("<table cellspacing=\"10\">");
sb.append("<tr>");
sb.append("<th>Name</th>");
sb.append("<th>Last modified</th>");
sb.append("<th>Size</th>");
sb.append("</tr>");
sb.append("<tr>");
sb.append("<td colspan=3><a href='..'>..</a></td>");
sb.append("</tr>");
File[] childFiles = file.listFiles();
if (childFiles != null)
{
Arrays.sort(childFiles, DirectoryFileComparator.DIRECTORY_COMPARATOR);
for (File childFile : childFiles)
{
String name = childFile.getName();
if (name.startsWith(".") || childFile.isHidden())
{
continue;
}
String lastModified = new SimpleDateFormat("dd-MM-yyyy HH-mm-ss").format(
new Date(childFile.lastModified()));
sb.append("<tr>");
sb.append("<td><a href='" + URLEncoder.encode(name, "UTF-8") + (childFile.isDirectory() ?
"/" : "") + "'>" + name +
(childFile.isDirectory() ? "/" : "") + "</a></td>");
sb.append("<td>" + lastModified + "</td>");
sb.append("<td>" + FileUtils.byteCountToDisplaySize(childFile.length()) + "</td>");
sb.append("</tr>");
}
}
sb.append("</table>");
sb.append("</body>");
sb.append("</html>");
response.setContentType("text/html;charset=UTF-8");
response.setStatus(HttpStatus.FOUND.value());
response.getWriter()
.write(sb.toString());
response.getWriter()
.flush();
response.getWriter()
.close();
}
catch (Exception e)
{
logger.error(" error accessing requested directory: " + file.getAbsolutePath(), e);
response.setStatus(404);
}
}
@ApiOperation(value = "Copies a path from one repository to another.",
position = 4)
@ApiResponses(value = { @ApiResponse(code = 200,
message = "The path was copied successfully."),
@ApiResponse(code = 400,
message = "Bad request."),
@ApiResponse(code = 404,
message = "The source/destination storageId/repositoryId/path does not exist!") })
@PreAuthorize("hasAuthority('ARTIFACTS_COPY')")
@RequestMapping(produces = MediaType.TEXT_PLAIN_VALUE,
value = "/copy/{path:.+}",
method = RequestMethod.POST)
public ResponseEntity copy(@ApiParam(value = "The source storageId",
required = true)
@RequestParam(name = "srcStorageId") String srcStorageId,
@ApiParam(value = "The source repositoryId",
required = true)
@RequestParam(name = "srcRepositoryId") String srcRepositoryId,
@ApiParam(value = "The destination storageId",
required = true)
@RequestParam(name = "destStorageId") String destStorageId,
@ApiParam(value = "The destination repositoryId",
required = true)
@RequestParam(name = "destRepositoryId") String destRepositoryId,
@PathVariable String path)
throws IOException, JAXBException
{
logger.debug("Copying " + path +
" from " + srcStorageId + ":" + srcRepositoryId +
" to " + destStorageId + ":" + destRepositoryId + "...");
try
{
if (getStorage(srcStorageId) == null)
{
return ResponseEntity.status(NOT_FOUND)
.body("The source storageId does not exist!");
}
if (getStorage(destStorageId) == null)
{
return ResponseEntity.status(NOT_FOUND)
.body("The destination storageId does not exist!");
}
if (getStorage(srcStorageId).getRepository(srcRepositoryId) == null)
{
return ResponseEntity.status(NOT_FOUND)
.body("The source repositoryId does not exist!");
}
if (getStorage(destStorageId).getRepository(destRepositoryId) == null)
{
return ResponseEntity.status(NOT_FOUND)
.body("The destination repositoryId does not exist!");
}
if (getStorage(srcStorageId) != null &&
getStorage(srcStorageId).getRepository(srcRepositoryId) != null &&
!new File(getStorage(srcStorageId).getRepository(srcRepositoryId)
.getBasedir(), path).exists())
{
return ResponseEntity.status(NOT_FOUND)
.body("The source path does not exist!");
}
getArtifactManagementService().copy(srcStorageId, srcRepositoryId, path, destStorageId, destRepositoryId);
}
catch (ArtifactStorageException e)
{
logger.error("Unable to copy artifact due to ArtifactStorageException", e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}
catch (Exception e)
{
logger.error("Unable to copy artifact", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(e.getMessage());
}
return ResponseEntity.ok("The path was copied successfully.");
}
@ApiOperation(value = "Deletes a path from a repository.",
position = 3)
@ApiResponses(value = { @ApiResponse(code = 200,
message = "The artifact was deleted."),
@ApiResponse(code = 400,
message = "Bad request."),
@ApiResponse(code = 404,
message = "The specified storageId/repositoryId/path does not exist!") })
@PreAuthorize("hasAuthority('ARTIFACTS_DELETE')")
@RequestMapping(value = "{storageId}/{repositoryId}/{path:.+}",
method = RequestMethod.DELETE)
public ResponseEntity delete(@ApiParam(value = "The storageId",
required = true)
@PathVariable String storageId,
@ApiParam(value = "The repositoryId",
required = true)
@PathVariable String repositoryId,
@ApiParam(value = "Whether to use force delete")
@RequestParam(defaultValue = "false",
name = "force",
required = false) boolean force,
@PathVariable String path)
throws IOException, JAXBException
{
logger.info("Deleting " + storageId + ":" + repositoryId + "/" + path + "...");
try
{
if (getStorage(storageId) == null)
{
return ResponseEntity.status(NOT_FOUND)
.body("The specified storageId does not exist!");
}
if (getStorage(storageId).getRepository(repositoryId) == null)
{
return ResponseEntity.status(NOT_FOUND)
.body("The specified repositoryId does not exist!");
}
if (getStorage(storageId) != null &&
getStorage(storageId).getRepository(repositoryId) != null &&
!new File(getStorage(storageId).getRepository(repositoryId)
.getBasedir(), path).exists())
{
return ResponseEntity.status(NOT_FOUND)
.body("The specified path does not exist!");
}
getArtifactManagementService().delete(storageId, repositoryId, path, force);
}
catch (ArtifactStorageException e)
{
logger.error(e.getMessage(), e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}
return ResponseEntity.ok("The artifact was deleted.");
}
public ArtifactManagementService getArtifactManagementService()
{
return mavenArtifactManagementService;
}
}