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; } }