/** * Copyright (c) Codice Foundation * <p/> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p/> * 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 * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package org.codice.ddf.commands.catalog; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Paths; import java.time.Instant; import java.util.Collections; import java.util.HashMap; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.apache.commons.io.FilenameUtils; import org.apache.karaf.shell.api.action.Argument; import org.apache.karaf.shell.api.action.Command; import org.apache.karaf.shell.api.action.Option; import org.apache.karaf.shell.api.action.lifecycle.Reference; import org.apache.karaf.shell.api.action.lifecycle.Service; import org.codice.ddf.catalog.transformer.zip.ZipValidator; import org.codice.ddf.commands.util.CatalogCommandRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.io.ByteSource; import ddf.catalog.content.StorageProvider; import ddf.catalog.content.data.ContentItem; import ddf.catalog.content.data.impl.ContentItemImpl; import ddf.catalog.content.operation.impl.CreateStorageRequestImpl; import ddf.catalog.data.Metacard; import ddf.catalog.operation.impl.CreateRequestImpl; import ddf.catalog.transform.CatalogTransformerException; import ddf.catalog.transform.InputTransformer; import ddf.security.common.audit.SecurityLogger; /** * Imports Metacards, History, and their content from a zip file into the catalog. * <b> This code is experimental. While this interface is functional and tested, it may change or be * removed in a future version of the library. </b> */ @Service @Command(scope = CatalogCommands.NAMESPACE, name = "import", description = "Imports Metacards and history into the current Catalog") public class ImportCommand extends CatalogCommands { private static final Logger LOGGER = LoggerFactory.getLogger(ImportCommand.class); private static final int ID = 2; private static final int TYPE = 3; private static final int NAME = 4; private static final int DERIVED_NAME = 5; @Reference private StorageProvider storageProvider; @Argument(name = "Import File", description = "The file to import", index = 0, multiValued = false, required = true) String importFile; @Option(name = "--skip-signature-verification", required = false, multiValued = false, description = "Exports the data but does NOT sign the resulting zip file. " + "WARNING: This file will not be able to be verified on import for integrity and authenticity.") boolean unsafe = false; @Option(name = "--force", required = false, aliases = { "-f"}, multiValued = false, description = "Do not prompt") boolean force = false; @Override protected Object executeWithSubject() throws Exception { int metacards = 0; int content = 0; int derivedContent = 0; ZipValidator zipValidator = initZipValidator(); File file = initImportFile(importFile); InputTransformer transformer = getServiceByFilter(InputTransformer.class, String.format("(%s=%s)", "id", DEFAULT_TRANSFORMER_ID)).orElseThrow(() -> new CatalogCommandRuntimeException( "Could not get " + DEFAULT_TRANSFORMER_ID + " input transformer")); if (unsafe) { if (!force) { console.println( "This will import data with no check to see if data is modified/corrupt. Do you wish to continue?"); String input = getUserInputModifiable().toString(); if (!input.matches("^[yY][eE]?[sS]?$")) { console.println("ABORTED IMPORT."); return null; } } SecurityLogger.audit("Skipping validation check of imported data. There are no " + "guarantees of integrity or authenticity of the imported data." + "File being imported: {}", importFile); } else { if (!zipValidator.validateZipFile(importFile)) { throw new CatalogCommandRuntimeException("Signature on zip file is not valid"); } } SecurityLogger.audit("Called catalog:import command on the file: {}", importFile); console.println("Importing file"); Instant start = Instant.now(); try (InputStream fis = new FileInputStream(file); ZipInputStream zipInputStream = new ZipInputStream(fis)) { ZipEntry entry = zipInputStream.getNextEntry(); while (entry != null) { String filename = entry.getName(); if (filename.startsWith("META-INF")) { entry = zipInputStream.getNextEntry(); continue; } String[] pathParts = filename.split("\\" + File.separator); if (pathParts.length < 5) { console.println("Entry is not valid! " + filename); entry = zipInputStream.getNextEntry(); continue; } String id = pathParts[ID]; String type = pathParts[TYPE]; switch (type) { case "metacard": { metacards++; String metacardName = pathParts[NAME]; Metacard metacard = null; try { metacard = transformer.transform(new UncloseableBufferedInputStreamWrapper( zipInputStream), id); } catch (IOException | CatalogTransformerException e) { LOGGER.debug("Could not transform metacard: {}", id); } catalogProvider.create(new CreateRequestImpl(metacard)); break; } case "content": { content++; String contentFilename = pathParts[NAME]; ContentItem contentItem = new ContentItemImpl(id, new ZipEntryByteSource(new UncloseableBufferedInputStreamWrapper( zipInputStream)), null, contentFilename, entry.getSize(), null); CreateStorageRequestImpl createStorageRequest = new CreateStorageRequestImpl( Collections.singletonList(contentItem), id, new HashMap<>()); storageProvider.create(createStorageRequest); storageProvider.commit(createStorageRequest); break; } case "derived": { derivedContent++; String qualifier = pathParts[NAME]; String derivedContentName = pathParts[DERIVED_NAME]; ContentItem contentItem = new ContentItemImpl(id, qualifier, new ZipEntryByteSource(new UncloseableBufferedInputStreamWrapper( zipInputStream)), null, derivedContentName, entry.getSize(), null); CreateStorageRequestImpl createStorageRequest = new CreateStorageRequestImpl( Collections.singletonList(contentItem), id, new HashMap<>()); storageProvider.create(createStorageRequest); storageProvider.commit(createStorageRequest); break; } default: { LOGGER.debug("Cannot interpret type of {}", type); } } entry = zipInputStream.getNextEntry(); } } catch (Exception e) { printErrorMessage(String.format("Exception while importing metacards (%s)%nFor more information set the log level to INFO (log:set INFO org.codice.ddf.commands.catalog) ", e.getMessage())); LOGGER.info("Exception while importing metacards", e); throw e; } console.println("File imported successfully. Imported in: " + getFormattedDuration(start)); console.println("Number of metacards imported: " + metacards); console.println("Number of content imported: " + content); console.println("Number of derived content imported: " + derivedContent); return null; } private File initImportFile(String importFile) { File file = new File(importFile); if (!file.exists()) { throw new CatalogCommandRuntimeException("File does not exist: " + importFile); } if (!FilenameUtils.isExtension(importFile, "zip")) { throw new CatalogCommandRuntimeException("File must be a zip file: " + importFile); } return file; } private ZipValidator initZipValidator() { ZipValidator zipValidator = new ZipValidator(); zipValidator.setSignaturePropertiesPath(Paths.get(System.getProperty("ddf.home"), "/etc/ws-security/server/signature.properties") .toString()); zipValidator.init(); return zipValidator; } private static class ZipEntryByteSource extends ByteSource { private InputStream input; private ZipEntryByteSource(InputStream input) { this.input = input; } @Override public InputStream openStream() throws IOException { return input; } } /** * Identical to BufferedInputStream with the exception that it does not close the underlying * resource stream when the close() method is called. The buffer is still emptied when closed. * <br/> * This is useful for cases when the inputstream being consumed belongs to something that * should not be closed. */ private static class UncloseableBufferedInputStreamWrapper extends BufferedInputStream { private static final AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> BUF_UPDATER = AtomicReferenceFieldUpdater.newUpdater(BufferedInputStream.class, byte[].class, "buf"); public UncloseableBufferedInputStreamWrapper(InputStream in) { super(in); } @Override public void close() throws IOException { byte[] buffer; while ((buffer = buf) != null) { if (BUF_UPDATER.compareAndSet(this, buffer, null)) { InputStream input = in; in = null; // Purposely do not close `input` return; } // Else retry in case a new buf was CASed in fill() } } } }