/** * 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.storage.fs; import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Date; import java.util.Iterator; import java.util.Map; import org.apache.commons.io.IOUtils; import org.roda.core.common.iterables.CloseableIterable; import org.roda.core.data.common.RodaConstants; 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.NotFoundException; import org.roda.core.data.exceptions.RequestNotValidException; import org.roda.core.data.utils.JsonUtils; import org.roda.core.data.v2.ip.StoragePath; import org.roda.core.storage.Binary; import org.roda.core.storage.BinaryVersion; import org.roda.core.storage.Container; import org.roda.core.storage.ContentPayload; import org.roda.core.storage.DefaultBinary; import org.roda.core.storage.DefaultBinaryVersion; import org.roda.core.storage.DefaultContainer; import org.roda.core.storage.DefaultDirectory; import org.roda.core.storage.DirectResourceAccess; import org.roda.core.storage.Directory; import org.roda.core.storage.EmptyClosableIterable; import org.roda.core.storage.Entity; import org.roda.core.storage.Resource; import org.roda.core.storage.StorageService; import org.roda.core.storage.StorageServiceUtils; import org.roda.core.util.IdUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Class that persists binary files and their containers in the File System. * * <p> * 20160718 hsilva: it has been decided that all Filesystem Storage Service * delete methods would not effectively delete files/folders but instead move * them to a 'trash' folder with the same folder structure * </p> * * @author Luis Faria <lfaria@keep.pt> * @author Hélder Silva <hsilva@keep.pt> */ public class FileStorageService implements StorageService { private static final Logger LOGGER = LoggerFactory.getLogger(FileStorageService.class); public static final String HISTORY_SUFFIX = "-history"; private static final String HISTORY_DATA_FOLDER = "data"; private static final String HISTORY_METADATA_FOLDER = "metadata"; private final Path rodaDataPath; private final Path basePath; private final Path historyPath; private final Path historyDataPath; private final Path historyMetadataPath; private final Path trashPath; public FileStorageService(Path basePath, String trashDirName) throws GenericException { this.basePath = basePath; rodaDataPath = this.basePath.getParent(); historyPath = rodaDataPath.resolve(basePath.getFileName() + HISTORY_SUFFIX); historyDataPath = historyPath.resolve(HISTORY_DATA_FOLDER); historyMetadataPath = historyPath.resolve(HISTORY_METADATA_FOLDER); trashPath = rodaDataPath.resolve(trashDirName == null ? "trash" : trashDirName); initialize(basePath); initialize(historyPath); initialize(historyDataPath.resolve(RodaConstants.STORAGE_CONTAINER_AIP)); initialize(historyMetadataPath.resolve(RodaConstants.STORAGE_CONTAINER_AIP)); initialize(trashPath); } public FileStorageService(Path basePath) throws GenericException { this(basePath, null); } private void initialize(Path path) throws GenericException { if (!FSUtils.exists(path)) { try { Files.createDirectories(path); } catch (IOException e) { throw new GenericException("Could not create path " + path, e); } } else if (!FSUtils.isDirectory(path)) { throw new GenericException("Path is not a directory " + path); } else if (!Files.isReadable(path)) { throw new GenericException("Cannot read from path " + path); } else if (!Files.isWritable(path)) { throw new GenericException("Cannot write to path " + path); } else { // do nothing } } @Override public CloseableIterable<Container> listContainers() throws GenericException { return FSUtils.listContainers(basePath); } @Override public Container createContainer(StoragePath storagePath) throws GenericException, AlreadyExistsException { Path containerPath = FSUtils.getEntityPath(basePath, storagePath); Path directory = null; try { directory = Files.createDirectory(containerPath); return new DefaultContainer(storagePath); } catch (FileAlreadyExistsException e) { // cleanup try { FSUtils.deletePath(directory); } catch (NotFoundException e1) { LOGGER.warn("Error while trying to clean up", e1); } throw new AlreadyExistsException("Could not create container at " + containerPath, e); } catch (IOException e) { // cleanup try { FSUtils.deletePath(directory); } catch (NotFoundException e1) { LOGGER.warn("Error while trying to clean up", e1); } throw new GenericException("Could not create container at " + containerPath, e); } } @Override public Container getContainer(StoragePath storagePath) throws GenericException, RequestNotValidException, NotFoundException { if (!storagePath.isFromAContainer()) { throw new RequestNotValidException("Storage path is not from a container"); } Path containerPath = FSUtils.getEntityPath(basePath, storagePath); Container container; if (FSUtils.exists(containerPath)) { container = new DefaultContainer(storagePath); } else { throw new NotFoundException("Container not found: " + storagePath); } return container; } @Override public void deleteContainer(StoragePath storagePath) throws NotFoundException, GenericException { Path containerPath = FSUtils.getEntityPath(basePath, storagePath); trash(containerPath); // cleanup history deleteAllBinaryVersionsUnder(storagePath); } private void trash(Path fromPath) throws GenericException, NotFoundException { try { Path toPath = trashPath.resolve(rodaDataPath.relativize(fromPath)); LOGGER.debug("Moving to trash: {} to {}", fromPath, toPath); FSUtils.move(fromPath, toPath, true); } catch (AlreadyExistsException e) { String unique = IdUtils.createUUID(); Path uniqueToPath = trashPath.resolve(unique).resolve(rodaDataPath.relativize(fromPath)); try { LOGGER.debug("Re-trying to move to trash: {} to {}", fromPath, uniqueToPath); FSUtils.move(fromPath, uniqueToPath, true); } catch (AlreadyExistsException e1) { LOGGER.error("Error moving to trash: {} to {}", fromPath, uniqueToPath, e1); throw new GenericException("Unexpected exception while moving to trash", e1); } } } @Override public CloseableIterable<Resource> listResourcesUnderContainer(StoragePath storagePath, boolean recursive) throws NotFoundException, GenericException { Path path = FSUtils.getEntityPath(basePath, storagePath); if (recursive) { return FSUtils.recursivelyListPath(basePath, path); } else { return FSUtils.listPath(basePath, path); } } @Override public Long countResourcesUnderContainer(StoragePath storagePath, boolean recursive) throws NotFoundException, GenericException { Path path = FSUtils.getEntityPath(basePath, storagePath); if (recursive) { return FSUtils.recursivelyCountPath(path); } else { return FSUtils.countPath(path); } } @Override public Directory createDirectory(StoragePath storagePath) throws AlreadyExistsException, GenericException { Path dirPath = FSUtils.getEntityPath(basePath, storagePath); Path directory = null; if (FSUtils.exists(dirPath)) { throw new AlreadyExistsException("Could not create directory at " + dirPath); } try { directory = Files.createDirectories(dirPath); return new DefaultDirectory(storagePath); } catch (IOException e) { // cleanup try { FSUtils.deletePath(directory); } catch (NotFoundException | GenericException e1) { LOGGER.warn("Error while cleaning up", e1); } throw new GenericException("Could not create directory at " + dirPath, e); } } @Override public Directory createRandomDirectory(StoragePath parentStoragePath) throws RequestNotValidException, GenericException, NotFoundException, AlreadyExistsException { Path parentDirPath = FSUtils.getEntityPath(basePath, parentStoragePath); Path directory = null; try { directory = FSUtils.createRandomDirectory(parentDirPath); return new DefaultDirectory(FSUtils.getStoragePath(basePath, directory)); } catch (FileAlreadyExistsException e) { // cleanup FSUtils.deletePath(directory); throw new AlreadyExistsException("Could not create random directory under " + parentDirPath, e); } catch (IOException e) { // cleanup FSUtils.deletePath(directory); throw new GenericException("Could not create random directory under " + parentDirPath, e); } } @Override public Directory getDirectory(StoragePath storagePath) throws RequestNotValidException, NotFoundException, GenericException { if (storagePath.isFromAContainer()) { throw new RequestNotValidException("Invalid storage path for a directory: " + storagePath); } Path directoryPath = FSUtils.getEntityPath(basePath, storagePath); Resource resource = FSUtils.convertPathToResource(basePath, directoryPath); if (resource instanceof Directory) { return (Directory) resource; } else { throw new RequestNotValidException("Looking for a directory but found something else: " + storagePath); } } @Override public CloseableIterable<Resource> listResourcesUnderDirectory(StoragePath storagePath, boolean recursive) throws NotFoundException, GenericException { Path directoryPath = FSUtils.getEntityPath(basePath, storagePath); if (recursive) { return FSUtils.recursivelyListPath(basePath, directoryPath); } else { return FSUtils.listPath(basePath, directoryPath); } } @Override public Long countResourcesUnderDirectory(StoragePath storagePath, boolean recursive) throws NotFoundException, GenericException { Path directoryPath = FSUtils.getEntityPath(basePath, storagePath); if (recursive) { return FSUtils.recursivelyCountPath(directoryPath); } else { return FSUtils.countPath(directoryPath); } } @Override public Binary createBinary(StoragePath storagePath, ContentPayload payload, boolean asReference) throws GenericException, AlreadyExistsException { if (asReference) { throw new GenericException("Method not yet implemented"); } else { Path binPath = FSUtils.getEntityPath(basePath, storagePath); if (FSUtils.exists(binPath)) { throw new AlreadyExistsException("Binary already exists: " + binPath); } else { try { // ensuring parent exists Path parent = binPath.getParent(); if (!FSUtils.exists(parent)) { Files.createDirectories(parent); } // writing file payload.writeToPath(binPath); ContentPayload newPayload = new FSPathContentPayload(binPath); Long sizeInBytes = Files.size(binPath); boolean isReference = false; Map<String, String> contentDigest = null; return new DefaultBinary(storagePath, newPayload, sizeInBytes, isReference, contentDigest); } catch (FileAlreadyExistsException e) { throw new AlreadyExistsException("Binary already exists: " + binPath); } catch (IOException e) { throw new GenericException("Could not create binary", e); } } } } @Override public Binary createRandomBinary(StoragePath parentStoragePath, ContentPayload payload, boolean asReference) throws GenericException, RequestNotValidException { if (asReference) { throw new GenericException("Method not yet implemented"); } else { Path parent = FSUtils.getEntityPath(basePath, parentStoragePath); try { // ensure parent exists if (!FSUtils.exists(parent)) { Files.createDirectories(parent); } // create file Path binPath = FSUtils.createRandomFile(parent); // writing file payload.writeToPath(binPath); StoragePath storagePath = FSUtils.getStoragePath(basePath, binPath); ContentPayload newPayload = new FSPathContentPayload(binPath); Long sizeInBytes = Files.size(binPath); boolean isReference = false; Map<String, String> contentDigest = null; return new DefaultBinary(storagePath, newPayload, sizeInBytes, isReference, contentDigest); } catch (IOException e) { throw new GenericException("Could not create binary", e); } } } @Override public Binary updateBinaryContent(StoragePath storagePath, ContentPayload payload, boolean asReference, boolean createIfNotExists) throws GenericException, NotFoundException, RequestNotValidException { if (asReference) { throw new GenericException("Method not yet implemented"); } else { Path binaryPath = FSUtils.getEntityPath(basePath, storagePath); boolean fileExists = FSUtils.exists(binaryPath); if (!fileExists && !createIfNotExists) { throw new NotFoundException("Binary does not exist: " + binaryPath); } else if (fileExists && !FSUtils.isFile(binaryPath)) { throw new GenericException("Looking for a binary but found something else"); } else { try { payload.writeToPath(binaryPath); } catch (IOException e) { throw new GenericException("Could not update binary content", e); } } Resource resource = FSUtils.convertPathToResource(basePath, binaryPath); if (resource instanceof Binary) { return (DefaultBinary) resource; } else { throw new GenericException("Looking for a binary but found something else"); } } } @Override public Binary getBinary(StoragePath storagePath) throws RequestNotValidException, NotFoundException, GenericException { Path binaryPath = FSUtils.getEntityPath(basePath, storagePath); Resource resource = FSUtils.convertPathToResource(basePath, binaryPath); if (resource instanceof Binary) { return (Binary) resource; } else { throw new RequestNotValidException("Looking for a binary but found something else"); } } @Override public void deleteResource(StoragePath storagePath) throws NotFoundException, GenericException { Path resourcePath = FSUtils.getEntityPath(basePath, storagePath); trash(resourcePath); // cleanup history deleteAllBinaryVersionsUnder(storagePath); } public Path resolve(StoragePath storagePath) { return FSUtils.getEntityPath(basePath, storagePath); } @Override public void copy(StorageService fromService, StoragePath fromStoragePath, StoragePath toStoragePath) throws AlreadyExistsException, GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException { if (fromService instanceof FileStorageService) { Path sourcePath = ((FileStorageService) fromService).resolve(fromStoragePath); Path targetPath = FSUtils.getEntityPath(basePath, toStoragePath); FSUtils.copy(sourcePath, targetPath, false); } else { Class<? extends Entity> rootEntity = fromService.getEntity(fromStoragePath); StorageServiceUtils.copyBetweenStorageServices(fromService, fromStoragePath, this, toStoragePath, rootEntity); } } @Override public void move(StorageService fromService, StoragePath fromStoragePath, StoragePath toStoragePath) throws AlreadyExistsException, GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException { if (fromService instanceof FileStorageService) { Path sourcePath = ((FileStorageService) fromService).resolve(fromStoragePath); Path targetPath = FSUtils.getEntityPath(basePath, toStoragePath); FSUtils.move(sourcePath, targetPath, false); } else { Class<? extends Entity> rootEntity = fromService.getEntity(fromStoragePath); StorageServiceUtils.moveBetweenStorageServices(fromService, fromStoragePath, this, toStoragePath, rootEntity); } } @Override public Class<? extends Entity> getEntity(StoragePath storagePath) throws NotFoundException { Path entity = FSUtils.getEntityPath(basePath, storagePath); if (FSUtils.exists(entity)) { if (FSUtils.isDirectory(entity)) { if (storagePath.isFromAContainer()) { return DefaultContainer.class; } else { return DefaultDirectory.class; } } else { return DefaultBinary.class; } } else { throw new NotFoundException("Entity was not found: " + storagePath); } } @Override public DirectResourceAccess getDirectAccess(final StoragePath storagePath) { return new DirectResourceAccess() { @Override public Path getPath() { // TODO disable write access to resource // for UNIX programs using user with read-only permissions // for Java programs using SecurityManager and Policy return FSUtils.getEntityPath(basePath, storagePath); } @Override public void close() throws IOException { // nothing to do } }; } @Override public CloseableIterable<BinaryVersion> listBinaryVersions(StoragePath storagePath) throws GenericException, RequestNotValidException, NotFoundException, AuthorizationDeniedException { Path fauxPath = FSUtils.getEntityPath(historyDataPath, storagePath); Path parent = fauxPath.getParent(); final String baseName = fauxPath.getFileName().toString(); CloseableIterable<BinaryVersion> iterable; if (!FSUtils.exists(parent)) { return new EmptyClosableIterable<>(); } try { final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(parent, new DirectoryStream.Filter<Path>() { @Override public boolean accept(Path entry) throws IOException { return entry.getFileName().toString().startsWith(baseName); } }); final Iterator<Path> pathIterator = directoryStream.iterator(); iterable = new CloseableIterable<BinaryVersion>() { @Override public Iterator<BinaryVersion> iterator() { return new Iterator<BinaryVersion>() { @Override public boolean hasNext() { return pathIterator.hasNext(); } @Override public BinaryVersion next() { Path next = pathIterator.next(); BinaryVersion ret; try { ret = FSUtils.convertPathToBinaryVersion(historyDataPath, historyMetadataPath, next); } catch (GenericException | NotFoundException | RequestNotValidException e) { LOGGER.error("Error while list path " + basePath + " while parsing resource " + next, e); ret = null; } return ret; } }; } @Override public void close() throws IOException { directoryStream.close(); } }; } catch (NoSuchFileException e) { throw new NotFoundException("Could not find versions of " + storagePath, e); } catch (IOException e) { throw new GenericException("Error finding version of " + storagePath, e); } return iterable; } @Override public BinaryVersion getBinaryVersion(StoragePath storagePath, String version) throws RequestNotValidException, NotFoundException, GenericException { Path binVersionPath = FSUtils.getEntityPath(historyDataPath, storagePath, version); return FSUtils.convertPathToBinaryVersion(historyDataPath, historyMetadataPath, binVersionPath); } @Override public BinaryVersion createBinaryVersion(StoragePath storagePath, Map<String, String> properties) throws RequestNotValidException, NotFoundException, GenericException { Path binPath = FSUtils.getEntityPath(basePath, storagePath); String id = IdUtils.createUUID(); Path dataPath = FSUtils.getEntityPath(historyDataPath, storagePath, id); Path metadataPath = FSUtils.getBinaryHistoryMetadataPath(historyDataPath, historyMetadataPath, dataPath); if (!FSUtils.exists(binPath)) { throw new NotFoundException("Binary does not exist: " + binPath); } if (!FSUtils.isFile(binPath)) { throw new RequestNotValidException("Not a regular file: " + binPath); } if (FSUtils.exists(dataPath)) { throw new GenericException("Binary version id collided: " + dataPath); } try { // ensuring parent exists Path parent = dataPath.getParent(); if (!FSUtils.exists(parent)) { Files.createDirectories(parent); } // writing file Files.copy(binPath, dataPath); // Creating metadata DefaultBinaryVersion b = new DefaultBinaryVersion(); b.setId(id); b.setProperties(properties); b.setCreatedDate(new Date()); Files.createDirectories(metadataPath.getParent()); JsonUtils.writeObjectToFile(b, metadataPath); return FSUtils.convertPathToBinaryVersion(historyDataPath, historyMetadataPath, dataPath); } catch (IOException e) { throw new GenericException("Could not create binary", e); } } @Override public void revertBinaryVersion(StoragePath storagePath, String version) throws NotFoundException, RequestNotValidException, GenericException { Path binPath = FSUtils.getEntityPath(basePath, storagePath); Path binVersionPath = FSUtils.getEntityPath(historyDataPath, storagePath, version); if (!FSUtils.exists(binPath)) { throw new NotFoundException("Binary does not exist: " + binPath); } if (!FSUtils.isFile(binPath)) { throw new RequestNotValidException("Not a regular file: " + binPath); } if (!FSUtils.exists(binVersionPath)) { throw new NotFoundException("Binary version does not exist: " + binVersionPath); } try { // writing file Files.copy(binVersionPath, binPath, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { throw new GenericException("Could not create binary", e); } } @Override public void deleteBinaryVersion(StoragePath storagePath, String version) throws NotFoundException, GenericException, RequestNotValidException { Path dataPath = FSUtils.getEntityPath(historyDataPath, storagePath, version); Path metadataPath = FSUtils.getBinaryHistoryMetadataPath(historyDataPath, historyMetadataPath, dataPath); trash(dataPath); trash(metadataPath); // cleanup created parents FSUtils.deleteEmptyAncestorsQuietly(dataPath, historyDataPath); FSUtils.deleteEmptyAncestorsQuietly(metadataPath, historyMetadataPath); } private void deleteAllBinaryVersionsUnder(StoragePath storagePath) { Path resourcePath = FSUtils.getEntityPath(basePath, storagePath); Path relativePath = basePath.relativize(resourcePath); Path resourceHistoryDataPath = historyDataPath.resolve(relativePath); if (FSUtils.isDirectory(resourceHistoryDataPath)) { try { Path resourceHistoryMetadataPath = historyMetadataPath .resolve(historyDataPath.relativize(resourceHistoryDataPath)); trash(resourceHistoryDataPath); trash(resourceHistoryMetadataPath); FSUtils.deleteEmptyAncestorsQuietly(resourceHistoryDataPath, historyDataPath); FSUtils.deleteEmptyAncestorsQuietly(resourceHistoryMetadataPath, historyMetadataPath); } catch (GenericException | NotFoundException e) { LOGGER.warn("Could not delete history under " + resourceHistoryDataPath, e); } } else { Path parent = resourceHistoryDataPath.getParent(); final String baseName = resourceHistoryDataPath.getFileName().toString(); if (FSUtils.exists(parent)) { DirectoryStream<Path> directoryStream = null; try { directoryStream = Files.newDirectoryStream(parent, new DirectoryStream.Filter<Path>() { @Override public boolean accept(Path entry) throws IOException { return entry.getFileName().toString().startsWith(baseName); } }); for (Path p : directoryStream) { trash(p); Path pMetadata = FSUtils.getBinaryHistoryMetadataPath(historyDataPath, historyMetadataPath, p); trash(pMetadata); FSUtils.deleteEmptyAncestorsQuietly(p, historyDataPath); FSUtils.deleteEmptyAncestorsQuietly(pMetadata, historyMetadataPath); } } catch (IOException | GenericException | NotFoundException e) { LOGGER.warn("Could not delete history under " + resourceHistoryDataPath, e); } finally { IOUtils.closeQuietly(directoryStream); } } } } @Override public boolean hasDirectory(StoragePath storagePath) { try { this.getDirectory(storagePath); return true; } catch (NotFoundException | RequestNotValidException | GenericException e) { return false; } } @Override public boolean hasBinary(StoragePath storagePath) { try { this.getBinary(storagePath); return true; } catch (NotFoundException | RequestNotValidException | GenericException e) { return false; } } }