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.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.zip.Deflater; import java.util.zip.ZipOutputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.sigmah.server.dao.FileDAO; import org.sigmah.server.dao.OrgUnitDAO; import org.sigmah.server.dao.ProjectDAO; import org.sigmah.server.dao.UserDAO; import org.sigmah.server.dao.ValueDAO; import org.sigmah.server.domain.OrgUnit; import org.sigmah.server.domain.Project; import org.sigmah.server.domain.User; import org.sigmah.server.domain.element.FilesListElement; import org.sigmah.server.domain.value.FileVersion; import org.sigmah.server.domain.value.Value; import org.sigmah.server.file.FileStorageProvider; import org.sigmah.server.file.util.FileElement; import org.sigmah.server.file.util.FolderElement; import org.sigmah.server.file.util.RepositoryElement; import org.sigmah.server.handler.util.Handlers; import org.sigmah.server.servlet.util.ResponseHelper; import org.sigmah.shared.dto.BackupDTO; import org.sigmah.shared.dto.value.FileDTO.LoadingScope; import org.sigmah.shared.util.ValueResultUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import com.google.inject.persist.Transactional; /** * <p> * Runnable job in charge of generating an organization backup archive. * </p> * <p> * As this process may take a while, the job is executed in a parallel thread. * </p> * * @author Denis Colliot (dcolliot@ideia.fr) */ public class BackupArchiveJob implements Runnable { /** * Backup archive job argument POJO. * * @author Denis Colliot (dcolliot@ideia.fr) */ public static final class BackupArchiveJobArgument { private BackupDTO backup; private Integer userId; private Path tempArchiveFile; private Path finalArchiveFile; BackupArchiveJobArgument(final BackupDTO backup, final Integer userId, final Path tempArchiveFile, final Path finalArchiveFile) { this.backup = backup; this.userId = userId; this.tempArchiveFile = tempArchiveFile; this.finalArchiveFile = finalArchiveFile; } } /** * Logger. */ private static final Logger LOG = LoggerFactory.getLogger(BackupArchiveJob.class); /** * Assisted injected job arguments. */ private final BackupArchiveJobArgument arguments; /** * Injected {@link ProjectDAO}. */ @Inject private ProjectDAO projectDAO; /** * Injected {@link FileDAO}. */ @Inject private FileDAO fileDAO; /** * Injected {@link ValueDAO}. */ @Inject private ValueDAO valueDAO; /** * Injected {@link OrgUnitDAO}. */ @Inject private OrgUnitDAO orgUnitDAO; /** * Injected {@link UserDAO}. */ @Inject private UserDAO userDAO; /** * Injected {@link FileStorageProvider}. */ @Inject private FileStorageProvider fileStorageProvider; @Inject public BackupArchiveJob(@Assisted final BackupArchiveJobArgument arguments) { this.arguments = arguments; } /** * {@inheritDoc} */ @Override public void run() { final Path tempArchiveFile = arguments.tempArchiveFile; final Path finalArchiveFile = arguments.finalArchiveFile; try (final ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream(Files.newOutputStream(tempArchiveFile))) { zipOutputStream.setMethod(ZipOutputStream.DEFLATED); zipOutputStream.setLevel(Deflater.BEST_COMPRESSION); final RepositoryElement repository = buildOrgUnitRepository(arguments.backup, arguments.userId); repository.setName(""); zipRepository(repository, zipOutputStream, ""); // TODO Delete existing previous organization file(s). // Renames temporary '.tmp' file to complete '.zip' file. Files.move(tempArchiveFile, finalArchiveFile, StandardCopyOption.REPLACE_EXISTING); } catch (final Throwable t) { if (LOG.isErrorEnabled()) { LOG.error("An error occurred during backup archive generation process.", t); } try { Files.deleteIfExists(tempArchiveFile); Files.deleteIfExists(finalArchiveFile); } catch (final IOException e) { if (LOG.isErrorEnabled()) { LOG.error("An error occurred while deleting archive error file.", e); } } } } // -------------------------------------------------------------------------------------------------------------- // // UTILITY METHODS. // // -------------------------------------------------------------------------------------------------------------- /** * Builds the given {@code orgUnit} corresponding files repository tree. * * @param backup * The backup configuration. * @param userId * The id of the user requesting the repository build. * @return The given {@code orgUnit} corresponding files repository tree, or {@code null} if the given {@code user} is * not authorized to access the given {@code backup} corresponding OrgUnit. * @throws IllegalArgumentException * If one of the arguments is missing. * @throws UnsupportedOperationException * If the OrgUnit is not visible to the user. */ @Transactional // IMPORTANT: '@Transactional' annotation ensures proper database connections closure. protected RepositoryElement buildOrgUnitRepository(final BackupDTO backup, final Integer userId) { if (backup == null || userId == null) { throw new IllegalArgumentException("Invalid arguments necessary to build OrgUnit files repository."); } final LoadingScope loadingScope = backup.getLoadingScope(); final OrgUnit orgUnit = orgUnitDAO.findById(backup.getOrgUnitId()); final User user = userDAO.findById(userId); // -- // Controls. // -- if (orgUnit == null) { throw new IllegalArgumentException("Cannot find OrgUnit with id #" + backup.getOrgUnitId() + "."); } if (user == null) { throw new IllegalArgumentException("Cannot find User with id #" + userId + "."); } if (!Handlers.isOrgUnitVisible(orgUnit, user)) { throw new UnsupportedOperationException("OrgUnit #" + orgUnit.getId() + " is not visible to user #" + userId + "."); } // -- // Starts repository building. // -- final FolderElement root = new FolderElement("root", "root"); final Set<OrgUnit> orgUnitTree = new HashSet<OrgUnit>(); Handlers.crawlUnits(orgUnit, orgUnitTree, true); // Retrieves values for OrgUnits full tree. final List<Value> values = valueDAO.findValuesForOrgUnits(orgUnitTree); for (final Value value : values) { if (!(value.getElement() instanceof FilesListElement)) { // Not a file element. continue; } final Project ud = projectDAO.findById(value.getContainerId()); final OrgUnit o; if (ud != null) { // There is only one partner. o = ud.getPartners().iterator().next(); } else { // Container is an OrgUnit. o = orgUnitDAO.findById(value.getContainerId()); } final List<FileVersion> versions = fileDAO.findVersions(ValueResultUtils.splitValuesAsInteger(value.getValue()), loadingScope); FolderElement orgUnitRepository = (FolderElement) root.getById("o" + o.getId()); if (orgUnitRepository == null) { orgUnitRepository = new FolderElement("o" + o.getId(), validateFileName(o.getFullName())); root.appendChild(orgUnitRepository); } FolderElement fileFolderElementParent = null; if (ud != null) { fileFolderElementParent = (FolderElement) orgUnitRepository.getById("p" + ud.getId()); if (fileFolderElementParent == null) { fileFolderElementParent = new FolderElement("p" + ud.getId(), validateFileName(ud.getFullName())); orgUnitRepository.appendChild(fileFolderElementParent); } } else { fileFolderElementParent = orgUnitRepository; } for (final FileVersion version : versions) { if (loadingScope == LoadingScope.ALL_VERSIONS) { FolderElement fileRepository = (FolderElement) fileFolderElementParent.getById("f" + version.getParentFile().getId()); if (fileRepository == null) { fileRepository = new FolderElement("f" + version.getParentFile().getId(), validateFileName(version.getParentFile().getName()) + "_f" + version.getParentFile().getId()); fileFolderElementParent.appendChild(fileRepository); } final FileElement file = new FileElement("fv" + version.getId(), validateFileName(version.getName()) + "_v" + version.getId() + "." + version.getExtension(), version.getPath()); fileRepository.appendChild(file); } else if (loadingScope == LoadingScope.LAST_VERSION) { final FileElement file = new FileElement("f" + version.getId(), validateFileName(version.getName()) + "_f" + version.getId() + "." + version.getExtension(), version.getPath()); fileFolderElementParent.appendChild(file); } } } return root; } /** * <p> * Recursively browses the given {@code root} repository elements to populate the given {@code zipOutputStream} with * corresponding files. * </p> * <p> * If a referenced file cannot be found in the storage folder, it will be ignored (a {@code WARN} log is generated). * </p> * * @param root * The root repository element. * @param zipOutputStream * The stream to populate with files hierarchy. * @param actualPath * The current repository path. */ private void zipRepository(final RepositoryElement root, final ZipArchiveOutputStream zipOutputStream, final String actualPath) { final String path = (actualPath.equals("") ? root.getName() : actualPath + "/" + root.getName()); if (root instanceof FileElement) { final FileElement file = (FileElement) root; final String fileStorageId = file.getStorageId(); if(fileStorageProvider.exists(fileStorageId)) { try (final InputStream is = new BufferedInputStream(fileStorageProvider.open(fileStorageId), ResponseHelper.BUFFER_SIZE)) { zipOutputStream.putArchiveEntry(new ZipArchiveEntry(path)); final byte data[] = new byte[ResponseHelper.BUFFER_SIZE]; while ((is.read(data)) != -1) { zipOutputStream.write(data); } zipOutputStream.closeArchiveEntry(); } catch (final IOException e) { LOG.warn("File '" + fileStorageId + "' cannot be found ; continuing with next file.", e); } } else { LOG.warn("File '{0}' does not exists on the server ; continuing with next file.", fileStorageId); } } else if (root instanceof FolderElement) { final FolderElement folder = (FolderElement) root; for (final RepositoryElement element : folder.getChildren()) { zipRepository(element, zipOutputStream, path); } } } /** * Deletes the {@code "C:\fakepath\"} string from given {@code fileName} (which comes from an issue in Google Chrome). * It also replaces all wrong characters that can't be displayed in a file name or a directory name by "{@code _}". * * @param fileName * The file name to validate. * @return The validated file name. */ private static String validateFileName(final String fileName) { return fileName.replaceFirst("[cC]:\\\\fakepath\\\\", "").replaceAll("[\\/:*?\"<>|]", "_"); } }