package org.sigmah.server.file.impl;
/*
* #%L
* Sigmah
* %%
* Copyright (C) 2010 - 2016 URD
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.ArrayUtils;
import org.sigmah.server.conf.Properties;
import org.sigmah.server.dao.OrgUnitDAO;
import org.sigmah.server.domain.User;
import org.sigmah.server.file.BackupArchiveJobFactory;
import org.sigmah.server.file.BackupArchiveManager;
import org.sigmah.server.file.impl.BackupArchiveJob.BackupArchiveJobArgument;
import org.sigmah.shared.conf.PropertyKey;
import org.sigmah.shared.dto.BackupDTO;
import org.sigmah.shared.dto.value.FileDTO.LoadingScope;
import org.sigmah.shared.util.FileType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
import java.util.Arrays;
import java.util.Comparator;
import org.sigmah.shared.dispatch.FunctionalException;
/**
* {@link BackupArchiveManager} implementation.
*
* @author Denis Colliot (dcolliot@ideia.fr)
*/
public class BackupArchiveManagerImpl implements BackupArchiveManager {
/**
* Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(BackupArchiveManagerImpl.class);
/**
* Separator used in backup archive files name.
*/
private static final String BACKUP_ARCHIVE_NAME_SEP = "-";
/**
* Archive files extension (with separator).
*/
private static final String BACKUP_ARCHIVE_EXT = FileType.ZIP.getExtension();
/**
* Temporary files extension (with separator).
*/
private static final String BACKUP_ARCHIVE_TEMP_EXT = ".tmp";
/**
* <p>
* Backup archive file name pattern.
* </p>
* <p>
* Detects following groups:
* </p>
*
* <pre>
* {organizationId}_{orgUnitId}_{loadingScope}.{extension}
* </pre>
*/
private static final Pattern BACKUP_ARCHIVE_NAME_PATTERN = Pattern.compile("(\\d*)"
+ BACKUP_ARCHIVE_NAME_SEP
+ "(\\d*)"
+ BACKUP_ARCHIVE_NAME_SEP
+ "("
+ LoadingScope.ALL_VERSIONS
+ '|'
+ LoadingScope.LAST_VERSION
+ ")"
+ "("
+ BACKUP_ARCHIVE_EXT
+ '|'
+ BACKUP_ARCHIVE_TEMP_EXT
+ ")");
/**
* Injected application properties.
*/
private final Properties properties;
/**
* Injected {@link OrgUnitDAO}.
*/
private final OrgUnitDAO orgUnitDAO;
/**
* Injected {@link BackupArchiveJobFactory}.
*/
private final BackupArchiveJobFactory backupArchiveJobFactory;
@Inject
public BackupArchiveManagerImpl(final Properties properties, final OrgUnitDAO orgUnitDAO, final BackupArchiveJobFactory backupArchiveJobFactory) {
this.properties = properties;
this.orgUnitDAO = orgUnitDAO;
this.backupArchiveJobFactory = backupArchiveJobFactory;
}
/**
* {@inheritDoc}
*/
@Override
public InputStream open(final String archiveId) throws IOException {
if (LOG.isTraceEnabled()) {
LOG.trace("Opening a new stream to archive file '{}'.", archiveId);
}
return Files.newInputStream(Paths.get(getArchiveRootPath(), archiveId));
}
/**
* {@inheritDoc}
*/
@Override
public BackupDTO getRunningBackupProcessFile(final Integer organizationId) throws IOException {
if (LOG.isTraceEnabled()) {
LOG.trace("Looking for running backup process file for organization #{}.", organizationId);
}
final File[] tempFiles = Paths.get(getArchiveRootPath()).toFile().listFiles(new FilenameFilter() {
@Override
public boolean accept(final File file, final String name) {
return name.startsWith(organizationId + BACKUP_ARCHIVE_NAME_SEP) && name.endsWith(BACKUP_ARCHIVE_TEMP_EXT);
}
});
if (ArrayUtils.isEmpty(tempFiles)) {
if (LOG.isTraceEnabled()) {
LOG.trace("No running backup process file has been found for organization #{}.", organizationId);
}
return null;
}
return fromFile(tempFiles[0].toPath());
}
/**
* {@inheritDoc}
*/
@Override
public BackupDTO getExistingBackup(final Integer organizationId) throws IOException {
if (LOG.isTraceEnabled()) {
LOG.trace("Looking for existing backup file for organization #{}.", organizationId);
}
final File[] archiveFiles = Paths.get(getArchiveRootPath()).toFile().listFiles(new FilenameFilter() {
@Override
public boolean accept(final File file, final String name) {
return name.startsWith(organizationId + BACKUP_ARCHIVE_NAME_SEP) && name.endsWith(BACKUP_ARCHIVE_EXT);
}
});
if (ArrayUtils.isEmpty(archiveFiles)) {
if (LOG.isTraceEnabled()) {
LOG.trace("No existing backup file has been found for organization #{}.", organizationId);
}
return null;
}
// BUGFIX #671 & #772: Sorting files by creation date to retrieve the most recent backup.
Arrays.sort(archiveFiles, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
return getCreationDate(o2).compareTo(getCreationDate(o1));
}
});
return fromFile(archiveFiles[0].toPath());
}
/**
* Returns the creation date of the given file or the 1st january 1970
* if an error occured while trying to read the date.
*
* @param file File to access.
* @return Creation date of the given file.
*/
private Date getCreationDate(File file) {
try {
final BasicFileAttributeView view = Files.getFileAttributeView(file.toPath(), BasicFileAttributeView.class);
final BasicFileAttributes attributes = view.readAttributes();
return new Date(attributes.creationTime().toMillis());
} catch(IOException e) {
return new Date(0L);
}
}
/**
* {@inheritDoc}
*/
@Override
public void startBackupArchiveGeneration(final BackupDTO backup, final User user) throws IOException, FunctionalException {
if (LOG.isTraceEnabled()) {
LOG.trace("Starting new backup process for configuration '{}' ; Backup launched by user '{}'.", backup, user);
}
if (backup == null || backup.getOrganizationId() == null || backup.getOrgUnitId() == null) {
throw new IllegalArgumentException("Backup configuration is invalid.");
}
// Archive files.
final Path tempArchiveFile = Paths.get(getArchiveRootPath(), buildArchiveFileName(backup, true));
final Path finalArchiveFile = Paths.get(getArchiveRootPath(), buildArchiveFileName(backup, false));
// Creates temporary file.
try {
Files.createFile(tempArchiveFile);
} catch(IOException e) {
throw new FunctionalException(e, FunctionalException.ErrorCode.ADMIN_BACKUP_ARCHIVE_CREATION_FAILED, tempArchiveFile.toString());
}
if (LOG.isTraceEnabled()) {
LOG.trace("Backup process file has been created: '{}'. Initializing job.", tempArchiveFile);
}
// Process is executed in a different thread in order to release current thread.
Executors.newSingleThreadExecutor().execute(
backupArchiveJobFactory.newJob(new BackupArchiveJobArgument(backup, user.getId(), tempArchiveFile, finalArchiveFile)));
}
// --------------------------------------------------------------------------------------------------------------
//
// UTILITY METHODS.
//
// --------------------------------------------------------------------------------------------------------------
/**
* Returns the archives storage root directory path.
*
* @return The archives storage root directory path.
*/
private String getArchiveRootPath() {
return properties.getProperty(PropertyKey.ARCHIVE_REPOSITORY_NAME);
}
/**
* <p>
* Builds the given {@code backup} corresponding archive file name.
* </p>
* <p>
* Archive file name is generated with following format: {@code <organizationId>_<orgUnitId>_<loadingMode>.zip}
* </p>
*
* @param backup
* The backup configuration.
* @param tempFile
* {@code true} to generate a temporary file name, {@code false} to generate a complete file name.
* @return The {@code backup} corresponding archive file name (with extension).
*/
private static String buildArchiveFileName(final BackupDTO backup, final boolean tempFile) {
final StringBuilder builder = new StringBuilder();
builder.append(backup.getOrganizationId());
builder.append(BACKUP_ARCHIVE_NAME_SEP);
builder.append(backup.getOrgUnitId());
builder.append(BACKUP_ARCHIVE_NAME_SEP);
builder.append(backup.getLoadingScope().name());
builder.append(tempFile ? BACKUP_ARCHIVE_TEMP_EXT : BACKUP_ARCHIVE_EXT);
return builder.toString();
}
/**
* Builds the given {@code file} corresponding {@link BackupDTO}.
*
* @param file
* The backup file path.
* @return The given {@code file} corresponding {@link BackupDTO}.
* @throws IOException
* If an I/O error occurs.
* @throws IllegalArgumentException
* If the given {@code file} does not reference a valid backup file.
*/
private BackupDTO fromFile(final Path file) throws IOException {
final String filename = file.getFileName().toString();
final Matcher matcher = BACKUP_ARCHIVE_NAME_PATTERN.matcher(filename);
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid backup archive file name '" + filename + "'.");
}
final Integer organizationId = Integer.parseInt(matcher.group(1));
final Integer orgUnitId = Integer.parseInt(matcher.group(2));
final LoadingScope loadingScope = LoadingScope.valueOf(matcher.group(3));
final String extension = matcher.group(4);
final BasicFileAttributeView view = Files.getFileAttributeView(file, BasicFileAttributeView.class);
final BasicFileAttributes attributes = view.readAttributes();
final BackupDTO result = new BackupDTO();
result.setOrganizationId(organizationId);
result.setOrgUnitId(orgUnitId);
result.setOrgUnitName(orgUnitDAO.findById(orgUnitId).getFullName());
result.setLoadingScope(loadingScope);
result.setCreationDate(new Date(attributes.creationTime().toMillis()));
result.setArchiveFileName(filename);
result.setRunning(extension.equals(BACKUP_ARCHIVE_TEMP_EXT));
return result;
}
}