/** * 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 ddf.content.provider.filesystem; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import javax.activation.MimeType; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.LoggerFactory; import org.slf4j.ext.XLogger; import ddf.content.data.ContentItem; import ddf.content.operation.CreateRequest; import ddf.content.operation.CreateResponse; import ddf.content.operation.DeleteRequest; import ddf.content.operation.DeleteResponse; import ddf.content.operation.ReadRequest; import ddf.content.operation.ReadResponse; import ddf.content.operation.UpdateRequest; import ddf.content.operation.UpdateResponse; import ddf.content.operation.impl.CreateResponseImpl; import ddf.content.operation.impl.DeleteResponseImpl; import ddf.content.operation.impl.ReadResponseImpl; import ddf.content.operation.impl.UpdateResponseImpl; import ddf.content.storage.StorageException; import ddf.content.storage.StorageProvider; import ddf.mime.MimeTypeMapper; /** * The File System Provider provides the implementation to create/update/delete content items as * files in the DDF Content Repository. The File System Provider is an implementation of the * {@link StorageProvider} interface. When installed, it is invoked by the @link{ContentFramework} to * create, update, or delete a file in the DDF Content Repository, which is * located in the <code><DDF_INSTALL_DIR>/content/store directory</code>. */ public class FileSystemProvider implements StorageProvider { public static final String CONTENT_URI_PREFIX = "content:"; public static final String DEFAULT_CONTENT_REPOSITORY = "content" + File.separator + "store"; private static final String DEFAULT_MIME_TYPE = "application/octet-stream"; /** * Optional id parameter for mime type, e.g., application/json;id=geojson */ private static final String ID_PARAMETER = "id"; private static final XLogger LOGGER = new XLogger(LoggerFactory.getLogger(FileSystemProvider.class)); /** * Mapper for file extensions-to-mime types (and vice versa) */ private MimeTypeMapper mimeTypeMapper; /** * Root directory for entire content repository */ private String baseContentDirectory; /** * Default constructor, invoked by blueprint. */ public FileSystemProvider() { LOGGER.info("File System Provider initializing..."); } @Override public CreateResponse create(CreateRequest createRequest) throws StorageException { LOGGER.trace("ENTERING: create"); ContentItem item = createRequest.getContentItem(); ContentItem createdItem = null; try { // Create the root directory for entire content repository if does not // already exist. File parentDir = new File(baseContentDirectory); if (!parentDir.exists() && !parentDir.mkdirs()) { throw new IOException("Error creating content file system root directory"); } createdItem = generateContentFile(item); } catch (IOException e) { throw new StorageException(e); } CreateResponse response = new CreateResponseImpl(createRequest, createdItem); LOGGER.trace("EXITING: create"); return response; } @Override public ReadResponse read(ReadRequest readRequest) throws StorageException { LOGGER.trace("ENTERING: read"); String id = readRequest.getId(); File file = getFileForContentId(id); LOGGER.debug("Reading file {} from directory {}", file.getName(), id); String extension = FilenameUtils.getExtension(file.getName()); String mimeType = DEFAULT_MIME_TYPE; try { mimeType = mimeTypeMapper.guessMimeType(new FileInputStream(file), extension); } catch (Exception e) { LOGGER.warn("Could not determine mime type for file extension = {}; defaulting to {}", extension, DEFAULT_MIME_TYPE); } LOGGER.debug("mimeType = {}", mimeType); ContentFile returnItem = new ContentFile(file, id, mimeType); ReadResponse response = new ReadResponseImpl(readRequest, returnItem); LOGGER.trace("EXITING: read"); return response; } @Override public UpdateResponse update(UpdateRequest updateRequest) throws StorageException { LOGGER.trace("ENTERING: update"); ContentItem item = updateRequest.getContentItem(); ContentItem updatedItem = null; LOGGER.debug("Updating item with id = {}", item.getId()); try { updatedItem = updateContentFile(item); } catch (IOException e) { throw new StorageException(e); } UpdateResponse response = new UpdateResponseImpl(updateRequest, updatedItem); LOGGER.trace("EXITING: update"); return response; } @Override public DeleteResponse delete(DeleteRequest deleteRequest) throws StorageException { LOGGER.trace("ENTERING: delete"); ContentItem itemToBeDeleted = deleteRequest.getContentItem(); String id = itemToBeDeleted.getId(); boolean isDeleted = false; LOGGER.debug("File to be deleted: {}", id); File fileToBeDeleted = getFileForContentId(id); if (!fileToBeDeleted.exists()) { throw new StorageException("File doesn't exist for id: " + id); } ContentItem deletedContentItem = null; if (!fileToBeDeleted.isDirectory()) { isDeleted = fileToBeDeleted.delete(); if (!isDeleted) { throw new StorageException("Could not delete file: " + id); } else { // Delete parent directory (identified by contentId) since it is now empty // (always only one file per GUID directory) and will never be used again. File dirToBeDeleted = getDirectoryForContentId(id); try { FileUtils.deleteDirectory(dirToBeDeleted); } catch (IOException e) { LOGGER.info("Unable to delete directory {} for id = {}", dirToBeDeleted.getAbsolutePath(), id); } deletedContentItem = new ContentFile(null, id, itemToBeDeleted.getMimeTypeRawData()); String contentUri = CONTENT_URI_PREFIX + deletedContentItem.getId(); LOGGER.debug("contentUri = {}", contentUri); deletedContentItem.setUri(contentUri); } } else { throw new StorageException("Invalid ID. Cannot delete directory."); } DeleteResponse response = new DeleteResponseImpl(deleteRequest, deletedContentItem, isDeleted); LOGGER.trace("EXITING: delete"); return response; } private ContentItem generateContentFile(ContentItem item) throws IOException, StorageException { LOGGER.trace("ENTERING: generateContentFile"); String mimeType = getMimeType(item.getMimeType()); String fileId = item.getId(); if (StringUtils.isNotEmpty(item.getFilename())) { fileId += File.separator + item.getFilename(); } LOGGER.debug( "itemId = " + item.getId() + ", mimeType = " + mimeType + ", itemFilename = " + item.getFilename()); LOGGER.debug("fileId = {}", fileId); File createdFile = createFile(fileId); FileUtils.copyInputStreamToFile(item.getInputStream(), createdFile); ContentItem contentItem = new ContentFile(createdFile, item.getId(), item.getMimeTypeRawData(), item.getFilename()); String contentUri = CONTENT_URI_PREFIX + contentItem.getId(); LOGGER.debug("contentUri = {}", contentUri); contentItem.setUri(contentUri); LOGGER.trace("EXITING: generateContentFile"); return contentItem; } private ContentItem updateContentFile(ContentItem item) throws IOException, StorageException { LOGGER.trace("ENTERING: updateContentFile"); String fileId = item.getId(); LOGGER.debug("File ID: {}", fileId); File fileToUpdate = getFileForContentId(fileId); ContentItem contentItem = null; if (fileToUpdate.exists()) { FileUtils.copyInputStreamToFile(item.getInputStream(), fileToUpdate); contentItem = new ContentFile(fileToUpdate, item.getId(), item.getMimeTypeRawData()); String contentUri = CONTENT_URI_PREFIX + contentItem.getId(); LOGGER.debug("contentUri = {}", contentUri); contentItem.setUri(contentUri); LOGGER.debug("updated file length = " + contentItem.getSize()); } else { String msg = "Unable to update - Content Item does not exist with id " + item.getId() + " (fileId = " + fileId + ")"; LOGGER.debug(msg); throw new StorageException(msg); } LOGGER.trace("EXITING: updateContentFile"); return contentItem; } private File createFile(final String newFileID) throws IOException { LOGGER.trace("ENTERING: createFile"); File file = getFileFromContentRepository(newFileID); if (file == null) { throw new IOException("Error getting file: " + newFileID); } // create directories File directory = file.getParentFile(); if (directory == null || (!directory.exists() && !directory.mkdirs())) { throw new IOException("Error creating directory structure to save file."); } // create file and write to it, if it doesn't already exist if (!file.exists() && !file.createNewFile()) { throw new IOException("Error creating file: " + file.getAbsolutePath()); } LOGGER.trace("EXITING: createFile"); return file; } private File getFileFromContentRepository(String id) { LOGGER.trace("ENTERING: getFileFromContentRepository"); LOGGER.debug("id = {}", id); File baseURIFile = null; if (StringUtils.isNotEmpty(id)) { // Normalize and concatenate the paths String filepath = FilenameUtils.concat(baseContentDirectory, removeSlashPrefix(id)); LOGGER.debug("Full filepath = {}", filepath); baseURIFile = new File(filepath); } else { LOGGER.warn( "Could not obtain reference to content item. Possibly invalid content id: {}", id); } LOGGER.trace("EXITING: getFileFromContentRepository"); return baseURIFile; } private String removeSlashPrefix(final String path) { String newPath = path; char firstChar = path.charAt(0); if (firstChar == '/' || firstChar == '\\') { newPath = path.substring(1); } return newPath; } public MimeTypeMapper getMimeTypeMapper() { return mimeTypeMapper; } public void setMimeTypeMapper(MimeTypeMapper mimeTypeMapper) { this.mimeTypeMapper = mimeTypeMapper; } public String getBaseContentDirectory() { return baseContentDirectory; } public void setBaseContentDirectory(final String baseDirectory) { String newBaseDir = ""; if (!baseDirectory.isEmpty()) { String path = FilenameUtils.normalize(baseDirectory); File directory = new File(path); // Create the directory if it doesn't exist if ((!directory.exists() && directory.mkdirs()) || (directory.isDirectory() && directory .canRead())) { LOGGER.info("Setting base content directory to: {}", path); newBaseDir = path; } } // if invalid baseDirectory was provided or baseDirectory is // an empty string, default to the DEFAULT_CONTENT_REPOSITORY in <karaf.home> if (newBaseDir.isEmpty()) { try { final File karafHomeDir = new File(System.getProperty("karaf.home")); if (karafHomeDir.isDirectory()) { final File fspDir = new File( karafHomeDir + File.separator + DEFAULT_CONTENT_REPOSITORY); // if directory does not exist, try to create it if (fspDir.isDirectory() || fspDir.mkdirs()) { LOGGER.info("Setting base content directory to: {}", fspDir.getAbsolutePath()); newBaseDir = fspDir.getAbsolutePath(); } else { LOGGER.warn( "Unable to create FileSystemProvider folder: {}. Please check that DDF has permissions to create this folder. Using default folder.", fspDir.getAbsolutePath()); } } else { LOGGER.warn( "Karaf home folder defined by system property karaf.home is not a directory. Using default folder."); } } catch (NullPointerException npe) { LOGGER.warn( "Unable to create FileSystemProvider folder - karaf.home system property not defined. Using default folder."); } } this.baseContentDirectory = newBaseDir; LOGGER.debug("Set base content directory to: {}", this.baseContentDirectory); } private String getMimeType(MimeType mimeType) { LOGGER.trace("ENTERING: getMimeType"); String mimeTypeStr = mimeType.getBaseType(); String mimeTypeIdValue = mimeType.getParameter(ID_PARAMETER); if (StringUtils.isNotEmpty(mimeTypeIdValue)) { mimeTypeStr += ";id=" + mimeTypeIdValue; } LOGGER.debug("mimeTypeStr = {}", mimeTypeStr); LOGGER.trace("EXITING: getMimeType"); return mimeTypeStr; } private File getDirectoryForContentId(String contentId) throws StorageException { LOGGER.trace("ENTERING: getDirectoryForContentId"); LOGGER.debug("contentId = {}", contentId); File dir = getFileFromContentRepository(contentId); if (dir == null || !dir.exists() || !dir.isDirectory()) { throw new StorageException( "Directory does not exist in content repository with id = " + contentId); } LOGGER.trace("EXITING: getDirectoryForContentId"); return dir; } private File getFileForContentId(String contentId) throws StorageException { LOGGER.trace("ENTERING: getFileFromContentId"); LOGGER.debug("contentId = {}", contentId); File dir = getDirectoryForContentId(contentId); File[] files = dir.listFiles(); if (files == null || files.length == 0) { throw new StorageException("No files in directory " + contentId); } else if (files.length > 1) { throw new StorageException("More than one file in directory " + contentId + " - cannot determine which file to work on."); } LOGGER.trace("EXITING: getFileFromContentId"); return files[0]; } }