/* * Copyright 2016 ThoughtWorks, Inc. * * 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 com.thoughtworks.go.server.controller; import com.thoughtworks.go.domain.ConsoleOut; import com.thoughtworks.go.domain.JobIdentifier; import com.thoughtworks.go.domain.exception.IllegalArtifactLocationException; import com.thoughtworks.go.server.cache.ZipArtifactCache; import com.thoughtworks.go.server.security.HeaderConstraint; import com.thoughtworks.go.server.service.ArtifactsService; import com.thoughtworks.go.server.service.ConsoleActivityMonitor; import com.thoughtworks.go.server.service.ConsoleService; import com.thoughtworks.go.server.service.RestfulService; import com.thoughtworks.go.server.util.ErrorHandler; import com.thoughtworks.go.server.view.artifacts.ArtifactsView; import com.thoughtworks.go.server.view.artifacts.LocalArtifactsView; import com.thoughtworks.go.server.web.ArtifactFolderViewFactory; import com.thoughtworks.go.server.web.FileModelAndView; import com.thoughtworks.go.server.web.ResponseCodeView; import com.thoughtworks.go.util.ArtifactLogUtil; import com.thoughtworks.go.util.SystemEnvironment; import org.apache.commons.io.IOUtils; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; 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.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import static com.thoughtworks.go.server.web.ZipArtifactFolderViewFactory.zipViewFactory; import static com.thoughtworks.go.util.ArtifactLogUtil.isConsoleOutput; import static com.thoughtworks.go.util.GoConstants.*; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; @Controller public class ArtifactsController { private ArtifactsService artifactsService; private RestfulService restfulService; private static final Logger LOGGER = Logger.getLogger(ArtifactsController.class); private final ConsoleActivityMonitor consoleActivityMonitor; private ConsoleService consoleService; private final ArtifactFolderViewFactory folderViewFactory; private final ArtifactFolderViewFactory jsonViewFactory; private final ArtifactFolderViewFactory zipViewFactory; private HeaderConstraint headerConstraint; @Autowired ArtifactsController(ArtifactsService artifactsService, RestfulService restfulService, ZipArtifactCache zipArtifactCache, ConsoleActivityMonitor consoleActivityMonitor, ConsoleService consoleService, SystemEnvironment systemEnvironment) { this.artifactsService = artifactsService; this.restfulService = restfulService; this.consoleActivityMonitor = consoleActivityMonitor; this.consoleService = consoleService; this.folderViewFactory = FileModelAndView.htmlViewFactory(); this.jsonViewFactory = FileModelAndView.jsonViewfactory(); this.zipViewFactory = zipViewFactory(zipArtifactCache); this.headerConstraint = new HeaderConstraint(systemEnvironment); } /* RESTful URLs */ @RequestMapping("/repository/restful/artifact/GET/html") public ModelAndView getArtifactAsHtml(@RequestParam("pipelineName") String pipelineName, @RequestParam("pipelineLabel") String counterOrLabel, @RequestParam("stageName") String stageName, @RequestParam(value = "stageCounter", required = false) String stageCounter, @RequestParam("buildName") String buildName, @RequestParam("filePath") String filePath, @RequestParam(value = "sha1", required = false) String sha, @RequestParam(value = "serverAlias", required = false) String serverAlias) throws Exception { return getArtifact(filePath, folderViewFactory, pipelineName, counterOrLabel, stageName, stageCounter, buildName, sha, serverAlias); } @RequestMapping("/repository/restful/artifact/GET/json") public ModelAndView getArtifactAsJson(@RequestParam("pipelineName") String pipelineName, @RequestParam("pipelineLabel") String counterOrLabel, @RequestParam("stageName") String stageName, @RequestParam(value = "stageCounter", required = false) String stageCounter, @RequestParam("buildName") String buildName, @RequestParam("filePath") String filePath, @RequestParam(value = "sha1", required = false) String sha ) throws Exception { return getArtifact(filePath, jsonViewFactory, pipelineName, counterOrLabel, stageName, stageCounter, buildName, sha, null); } @RequestMapping("/repository/restful/artifact/GET/zip") public ModelAndView getArtifactAsZip(@RequestParam("pipelineName") String pipelineName, @RequestParam("pipelineLabel") String counterOrLabel, @RequestParam("stageName") String stageName, @RequestParam(value = "stageCounter", required = false) String stageCounter, @RequestParam("buildName") String buildName, @RequestParam("filePath") String filePath, @RequestParam(value = "sha1", required = false) String sha ) throws Exception { return getArtifact(filePath, zipViewFactory, pipelineName, counterOrLabel, stageName, stageCounter, buildName, sha, null); } @RequestMapping("/repository/restful/artifact/GET/*") public void fetch(HttpServletRequest request, HttpServletResponse response) throws Exception { request.getRequestDispatcher("/repository/restful/artifact/GET/html").forward(request, response); } @RequestMapping("/repository/restful/artifact/POST/*") public ModelAndView postArtifact(@RequestParam("pipelineName") String pipelineName, @RequestParam("pipelineLabel") String counterOrLabel, @RequestParam("stageName") String stageName, @RequestParam(value = "stageCounter", required = false) String stageCounter, @RequestParam("buildName") String buildName, @RequestParam(value = "buildId", required = false) Long buildId, @RequestParam("filePath") String filePath, @RequestParam(value = "attempt", required = false) Integer attempt, MultipartHttpServletRequest request) throws Exception { JobIdentifier jobIdentifier; if(!headerConstraint.isSatisfied(request)) { return ResponseCodeView.create(HttpServletResponse.SC_BAD_REQUEST, "Missing required header 'Confirm'"); } try { jobIdentifier = restfulService.findJob(pipelineName, counterOrLabel, stageName, stageCounter, buildName, buildId); } catch (Exception e) { return buildNotFound(pipelineName, counterOrLabel, stageName, stageCounter, buildName); } int convertedAttempt = attempt == null ? 1 : attempt; try { File artifact = artifactsService.findArtifact(jobIdentifier, filePath); if (artifact.exists() && artifact.isFile()) { return FileModelAndView.fileAlreadyExists(filePath); } MultipartFile multipartFile = multipartFile(request); if (multipartFile == null) { return FileModelAndView.invalidUploadRequest(); } boolean success = saveFile(convertedAttempt, artifact, multipartFile, shouldUnzipStream(multipartFile)); if (!success) { return FileModelAndView.errorSavingFile(filePath); } success = updateChecksumFile(request, jobIdentifier, filePath); if (!success) { return FileModelAndView.errorSavingChecksumFile(filePath); } return FileModelAndView.fileCreated(filePath); } catch (IllegalArtifactLocationException e) { return FileModelAndView.forbiddenUrl(filePath); } } private boolean updateChecksumFile(MultipartHttpServletRequest request, JobIdentifier jobIdentifier, String filePath) throws IOException, IllegalArtifactLocationException { MultipartFile checksumMultipartFile = getChecksumFile(request); if (checksumMultipartFile != null) { String checksumFilePath = String.format("%s/%s/%s", artifactsService.findArtifactRoot(jobIdentifier), ArtifactLogUtil.CRUISE_OUTPUT_FOLDER, ArtifactLogUtil.MD5_CHECKSUM_FILENAME); File checksumFile = artifactsService.getArtifactLocation(checksumFilePath); synchronized (checksumFilePath.intern()) { return artifactsService.saveOrAppendFile(checksumFile, checksumMultipartFile.getInputStream()); } } else { LOGGER.warn(String.format("[Artifacts Upload] Checksum file not uploaded for artifact at path '%s'", filePath)); } return true; } private boolean saveFile(int convertedAttempt, File artifact, MultipartFile multipartFile, boolean shouldUnzip) throws IOException { InputStream inputStream = null; boolean success; try { inputStream = multipartFile.getInputStream(); success = artifactsService.saveFile(artifact, inputStream, shouldUnzip, convertedAttempt); } finally { IOUtils.closeQuietly(inputStream); } return success; } @RequestMapping("/repository/restful/artifact/PUT/*") public ModelAndView putArtifact(@RequestParam("pipelineName") String pipelineName, @RequestParam("pipelineLabel") String counterOrLabel, @RequestParam("stageName") String stageName, @RequestParam(value = "stageCounter", required = false) String stageCounter, @RequestParam("buildName") String buildName, @RequestParam(value = "buildId", required = false) Long buildId, @RequestParam("filePath") String filePath, @RequestParam(value = "agentId", required = false) String agentId, HttpServletRequest request ) throws Exception { if (filePath.contains("..")) { return FileModelAndView.forbiddenUrl(filePath); } JobIdentifier jobIdentifier; try { jobIdentifier = restfulService.findJob(pipelineName, counterOrLabel, stageName, stageCounter, buildName, buildId); } catch (Exception e) { return buildNotFound(pipelineName, counterOrLabel, stageName, stageCounter, buildName); } if (isConsoleOutput(filePath)) { return putConsoleOutput(jobIdentifier, request.getInputStream()); } else { return putArtifact(jobIdentifier, filePath, request.getInputStream()); } } /* Other URLs */ @RequestMapping(value = "/**/consoleout.json", method = RequestMethod.GET) public ModelAndView consoleout(@RequestParam("pipelineName") String pipelineName, @RequestParam("pipelineLabel") String counterOrLabel, @RequestParam("stageName") String stageName, @RequestParam("buildName") String buildName, @RequestParam(value = "stageCounter", required = false) String stageCounter, @RequestParam(value = "startLineNumber", required = false) Integer start ) throws Exception { int startLine = start == null ? 0 : start; try { JobIdentifier identifier = restfulService.findJob(pipelineName, counterOrLabel, stageName, stageCounter, buildName); ConsoleOut consoleOut = consoleService.getConsoleOut(identifier, startLine); return new ModelAndView(new ConsoleOutView(consoleOut.calculateNextStart(), consoleOut.output())); } catch (FileNotFoundException e) { return new ModelAndView(new ConsoleOutView(0, "")); } catch (Exception e) { return buildNotFound(pipelineName, counterOrLabel, stageName, stageCounter, buildName); } } @ErrorHandler public ModelAndView handleError(HttpServletRequest request, HttpServletResponse response, Exception e) { LOGGER.error("Error loading artifacts: ", e); Map model = new HashMap(); model.put(ERROR_FOR_PAGE, "Artifact does not exist."); return new ModelAndView("exceptions_page", model); } ModelAndView getArtifact(String filePath, ArtifactFolderViewFactory folderViewFactory, String pipelineName, String counterOrLabel, String stageName, String stageCounter, String buildName, String sha, String serverAlias) throws Exception { LOGGER.info(String.format("[Artifact Download] Trying to resolve '%s' for '%s/%s/%s/%s/%s'", filePath, pipelineName, counterOrLabel, stageName, stageCounter, buildName)); long before = System.currentTimeMillis(); ArtifactsView view; //Work out the job that we are trying to retrieve JobIdentifier translatedId; try { translatedId = restfulService.findJob(pipelineName, counterOrLabel, stageName, stageCounter, buildName); } catch (Exception e) { return buildNotFound(pipelineName, counterOrLabel, stageName, stageCounter, buildName); } if (filePath.contains("..")) { return FileModelAndView.forbiddenUrl(filePath); } view = new LocalArtifactsView(folderViewFactory, artifactsService, translatedId, consoleService); ModelAndView createdView = view.createView(filePath, sha); LOGGER.info(String.format("[Artifact Download] Successfully resolved '%s' for '%s/%s/%s/%s/%s'. It took: %sms", filePath, pipelineName, counterOrLabel, stageName, stageCounter, buildName, System.currentTimeMillis() - before)); return createdView; } private boolean shouldUnzipStream(MultipartFile multipartFile) { return multipartFile.getName().equals(ZIP_MULTIPART_FILENAME); } private MultipartFile multipartFile(MultipartHttpServletRequest request) throws IOException { MultipartFile multipartFile = request.getFile(REGULAR_MULTIPART_FILENAME); if (multipartFile == null) { multipartFile = request.getFile(ZIP_MULTIPART_FILENAME); } return multipartFile; } private MultipartFile getChecksumFile(MultipartHttpServletRequest request) throws IOException { return request.getFile(CHECKSUM_MULTIPART_FILENAME); } private ModelAndView putConsoleOutput(final JobIdentifier jobIdentifier, final InputStream inputStream) throws Exception { File consoleLogFile = consoleService.consoleLogFile(jobIdentifier); boolean updated = consoleService.updateConsoleLog(consoleLogFile, inputStream); if (updated) { consoleActivityMonitor.consoleUpdatedFor(jobIdentifier); return FileModelAndView.fileAppended(consoleLogFile.getPath()); } else { return FileModelAndView.errorSavingFile(consoleLogFile.getPath()); } } private ModelAndView putArtifact(JobIdentifier jobIdentifier, String filePath, InputStream inputStream) throws Exception { File artifact = artifactsService.findArtifact(jobIdentifier, filePath); if (artifactsService.saveOrAppendFile(artifact, inputStream)) { return FileModelAndView.fileAppended(filePath); } else { return FileModelAndView.errorSavingFile(filePath); } } private ModelAndView buildNotFound(String pipelineName, String counterOrLabel, String stageName, String stageCounter, String buildName) { return ResponseCodeView.create(SC_NOT_FOUND, String.format("Job %s/%s/%s/%s/%s not found.", pipelineName, counterOrLabel, stageName, stageCounter, buildName)); } }