FilesystemStorageServiceImpl.java

/*
 * Copyright 2018 Global Crop Diversity Trust
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.genesys.filerepository.service.impl;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.apache.commons.io.IOUtils;
import org.genesys.filerepository.BytesStorageException;
import org.genesys.filerepository.service.BytesStorageService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

/**
 * The Class FilesystemStorageServiceImpl.
 *
 * @author mobreza
 */
@Service("fileSystemStorage")
@Slf4j
public class FilesystemStorageServiceImpl implements InitializingBean, BytesStorageService {

	/** Repository base directory. */
	private File repoDir;
	private Path repoPath;

	/**
	 * Sets the repository base directory.
	 *
	 * @param repoDir the new repository base directory
	 */
	public void setRepositoryBaseDirectory(final File repoDir) {
		this.repoDir = repoDir;
	}

	/**
	 * After properties set.
	 *
	 * @throws BytesStorageException the bytes storage exception
	 */
	@Override
	public void afterPropertiesSet() throws BytesStorageException {
		sanityCheck();
		this.repoPath = Paths.get(repoDir.getAbsolutePath());
	}

	/**
	 * Sanity check.
	 *
	 * @throws BytesStorageException the bytes storage exception
	 */
	protected void sanityCheck() throws BytesStorageException {
		if (!repoDir.isDirectory()) {
			throw new BytesStorageException("Base path " + repoDir + " is not a directory");
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.genesys.filerepository.service.BytesStorageService#upsert(
	 * java.lang.String, java.lang.String, byte[])
	 */
	@Override
	public void upsert(Path bytesFile, final byte[] data) throws IOException {
		final Path normalPath = bytesFile.normalize().toAbsolutePath();
		final Path destinationFilePath = getDestPath(normalPath);

		log.trace("Trying to upsert path={}", normalPath);
		assert (destinationFilePath != null && destinationFilePath.getParent() != null);
		final File destinationDir = destinationFilePath.getParent().toFile();

		if (!destinationDir.getCanonicalPath().startsWith(repoDir.getCanonicalPath())) {
			throw new IOException("Not within repository path " + destinationDir.getAbsolutePath());
		} else if (!destinationDir.exists()) {
			if (!destinationDir.mkdirs()) {
				throw new IOException("Destination folder could not be created " + destinationDir.getAbsolutePath());
			}
		} else if (!destinationDir.isDirectory()) {
			throw new IOException("Exists, not a directory " + destinationDir.getAbsolutePath());
		}

		final File destinationFile = destinationFilePath.toFile();
		if (!destinationFile.getCanonicalPath().startsWith(repoDir.getCanonicalPath())) {
			throw new IOException("Not within repository path " + destinationFile.getAbsolutePath());
		}

		try (BufferedOutputStream output = new BufferedOutputStream(Files.newOutputStream(destinationFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) {
			IOUtils.write(data, output);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.genesys.filerepository.service.BytesStorageService#upsert(
	 * java.lang.String, java.lang.String, byte[])
	 */
	@Override
	public void upsert(Path bytesFile, final File fileWithData) throws IOException {

		if (fileWithData == null || !fileWithData.exists()) {
			throw new IOException("File is null or does not exist.");
		}

		final Path normalPath = bytesFile.normalize().toAbsolutePath();
		final Path destinationFilePath = getDestPath(normalPath);

		log.trace("Trying to upsert path={}", normalPath);
		assert (destinationFilePath != null && destinationFilePath.getParent() != null);
		final File destinationDir = destinationFilePath.getParent().toFile();

		if (!destinationDir.getCanonicalPath().startsWith(repoDir.getCanonicalPath())) {
			throw new IOException("Not within repository path " + destinationDir.getAbsolutePath());
		} else if (!destinationDir.exists()) {
			if (!destinationDir.mkdirs()) {
				throw new IOException("Destination folder could not be created " + destinationDir.getAbsolutePath());
			}
		} else if (!destinationDir.isDirectory()) {
			throw new IOException("Exists, not a directory " + destinationDir.getAbsolutePath());
		}

		final File destinationFile = destinationFilePath.toFile();
		if (!destinationFile.getCanonicalPath().startsWith(repoDir.getCanonicalPath())) {
			throw new IOException("Not within repository path " + destinationFile.getAbsolutePath());
		}

		try (FileInputStream source  = new FileInputStream(fileWithData)) {
			try (FileOutputStream target = new FileOutputStream(destinationFile, false)) {
				long bytesTransferred = target.getChannel().transferFrom(source.getChannel(), 0, Long.MAX_VALUE);
				log.debug("Transferred {}b from {} to {}", bytesTransferred, fileWithData.toString(), bytesFile.toString());
			}
		}
	}

	/**
	 * Gets the path where subPath a sub-folder of {@link #repoDir}.
	 *
	 * @param subPath the normal path
	 * @return the dest path
	 */
	public Path getDestPath(Path subPath) {
		return Paths.get(repoPath.toString(), subPath.toString()).normalize().toAbsolutePath();
	}

	/*
	 * (non-Javadoc)
	 * @see org.genesys.filerepository.service.BytesStorageService#remove(
	 * java.lang.String, java.lang.String)
	 */
	@Override
	public void remove(Path bytesFile) throws IOException {
		final Path normalPath = bytesFile.normalize().toAbsolutePath();
		final Path destinationFilePath = getDestPath(normalPath);

		assert (destinationFilePath != null && destinationFilePath.getParent() != null);
		final File destinationDir = destinationFilePath.getParent().toFile();
		final File destinationFile = destinationFilePath.toFile();

		if (!destinationFile.getCanonicalPath().startsWith(repoDir.getCanonicalPath())) {
			throw new IOException("Not within repository path: " + destinationFile.getAbsolutePath());
		}

		if (destinationFile.exists()) {
			if (!destinationFile.delete()) {
				throw new IOException("Repository butes could not be removed at " + destinationFile.getAbsolutePath());
			}
		}

		// Delete empty dir
		if (destinationDir.exists() && destinationDir.isDirectory()) {

			final String[] dirContents = destinationDir.list();
			if ((dirContents != null) && (dirContents.length == 0)) {
				tryDelete(destinationDir);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.genesys.filerepository.service.BytesStorageService#get(java.
	 * lang.String, java.lang.String)
	 */
	@Override
	public byte[] get(Path bytesFile) throws IOException {
		final Path normalPath = bytesFile.normalize().toAbsolutePath();
		final Path destinationFilePath = getDestPath(normalPath);

		log.trace("Retrieving bytes of {}", normalPath);

		final File destinationFile = destinationFilePath.toFile();

		if (!destinationFile.getCanonicalPath().startsWith(repoDir.getCanonicalPath())) {
			throw new IOException("Not within repository path: " + destinationFile.getAbsolutePath());
		}

		byte[] data = null;
		if (destinationFile.exists()) {
			try (InputStream inputStream = Files.newInputStream(destinationFile.toPath())) {
				data = IOUtils.toByteArray(inputStream);
			}
		} else {
			log.warn("Repository bytes not found at {}", destinationFile.getAbsolutePath());
		}

		return data;
	}

	@Override
	public void get(Path bytesFile, Consumer<InputStream> consumerOfStream) throws IOException {
		final Path normalPath = bytesFile.normalize().toAbsolutePath();
		final Path destinationFilePath = getDestPath(normalPath);

		log.trace("Retrieving bytes of {}", normalPath);

		final File destinationFile = destinationFilePath.toFile();

		if (!destinationFile.getCanonicalPath().startsWith(repoDir.getCanonicalPath())) {
			throw new IOException("Not within repository path: " + destinationFile.getAbsolutePath());
		}

		if (destinationFile.exists()) {
			try (InputStream inputStream = Files.newInputStream(destinationFile.toPath())) {
				consumerOfStream.accept(inputStream);
			}
		} else {
			throw new IOException("Repository bytes not found at " + destinationFile.getAbsolutePath());
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.genesys.filerepository.service.BytesStorageService#exists(java.lang.
	 * String, java.lang.String)
	 */
	@Override
	public boolean exists(final Path bytesFile) {

		return getDestPath(bytesFile).toFile().exists();
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.genesys.filerepository.service.BytesStorageService#listFiles(java.lang.
	 * String)
	 */
	@Override
	public List<String> listFiles(final Path path) {
		log.debug("Listing filenames at path={}", path);

		final File destinationDir = getDestPath(path).toFile();

		if (!destinationDir.exists() || !destinationDir.isDirectory()) {
			log.info("Returning empty files list for nonexistent dir={}", destinationDir.getAbsolutePath());
			return Collections.emptyList();
		}

		log.debug("Listing filenames in dir={}", destinationDir.getAbsolutePath());
		final File[] files = destinationDir.listFiles();
		if (files == null) {
			return Collections.emptyList();
		}
		return Arrays.stream(files).filter(file -> !file.isDirectory()).map(File::getName).collect(Collectors.toList());
	}

	private static void tryDelete(File file) {
		log.debug("DELETING {}", file.getAbsolutePath());
		try {
			if (!file.delete()) {
				log.debug("{} is not deleted", file.getAbsolutePath());
			}
		} catch (Exception e) {
			log.debug("{} is not deleted", file.getAbsolutePath());
		}
	}
}