RepositoryFileSystemFactory.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.ftp;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.ftpserver.ftplet.AuthenticationFailedException;
import org.apache.ftpserver.ftplet.FileSystemFactory;
import org.apache.ftpserver.ftplet.FileSystemView;
import org.apache.ftpserver.ftplet.FtpException;
import org.apache.ftpserver.ftplet.FtpFile;
import org.apache.ftpserver.ftplet.User;
import org.genesys.filerepository.FileRepositoryException;
import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.genesys.filerepository.NoSuchRepositoryFileException;
import org.genesys.filerepository.model.RepositoryFile;
import org.genesys.filerepository.model.RepositoryFolder;
import org.genesys.filerepository.service.RepositoryService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
 * A factory for creating RepositoryFileSystem objects.
 */
@Component
@Slf4j
public class RepositoryFileSystemFactory implements FileSystemFactory, InitializingBean {

	/** The repository service. */
	@Autowired(required = true)
	private RepositoryService repositoryService;

	/** The bytes manager. */
	@Autowired(required = true)
	private TemporaryBytesManager bytesManager;

	/**
	 * File.
	 *
	 * @param repositoryFile the repository file
	 * @return the repository ftp file
	 */
	private RepositoryFtpFile file(final RepositoryFile repositoryFile, final RepositoryFileSystemView session) {
		log.trace("Making RepositoryFtpFile repositoryFile={}", repositoryFile);

		return new RepositoryFtpFile(repositoryFile) {

			@Override
			public String getOwnerName() {
				// TODO Auto-generated method stub
				return "root";
			}

			@Override
			public String getGroupName() {
				// TODO Auto-generated method stub
				return "wheel";
			}

			@Override
			public boolean mkdir() {
				log.debug("MKDIR on file not possible");
				return false;
			}

			@Override
			public boolean delete() {
				return FtpRunAs.asFtpUser(session.user, () -> {
					log.info("Delete file={}", this.getAbsolutePath());
					try {
						repositoryService.removeFile(repositoryFile);
						return true;
					} catch (NoSuchRepositoryFileException | IOException e) {
						log.warn(e.getMessage());
						return false;
					}
				});
			}

			@Override
			public boolean move(final FtpFile destination) {
				return FtpRunAs.asFtpUser(session.user, () -> {
					log.info("Move file={} to dest={}", this.getAbsolutePath(), Paths.get(destination.getAbsolutePath()));
					try {
						repositoryService.moveAndRenameFile(repositoryFile, Paths.get(destination.getAbsolutePath()));
						return true;
					} catch (InvalidRepositoryPathException | InvalidRepositoryFileDataException e) {
						log.warn("Error moving file: {}", e.getMessage());
						return false;
					}
				});
			}

			@Override
			public OutputStream createOutputStream(final long offset) throws IOException {
				return FtpRunAs.asFtpUser(session.user, () -> {
					log.info("Creating output stream for file={} at offset={}", getAbsolutePath(), offset);
					return bytesManager.createOutputStream(session.user, repositoryFile, offset);
				});
			}

			@Override
			public InputStream createInputStream(final long offset) throws IOException {
				return FtpRunAs.asFtpUser(session.user, () -> {
					log.info("Creating input stream for file={} at offset={}", getAbsolutePath(), offset);
					return bytesManager.createInputStream(repositoryFile, offset);
				});
			}

		};
	}

	/**
	 * Directory.
	 *
	 * @param path the path
	 * @param session the session
	 * @return the repository ftp directory
	 */
	private RepositoryFtpDirectory directory(final Path path, final RepositoryFileSystemView session) {
		log.trace("Viewing RepositoryFtpDirectory path={}", path);

		return new RepositoryFtpDirectory(path) {

			@Override
			public boolean move(final FtpFile destination) {
				return FtpRunAs.asFtpUser(session.user, () -> {
					log.info("Move directory={} to dest={}", this.getAbsolutePath(), destination.getAbsolutePath());
					try {
						repositoryService.renamePath(Paths.get(this.getAbsolutePath()), Paths.get(destination.getAbsolutePath()));
						return true;
					} catch (final InvalidRepositoryPathException e) {
						log.error("Failed to rename directory", e);
						return false;
					}
				});
			}

			@Override
			public boolean mkdir() {
				log.info("Mkdir directory={}", this.getAbsolutePath());
				try {
					return FtpRunAs.asFtpUser(session.user, () -> {
						repositoryService.ensureFolder(path);
						return true;
					});
				} catch (InvalidRepositoryPathException e) {
					log.error("{}", e.getMessage(), e);
					return false;
				}
			}

			@Override
			public List<? extends FtpFile> listFiles() {
				try {
					return FtpRunAs.asFtpUser(session.user, this::_listFiles);
				} catch (FileRepositoryException e) {
					throw new RuntimeException(e.getMessage(), e);
				}
			}

			private List<? extends FtpFile> _listFiles() throws InvalidRepositoryPathException {
				final Path root = path.normalize().toAbsolutePath();
				log.debug("Listing files in path={}", root);
				final ArrayList<FtpFile> all = new ArrayList<>();

				all.addAll(repositoryService.getFolders(root, RepositoryFolder.DEFAULT_SORT).stream().peek(rf -> {
					// System.err.println("repoFolder " + rf.getPath());
				}).map(rf -> directory(rf.getFolderPath(), session)).collect(Collectors.toList()));

				all.addAll(repositoryService.getFiles(root, RepositoryFile.DEFAULT_SORT).stream().peek(rf -> {
					// System.err.println("repoFile " + rf.getStorageFullPath());
				}).map(rf -> file(rf, session)).collect(Collectors.toList()));

				// Distinct sorted list of everything
				return all.stream().distinct().sorted(Comparator.comparing(FtpFile::getName)).peek(path1 -> {
					// System.err.println("listFiles " + path.getName());
				}).collect(Collectors.toUnmodifiableList());
			}

			@Override
			public String getOwnerName() {
				// TODO Auto-generated method stub
				return "root";
			}

			@Override
			public String getGroupName() {
				// TODO Auto-generated method stub
				return "wheel";
			}

			@Override
			public boolean delete() {
				return FtpRunAs.asFtpUser(session.user, () -> {
					log.info("Delete this={}", path);
					try {
						repositoryService.deleteFolder(path);
						return true;
					} catch (FileRepositoryException e) {
						return false;
					}
				});
			}

			@Override
			public boolean changeWorkingDirectory(final String dir) {
				final Path normalized = Paths.get(getAbsolutePath()).resolve(dir).normalize().toAbsolutePath();
				log.info("CWD this={} dir={} normalized={}", getAbsolutePath(), dir, normalized);
				if ("/".equals(normalized.toString())) {
					this.cwd(normalized);
					return true;
				}

				// Check if such path exists?
				try {
					RepositoryFolder folder = repositoryService.getFolder(normalized);
					if (folder != null) {
						this.cwd(normalized);
						return true;
					} else {
						log.warn("CWD to non-existent folder {} normalized={}", dir, normalized);
						return false;
					}
				} catch (InvalidRepositoryPathException e) {
					log.warn("CWD to non-existent folder {} normalized={}", dir, normalized, e);
					return false;
				}
			}
		};
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.apache.ftpserver.ftplet.FileSystemFactory#createFileSystemView(org.apache
	 * .ftpserver.ftplet.User)
	 */
	@Override
	public FileSystemView createFileSystemView(final User user) {
		log.info("Creating new repository view for {}", user.getName());

		return new RepositoryFileSystemView((FtpUser) user) {

			@Override
			public FtpFile getFile(final String file) throws FtpException {
				log.debug("getFile file={} for user={}", file, username);
				final Path path = file.startsWith("/") ? Paths.get(file).normalize() : Paths.get(cwd.getAbsolutePath(), file).normalize();
				log.trace("Resolved normalized={}", path);

				try {

					return isDirectory(path) ?
						// directory
						directory("/".equals(path.toString()) ? path : FtpRunAs.asFtpUser(user, () -> repositoryService.getFolder(path).getFolderPath()), this)
						// or file
						: file(FtpRunAs.asFtpUser(user, () -> repositoryService.getFile(path.getParent(), path.getFileName().toString())), this);

				} catch (final AuthenticationException e) {
					log.warn("Authentication problem {}", e.getMessage(), e);
					throw new AuthenticationFailedException(e.getMessage(), e);
				} catch (NoSuchRepositoryFileException e) {
					log.debug("No such file {}: {}", file, e.getMessage());

					assert path.getFileName() != null;
					return new CanBeAnythingFile(path.getParent(), path.getFileName().toString()) {

						@Override
						public boolean mkdir() {
							log.debug("MKDIR {}", path);
							try {
								RepositoryFolder repositoryFolder = FtpRunAs.asFtpUser(user, () -> repositoryService.ensureFolder(path));
								return repositoryFolder != null;
							} catch (InvalidRepositoryPathException e) {
								log.warn("{}", e.getMessage(), e);
								return false;
							}
						}

						@Override
						public boolean delete() {
							return true;
						}

						@Override
						public OutputStream createOutputStream(long offset) throws IOException {
							log.debug("STOR {}", path);
							if (path.getParent() != null) {
								log.debug("MKDIR {}", path.getParent());
								try {
									RepositoryFolder repositoryFolder = FtpRunAs.asFtpUser(user, () -> repositoryService.ensureFolder(path.getParent()));
									if (repositoryFolder == null) {
										throw new InvalidRepositoryPathException("Folder not created " + path.getParent());
									}
									return bytesManager.newFile(user, path);
								} catch (InvalidRepositoryPathException e) {
									log.warn("{}", e.getMessage(), e);
									throw new IOException(e.getMessage(), e);
								}
							} else {
								throw new IOException("Cannot store files to /");
							}
						}
					};
				} catch (FileRepositoryException e) {
					throw new FtpException(e.getMessage(), e);
				}
			}

			private boolean isDirectory(final Path path) throws FtpException {
				if ("/".equals(path.toString())) {
					return true;
				}

				try {
					log.trace("isDirectory {}", path);
					return repositoryService.hasPath(path);
				} catch (final InvalidRepositoryPathException e) {
					log.debug("Invalid repository path {}: {}", path, e.getMessage());
					throw new FtpException(e.getMessage(), e);
				}
			}

		};
		// AspectJProxyFactory factory = new AspectJProxyFactory(userView);
		// factory.addAspect(new FtpSpringSecurityAspect((FtpUser) user));
		// return factory.getProxy();
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
	 */
	@Override
	public void afterPropertiesSet() throws Exception {
		assert (this.repositoryService != null);
		log.warn("Initialized RFSF with service={}", this.repositoryService);
	}

	/**
	 * The Class RepositoryFileSystemView.
	 */
	private abstract class RepositoryFileSystemView implements FileSystemView {

		/** The user. */
		protected FtpUser user;

		/** The username. */
		protected String username;

		/** The cwd. */
		protected RepositoryFtpDirectory cwd = directory(Paths.get("/"), this);

		/** The home dir. */
		protected RepositoryFtpDirectory homeDir = directory(Paths.get("/"), this);

		/**
		 * Instantiates a new repository file system view.
		 *
		 * @param user the user
		 */
		public RepositoryFileSystemView(final FtpUser user) {
			this.user = user;
			username = user.getName();
		}

		/*
		 * (non-Javadoc)
		 * @see org.apache.ftpserver.ftplet.FileSystemView#isRandomAccessible()
		 */
		@Override
		public boolean isRandomAccessible() throws FtpException {
			// TODO Auto-generated method stub
			return false;
		}

		/*
		 * (non-Javadoc)
		 * @see org.apache.ftpserver.ftplet.FileSystemView#getWorkingDirectory()
		 */
		@Override
		public FtpFile getWorkingDirectory() throws FtpException {
			log.debug("getWorkingDirectory for user={}", username);
			return this.cwd;
		}

		/*
		 * (non-Javadoc)
		 * @see org.apache.ftpserver.ftplet.FileSystemView#getHomeDirectory()
		 */
		@Override
		public FtpFile getHomeDirectory() throws FtpException {
			log.debug("getHomeDirectory for user={}", username);
			return this.homeDir;
		}

		/*
		 * (non-Javadoc)
		 * @see org.apache.ftpserver.ftplet.FileSystemView#dispose()
		 */
		@Override
		public void dispose() {
			log.info("Disposing repository view for user={}", username);
		}

		/*
		 * (non-Javadoc)
		 * @see
		 * org.apache.ftpserver.ftplet.FileSystemView#changeWorkingDirectory(java.lang.
		 * String)
		 */
		@Override
		public boolean changeWorkingDirectory(final String dir) throws FtpException {
			log.debug("CWD dir={} for user={}", dir, username);
			return this.cwd.changeWorkingDirectory(dir);
		}
	}
}