/*
* 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.service;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.sql.DataSource;
import com.thoughtworks.go.config.GoMailSender;
import com.thoughtworks.go.database.Database;
import com.thoughtworks.go.i18n.LocalizedMessage;
import com.thoughtworks.go.security.CipherProvider;
import com.thoughtworks.go.server.domain.ServerBackup;
import com.thoughtworks.go.server.domain.Username;
import com.thoughtworks.go.server.messaging.EmailMessageDrafter;
import com.thoughtworks.go.server.persistence.ServerBackupRepository;
import com.thoughtworks.go.server.service.result.HttpLocalizedOperationResult;
import com.thoughtworks.go.server.util.ServerVersion;
import com.thoughtworks.go.server.web.BackupStatusProvider;
import com.thoughtworks.go.serverhealth.HealthStateType;
import com.thoughtworks.go.service.ConfigRepository;
import com.thoughtworks.go.util.SystemEnvironment;
import com.thoughtworks.go.util.TimeProvider;
import com.thoughtworks.go.util.VoidThrowingFn;
import org.apache.commons.io.DirectoryWalker;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @understands backing up db and config
*/
@Service
public class BackupService implements BackupStatusProvider {
private static final Logger LOGGER = Logger.getLogger(BackupService.class);
public static final String BACKUP = "backup_";
private final DataSource dataSource;
private final ArtifactsDirHolder artifactsDirHolder;
private final GoConfigService goConfigService;
private ServerBackupRepository serverBackupRepository;
private final TimeProvider timeProvider;
private final SystemEnvironment systemEnvironment;
private ServerVersion serverVersion;
private final ConfigRepository configRepository;
private final Database databaseStrategy;
private GoMailSender mailSender;
private volatile DateTime backupRunningSince;
private volatile String backupStartedBy;
final String CONFIG_BACKUP_ZIP = "config-dir.zip";
private static final String CONFIG_REPOSITORY_BACKUP_ZIP = "config-repo.zip";
private static final String VERSION_BACKUP_FILE = "version.txt";
private static final String BACKUP_MUTEX = "GO-SERVER-BACKUP-IN-PROGRESS".intern();
@Autowired
public BackupService(DataSource dataSource, ArtifactsDirHolder artifactsDirHolder, GoConfigService goConfigService, TimeProvider timeProvider,
ServerBackupRepository serverBackupRepository, SystemEnvironment systemEnvironment, ServerVersion serverVersion, ConfigRepository configRepository, Database databaseStrategy) {
this.dataSource = dataSource;
this.artifactsDirHolder = artifactsDirHolder;
this.goConfigService = goConfigService;
this.serverBackupRepository = serverBackupRepository;
this.systemEnvironment = systemEnvironment;
this.serverVersion = serverVersion;
this.configRepository = configRepository;
this.databaseStrategy = databaseStrategy;
this.timeProvider = timeProvider;
}
public void initialize() {
mailSender = goConfigService.getMailSender();
}
public ServerBackup startBackup(Username username, HttpLocalizedOperationResult result) {
if (!goConfigService.isUserAdmin(username)) {
result.unauthorized(LocalizedMessage.string("UNAUTHORIZED_TO_BACKUP"), HealthStateType.unauthorised());
return null;
}
synchronized (BACKUP_MUTEX) {
DateTime now = timeProvider.currentDateTime();
final File destDir = new File(backupLocation(), BACKUP + now.toString("YYYYMMdd-HHmmss"));
if (!destDir.mkdirs()) {
result.badRequest(LocalizedMessage.string("BACKUP_UNSUCCESSFUL", "Could not create the backup directory."));
return null;
}
try {
backupRunningSince = now;
backupStartedBy = username.getUsername().toString();
backupVersion(destDir);
backupConfig(destDir);
configRepository.doLocked(new VoidThrowingFn<IOException>() {
@Override public void run() throws IOException {
backupConfigRepository(destDir);
}
});
backupDb(destDir);
ServerBackup serverBackup = new ServerBackup(destDir.getAbsolutePath(), now.toDate(), username.getUsername().toString());
serverBackupRepository.save(serverBackup);
mailSender.send(EmailMessageDrafter.backupSuccessfullyCompletedMessage(destDir.getAbsolutePath(), goConfigService.adminEmail(), username));
result.setMessage(LocalizedMessage.string("BACKUP_COMPLETED_SUCCESSFULLY"));
return serverBackup;
} catch (Exception e) {
FileUtils.deleteQuietly(destDir);
result.badRequest(LocalizedMessage.string("BACKUP_UNSUCCESSFUL", e.getMessage()));
LOGGER.error("[Backup] Failed to backup Go.", e);
mailSender.send(EmailMessageDrafter.backupFailedMessage(e.getMessage(), goConfigService.adminEmail()));
} finally {
backupRunningSince = null;
backupStartedBy = null;
}
return null;
}
}
private void backupVersion(File backupDir) throws IOException {
File versionFile = new File(backupDir, VERSION_BACKUP_FILE);
FileUtils.writeStringToFile(versionFile, serverVersion.version());
}
private void backupConfigRepository(File backupDir) throws IOException {
File configRepoDir = systemEnvironment.getConfigRepoDir();
try (ZipOutputStream configRepoZipStream = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(new File(backupDir, CONFIG_REPOSITORY_BACKUP_ZIP))))) {
new DirectoryStructureWalker(configRepoDir.getAbsolutePath(), configRepoZipStream).walk();
}
}
private void backupConfig(File backupDir) throws IOException {
String configDirectory = systemEnvironment.getConfigDir();
try (ZipOutputStream configZip = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(new File(backupDir, CONFIG_BACKUP_ZIP))))) {
File cruiseConfigFile = new File(systemEnvironment.getCruiseConfigFile());
File cipherFile = systemEnvironment.getCipherFile();
new DirectoryStructureWalker(configDirectory, configZip, cruiseConfigFile, cipherFile).walk();
configZip.putNextEntry(new ZipEntry(cruiseConfigFile.getName()));
IOUtils.write(goConfigService.xml(), configZip);
configZip.putNextEntry(new ZipEntry(cipherFile.getName()));
IOUtils.write(new CipherProvider(systemEnvironment).getKey(), configZip);
}
}
private void backupDb(File backupDir) throws SQLException {
databaseStrategy.backup(backupDir);
}
public String backupLocation() {
return artifactsDirHolder.getBackupsDir().getAbsolutePath();
}
public Date lastBackupTime() {
ServerBackup serverBackup = serverBackupRepository.lastBackup();
return serverBackup == null ? null : serverBackup.getTime();
}
public String lastBackupUser() {
ServerBackup serverBackup = serverBackupRepository.lastBackup();
return serverBackup == null ? null : serverBackup.getUsername();
}
public void deleteAll() {
serverBackupRepository.deleteAll();
}
public boolean isBackingUp() {
return backupRunningSince != null;
}
public String backupRunningSinceISO8601() {
return !(backupRunningSince == null) ? backupRunningSince.toString() : null;
}
public String backupStartedBy() {
return !(backupStartedBy == null) ? backupStartedBy : null;
}
public String availableDiskSpace() {
File artifactsDir = artifactsDirHolder.getArtifactsDir();
return FileUtils.byteCountToDisplaySize(artifactsDir.getUsableSpace());
}
}
class DirectoryStructureWalker extends DirectoryWalker {
private final String configDirectory;
private final ZipOutputStream zipStream;
private final ArrayList<String> excludeFiles;
public DirectoryStructureWalker(String configDirectory, ZipOutputStream zipStream, File ...excludeFiles) {
this.excludeFiles = new ArrayList<>();
for (File excludeFile : excludeFiles) {
this.excludeFiles.add(excludeFile.getAbsolutePath());
}
this.configDirectory = new File(configDirectory).getAbsolutePath();
this.zipStream = zipStream;
}
@Override
protected boolean handleDirectory(File directory, int depth, Collection results) throws IOException {
if (! directory.getAbsolutePath().equals(configDirectory)) {
ZipEntry e = new ZipEntry(fromRoot(directory) + "/");
zipStream.putNextEntry(e);
}
return true;
}
@Override
protected void handleFile(File file, int depth, Collection results) throws IOException {
if (excludeFiles.contains(file.getAbsolutePath())) {
return;
}
zipStream.putNextEntry(new ZipEntry(fromRoot(file)));
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) {
IOUtils.copy(in, zipStream);
}
}
private String fromRoot(File directory) {
return directory.getAbsolutePath().substring(configDirectory.length() + 1);
}
public void walk() throws IOException {
walk(new File(this.configDirectory), null);
}
}