/*************************GO-LICENSE-START*********************************
* Copyright 2014 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.
*************************GO-LICENSE-END***********************************/
package com.thoughtworks.go.server.service;
import com.thoughtworks.go.domain.ArtifactUrlReader;
import com.thoughtworks.go.domain.JobIdentifier;
import com.thoughtworks.go.domain.Stage;
import com.thoughtworks.go.domain.StageIdentifier;
import com.thoughtworks.go.domain.exception.IllegalArtifactLocationException;
import com.thoughtworks.go.legacywrapper.LogParser;
import com.thoughtworks.go.server.dao.StageDao;
import com.thoughtworks.go.server.domain.LogFile;
import com.thoughtworks.go.server.view.artifacts.ArtifactDirectoryChooser;
import com.thoughtworks.go.server.view.artifacts.BuildIdArtifactLocator;
import com.thoughtworks.go.server.view.artifacts.PathBasedArtifactsLocator;
import com.thoughtworks.go.util.*;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipInputStream;
import static java.lang.String.format;
@Service
public class ArtifactsService implements ArtifactUrlReader {
private final ArtifactsDirHolder artifactsDirHolder;
private final ZipUtil zipUtil;
private final JobResolverService jobResolverService;
private final StageDao stageDao;
private SystemService systemService;
@Autowired
private LogParser logParser;
public static final Logger LOGGER = Logger.getLogger(ArtifactsService.class);
public static final String LOG_XML_NAME = "log.xml";
private ArtifactDirectoryChooser chooser;
@Autowired
public ArtifactsService(JobResolverService jobResolverService, StageDao stageDao,
ArtifactsDirHolder artifactsDirHolder, ZipUtil zipUtil, SystemService systemService) {
this(jobResolverService, stageDao, artifactsDirHolder, zipUtil, systemService, new ArtifactDirectoryChooser());
}
protected ArtifactsService(JobResolverService jobResolverService, StageDao stageDao,
ArtifactsDirHolder artifactsDirHolder, ZipUtil zipUtil, SystemService systemService, ArtifactDirectoryChooser chooser) {
this.artifactsDirHolder = artifactsDirHolder;
this.zipUtil = zipUtil;
this.jobResolverService = jobResolverService;
this.stageDao = stageDao;
this.systemService = systemService;
//This is a Chain of Responsibility to decide which view should be shown for a particular artifact URL
this.chooser = chooser;
}
public void initialize() {
chooser.add(new PathBasedArtifactsLocator(artifactsDirHolder.getArtifactsDir()));
chooser.add(new BuildIdArtifactLocator(artifactsDirHolder.getArtifactsDir()));
}
public boolean saveFile(File dest, InputStream stream, boolean shouldUnzip, int attempt) {
String destPath = dest.getAbsolutePath();
try {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Saving file [" + destPath + "]");
}
if (shouldUnzip) {
zipUtil.unzip(new ZipInputStream(stream), dest);
} else {
systemService.streamToFile(stream, dest);
}
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("File [" + destPath + "] saved.");
}
return true;
} catch (IOException e) {
final String message = format("Failed to save the file to: [%s]", destPath);
if (attempt < GoConstants.PUBLISH_MAX_RETRIES) {
LOGGER.warn(message, e);
} else {
LOGGER.error(message, e);
}
return false;
} catch (IllegalPathException e){
final String message = format("Failed to save the file to: [%s]", destPath);
LOGGER.error(message, e);
return false;
}
}
public boolean saveOrAppendFile(File dest, InputStream stream) {
String destPath = dest.getAbsolutePath();
try {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Appending file [" + destPath + "]");
}
systemService.streamToFile(stream, dest);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("File [" + destPath + "] appended.");
}
return true;
} catch (IOException e) {
LOGGER.error("Failed to save the file to : [" + destPath + "]", e);
return false;
}
}
public LogFile getInstanceLogFile(JobIdentifier jobIdentifier) throws IllegalArtifactLocationException {
File outputFolder = findArtifact(jobIdentifier, ArtifactLogUtil.CRUISE_OUTPUT_FOLDER);
return new LogFile(new File(outputFolder, LOG_XML_NAME));
}
public Map parseLogFile(LogFile logFile, boolean buildPassed) throws ArtifactsParseException {
try {
Map properties;
File cacheFile = serializedPropertiesFile(logFile.getFile());
if (cacheFile.exists()) {
properties = (Map) ObjectUtil.readObject(cacheFile);
} else if (logFile.getFile().exists()) {
properties = logParser.parseLogFile(logFile, buildPassed);
ObjectUtil.writeObject(properties, cacheFile);
} else {
properties = new HashMap();
}
return properties;
} catch (Exception e) {
LOGGER.error("Error parsing log file: ", e);
String filePath = logFile == null ? "null log file" : logFile.getFile().getPath();
throw new ArtifactsParseException("Error parsing log file: " + filePath, e);
}
}
public File serializedPropertiesFile(File logFile) {
return new File(logFile.getParent(), "." + logFile.getName() + ".ser");
}
public File findArtifact(JobIdentifier identifier, String path) throws IllegalArtifactLocationException {
return chooser.findArtifact(identifier, path);
}
public String findArtifactRoot(JobIdentifier identifier) throws IllegalArtifactLocationException {
JobIdentifier id = jobResolverService.actualJobIdentifier(identifier);
try {
String fullArtifactPath = chooser.findArtifact(id, "").getCanonicalPath();
String artifactRoot = artifactsDirHolder.getArtifactsDir().getCanonicalPath();
String relativePath = fullArtifactPath.replace(artifactRoot, "");
if (relativePath.startsWith(File.separator)) {
relativePath = relativePath.replaceFirst("\\" + File.separator, "");
}
return relativePath;
} catch (IOException e) {
throw new IllegalArtifactLocationException("No artifact found.", e);
}
}
public String findArtifactUrl(JobIdentifier jobIdentifier) {
JobIdentifier actualId = jobResolverService.actualJobIdentifier(jobIdentifier);
return format("/files/%s", actualId.buildLocator());
}
public String findArtifactUrl(JobIdentifier jobIdentifier, String path) {
return format("%s/%s", findArtifactUrl(jobIdentifier), path);
}
public File getArtifactLocation(String path) throws IllegalArtifactLocationException {
try {
File file = new File(artifactsDirHolder.getArtifactsDir(), path);
if (!FileUtil.isSubdirectoryOf(artifactsDirHolder.getArtifactsDir(), file)) {
throw new IllegalArtifactLocationException("Illegal artifact path " + path);
}
return file;
} catch (Exception e) {
throw new IllegalArtifactLocationException("Illegal artifact path " + path);
}
}
public void purgeArtifactsForStage(Stage stage) {
StageIdentifier stageIdentifier = stage.getIdentifier();
try {
File stageRoot = chooser.findArtifact(stageIdentifier, "");
File cachedStageRoot = chooser.findCachedArtifact(stageIdentifier);
deleteFile(cachedStageRoot);
boolean didDelete = deleteArtifactsExceptCruiseOutput(stageRoot);
if (!didDelete) {
LOGGER.error(format("Artifacts for stage '%s' at path '%s' was not deleted", stageIdentifier.entityLocator(), stageRoot.getAbsolutePath()));
}
} catch (Exception e) {
LOGGER.error(format("Error occurred while clearing artifacts for '%s'. Error: '%s'", stageIdentifier.entityLocator(), e.getMessage()), e);
}
stageDao.markArtifactsDeletedFor(stage);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(format("Marked stage '%s' as artifacts deleted.", stageIdentifier.entityLocator()));
}
}
private boolean deleteArtifactsExceptCruiseOutput(File stageRoot) throws IOException {
File[] jobs = stageRoot.listFiles();
if (jobs == null) { // null if security restricted
throw new IOException("Failed to list contents of " + stageRoot);
}
boolean didDelete = true;
for (File jobRoot : jobs) {
File[] artifacts = jobRoot.listFiles();
if (artifacts == null) { // null if security restricted
throw new IOException("Failed to list contents of " + stageRoot);
}
for (File artifact : artifacts) {
if (artifact.isDirectory() && artifact.getName().equals(ArtifactLogUtil.CRUISE_OUTPUT_FOLDER)) {
continue;
}
didDelete &= deleteFile(artifact);
}
}
return didDelete;
}
private boolean deleteFile(File file) {
return FileUtils.deleteQuietly(file);
}
}