/** * The contents of this file are subject to the license and copyright * detailed in the LICENSE file at the root of the source * tree and available online at * * https://github.com/keeps/roda */ package org.roda.core.plugins.plugins.base; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.io.IOUtils; import org.roda.core.common.ConsumesOutputStream; import org.roda.core.common.DownloadUtils; import org.roda.core.data.common.RodaConstants; import org.roda.core.data.common.RodaConstants.ExportType; import org.roda.core.data.common.RodaConstants.PreservationEventType; import org.roda.core.data.exceptions.AlreadyExistsException; import org.roda.core.data.exceptions.AuthorizationDeniedException; import org.roda.core.data.exceptions.GenericException; import org.roda.core.data.exceptions.InvalidParameterException; import org.roda.core.data.exceptions.JobException; import org.roda.core.data.exceptions.NotFoundException; import org.roda.core.data.exceptions.RequestNotValidException; import org.roda.core.data.v2.LiteOptionalWithCause; import org.roda.core.data.v2.ip.AIP; import org.roda.core.data.v2.ip.AIPState; import org.roda.core.data.v2.ip.StoragePath; import org.roda.core.data.v2.jobs.Job; import org.roda.core.data.v2.jobs.PluginParameter; import org.roda.core.data.v2.jobs.PluginParameter.PluginParameterType; import org.roda.core.data.v2.jobs.PluginType; import org.roda.core.data.v2.jobs.Report; import org.roda.core.data.v2.jobs.Report.PluginState; import org.roda.core.data.v2.validation.ValidationException; import org.roda.core.index.IndexService; import org.roda.core.model.ModelService; import org.roda.core.model.utils.ModelUtils; import org.roda.core.plugins.AbstractPlugin; import org.roda.core.plugins.Plugin; import org.roda.core.plugins.PluginException; import org.roda.core.plugins.orchestrate.SimpleJobPluginInfo; import org.roda.core.plugins.plugins.PluginHelper; import org.roda.core.storage.DefaultStoragePath; import org.roda.core.storage.Directory; import org.roda.core.storage.StorageService; import org.roda.core.storage.fs.FSUtils; import org.roda.core.storage.fs.FileStorageService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ExportAIPPlugin extends AbstractPlugin<AIP> { private static final Logger LOGGER = LoggerFactory.getLogger(ExportAIPPlugin.class); public static final String PLUGIN_PARAM_EXPORT_FOLDER_PARAMETER = "outputFolder"; public static final String PLUGIN_PARAM_EXPORT_TYPE = "exportType"; public static final String PLUGIN_PARAM_EXPORT_REMOVE_IF_ALREADY_EXISTS = "removeIfAlreadyExists"; private String outputFolder; private ExportType exportType; private boolean removeIfAlreadyExists; private static Map<String, PluginParameter> pluginParameters = new HashMap<>(); static { pluginParameters.put(PLUGIN_PARAM_EXPORT_FOLDER_PARAMETER, new PluginParameter(PLUGIN_PARAM_EXPORT_FOLDER_PARAMETER, "Destination folder", PluginParameterType.STRING, "/tmp/export", true, false, "Folder where the exported AIPs will be stored.")); pluginParameters.put(PLUGIN_PARAM_EXPORT_TYPE, new PluginParameter(PLUGIN_PARAM_EXPORT_TYPE, "Type of export", PluginParameterType.STRING, "FOLDER", true, false, "Type of export: ZIP – exports each AIP as a ZIP file; FOLDER – exports each AIP as a folder.")); pluginParameters.put(PLUGIN_PARAM_EXPORT_REMOVE_IF_ALREADY_EXISTS, new PluginParameter(PLUGIN_PARAM_EXPORT_REMOVE_IF_ALREADY_EXISTS, "Overwrite files/folders", PluginParameterType.BOOLEAN, "true", true, false, "Overwrites files and folders if they already exist on the destination folder.")); } @Override public void init() throws PluginException { // do nothing } @Override public void shutdown() { // do nothing } @Override public String getName() { return "AIP batch export"; } @Override public String getDescription() { return "Exports selected AIP(s) to a ZIP file or folder on the server file system. To retrieve the results of the export action you must " + "have access to the server file system.\nNOTE: This action can potentially generate a large amount of data. Make sure you select a destination " + "folder that has enough storage space to accommodate the results of the export action."; } @Override public String getVersionImpl() { return "1.0"; } @Override public List<PluginParameter> getParameters() { ArrayList<PluginParameter> parameters = new ArrayList<>(); parameters.add(pluginParameters.get(PLUGIN_PARAM_EXPORT_FOLDER_PARAMETER)); parameters.add(pluginParameters.get(PLUGIN_PARAM_EXPORT_TYPE)); parameters.add(pluginParameters.get(PLUGIN_PARAM_EXPORT_REMOVE_IF_ALREADY_EXISTS)); return parameters; } @Override public void setParameterValues(Map<String, String> parameters) throws InvalidParameterException { super.setParameterValues(parameters); if (parameters.containsKey(PLUGIN_PARAM_EXPORT_FOLDER_PARAMETER)) { outputFolder = parameters.get(PLUGIN_PARAM_EXPORT_FOLDER_PARAMETER); } if (parameters.containsKey(PLUGIN_PARAM_EXPORT_REMOVE_IF_ALREADY_EXISTS)) { removeIfAlreadyExists = Boolean .parseBoolean(getParameterValues().get(PLUGIN_PARAM_EXPORT_REMOVE_IF_ALREADY_EXISTS)); } if (parameters.containsKey(PLUGIN_PARAM_EXPORT_TYPE)) { try { exportType = ExportType.valueOf(parameters.get(PLUGIN_PARAM_EXPORT_TYPE)); } catch (Exception e) { LOGGER.error(e.getMessage(), e); exportType = ExportType.FOLDER; } } } @Override public Report execute(IndexService index, ModelService model, StorageService storage, List<LiteOptionalWithCause> liteList) throws PluginException { // FIXME 20170113 hsilva: see how to put this plugin using // PluginHelper.processObjects Report report = PluginHelper.initPluginReport(this); try { SimpleJobPluginInfo jobPluginInfo = PluginHelper.getInitialJobInformation(this, liteList.size()); PluginHelper.updateJobInformation(this, jobPluginInfo); Job job = PluginHelper.getJob(this, model); List<AIP> aips = PluginHelper.transformLitesIntoObjects(model, this, report, jobPluginInfo, liteList, job); Path outputPath = Paths.get(outputFolder); String error = null; try { if (!FSUtils.exists(outputPath)) { Files.createDirectories(outputPath); } if (!Files.isWritable(outputPath)) { error = "No permissions to write to " + outputPath.toString(); } } catch (IOException e) { LOGGER.error("Error creating base folder: " + e.getMessage()); error = e.getMessage(); } if (error == null && exportType == ExportType.ZIP) { report = exportMultiZip(aips, outputPath, report, model, index, storage, jobPluginInfo, job); } else if (error == null && exportType == ExportType.FOLDER) { report = exportFolders(aips, storage, model, index, report, jobPluginInfo, job); } else if (error != null) { jobPluginInfo.incrementObjectsProcessedWithFailure(aips.size()); report.setCompletionPercentage(100); report.setPluginState(PluginState.FAILURE); report.setPluginDetails("Error exporting AIPs: " + error); } jobPluginInfo.finalizeInfo(); PluginHelper.updateJobInformation(this, jobPluginInfo); } catch (JobException | AuthorizationDeniedException | NotFoundException | GenericException | RequestNotValidException e) { LOGGER.error("Could not update Job information"); } return report; } private Report exportFolders(List<AIP> aips, StorageService storage, ModelService model, IndexService index, Report report, SimpleJobPluginInfo jobPluginInfo, Job job) { try { FileStorageService localStorage = new FileStorageService(Paths.get(outputFolder)); for (AIP aip : aips) { LOGGER.debug("Exporting AIP {} to folder", aip.getId()); String error = null; StoragePath aipPath = ModelUtils.getAIPStoragePath(aip.getId()); try { localStorage.copy(storage, aipPath, DefaultStoragePath.parse(aip.getId())); } catch (AlreadyExistsException e) { if (removeIfAlreadyExists) { try { localStorage.deleteResource(DefaultStoragePath.parse(aip.getId())); localStorage.copy(storage, aipPath, DefaultStoragePath.parse(aip.getId())); } catch (AlreadyExistsException e2) { error = "Error removing/creating folder " + aipPath.toString(); } } else { error = "Folder " + aipPath.toString() + " already exists."; } } Report reportItem = PluginHelper.initPluginReportItem(this, aip.getId(), AIP.class, AIPState.ACTIVE); if (error != null) { reportItem.setPluginState(PluginState.FAILURE) .setPluginDetails("Export AIP did not end successfully: " + error); jobPluginInfo.incrementObjectsProcessedWithFailure(); } else { reportItem.setPluginState(PluginState.SUCCESS).setPluginDetails("Export AIP ended successfully"); jobPluginInfo.incrementObjectsProcessedWithSuccess(); } report.addReport(reportItem); PluginHelper.updatePartialJobReport(this, model, reportItem, true, job); try { boolean notify = true; PluginHelper.createPluginEvent(this, aip.getId(), model, index, reportItem.getPluginState(), "", notify); } catch (ValidationException | RequestNotValidException | NotFoundException | GenericException | AuthorizationDeniedException | AlreadyExistsException e) { LOGGER.error("Error creating event: " + e.getMessage(), e); } } } catch (GenericException | RequestNotValidException | AuthorizationDeniedException | NotFoundException e) { LOGGER.error(e.getMessage(), e); } return report; } private Report exportMultiZip(List<AIP> aips, Path outputPath, Report report, ModelService model, IndexService index, StorageService storage, SimpleJobPluginInfo jobPluginInfo, Job job) { for (AIP aip : aips) { LOGGER.debug("Exporting AIP {} to ZIP", aip.getId()); OutputStream os = null; String error = null; try { Path zip = outputPath.resolve(aip.getId() + ".zip"); if (FSUtils.exists(zip) && removeIfAlreadyExists) { Files.delete(zip); } else if (FSUtils.exists(zip) && !removeIfAlreadyExists) { error = "File " + zip.toString() + " already exists"; } if (error == null) { os = Files.newOutputStream(zip, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); Directory directory = storage.getDirectory(ModelUtils.getAIPStoragePath(aip.getId())); ConsumesOutputStream cos = DownloadUtils.download(storage, directory); cos.consumeOutputStream(os); } } catch (Exception e) { LOGGER.error("Error exporting AIP " + aip.getId() + ": " + e.getMessage()); error = e.getMessage(); } finally { if (os != null) { IOUtils.closeQuietly(os); } } Report reportItem = PluginHelper.initPluginReportItem(this, aip.getId(), AIP.class, AIPState.ACTIVE); if (error != null) { reportItem.setPluginState(PluginState.FAILURE) .setPluginDetails("Export AIP did not end successfully: " + error); jobPluginInfo.incrementObjectsProcessedWithFailure(); } else { reportItem.setPluginState(PluginState.SUCCESS).setPluginDetails("Export AIP ended successfully"); jobPluginInfo.incrementObjectsProcessedWithSuccess(); } report.addReport(reportItem); PluginHelper.updatePartialJobReport(this, model, reportItem, true, job); } return report; } @Override public Report beforeAllExecute(IndexService index, ModelService model, StorageService storage) throws PluginException { return new Report(); } @Override public Report afterAllExecute(IndexService index, ModelService model, StorageService storage) throws PluginException { return new Report(); } @Override public Plugin<AIP> cloneMe() { return new ExportAIPPlugin(); } @Override public PluginType getType() { return PluginType.AIP_TO_AIP; } @Override public boolean areParameterValuesValid() { return true; } @Override public PreservationEventType getPreservationEventType() { return PreservationEventType.MIGRATION; } @Override public String getPreservationEventDescription() { return "Exports AIPS to a local folder"; } @Override public String getPreservationEventSuccessMessage() { return "The AIPs were successfully exported"; } @Override public String getPreservationEventFailureMessage() { return "The AIPs were not exported"; } @Override public List<String> getCategories() { return Arrays.asList(RodaConstants.PLUGIN_CATEGORY_MANAGEMENT); } @Override public List<Class<AIP>> getObjectClasses() { return Arrays.asList(AIP.class); } }