FilesystemBasedFileImpl.java

/*
 * This file is part of Waarp Project (named also Waarp or GG).
 *
 *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
 *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
 * individual contributors.
 *
 *  All Waarp Project is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * Waarp 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with
 * Waarp . If not, see <http://www.gnu.org/licenses/>.
 */
package org.waarp.common.file.filesystembased;

import org.waarp.common.command.exception.CommandAbstractException;
import org.waarp.common.exception.FileEndOfTransferException;
import org.waarp.common.exception.FileTransferException;
import org.waarp.common.file.AbstractDir;
import org.waarp.common.file.AbstractFile;
import org.waarp.common.file.DataBlock;
import org.waarp.common.file.DirInterface;
import org.waarp.common.file.FileUtils;
import org.waarp.common.file.SessionInterface;
import org.waarp.common.logging.SysErrLogger;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * File implementation for Filesystem Based
 */
public abstract class FilesystemBasedFileImpl extends AbstractFile {
  private static final String ERROR_DURING_GET = "Error during get:";

  private static final String INTERNAL_ERROR_FILE_IS_NOT_READY =
      "Internal error, file is not ready";

  private static final String NO_FILE_IS_READY = "No file is ready";

  /**
   * Internal Logger
   */
  private static final WaarpLogger logger =
      WaarpLoggerFactory.getLogger(FilesystemBasedFileImpl.class);

  /**
   * SessionInterface
   */
  protected final SessionInterface session;

  /**
   * DirInterface associated with this file at creation. It is not necessary
   * the
   * directory that owns this file.
   */
  private final FilesystemBasedDirImpl dir;

  /**
   * {@link FilesystemBasedAuthImpl}
   */
  private final FilesystemBasedAuthImpl auth;

  /**
   * Current file if any
   */
  protected String currentFile;
  /**
   * Current Real File if any
   */
  protected File currentRealFile = null;

  /**
   * Is this file in append mode
   */
  protected boolean isAppend;

  /**
   * Valid Position of this file
   */
  private long position;

  /**
   * FileOutputStream Out
   */
  private FileOutputStream fileOutputStream;
  /**
   * FileInputStream In
   */
  private FileInputStream fileInputStream;

  private byte[] reusableBytes;

  /**
   * @param session
   * @param dir It is not necessary the directory that owns this file.
   * @param path
   * @param append
   *
   * @throws CommandAbstractException
   */
  protected FilesystemBasedFileImpl(final SessionInterface session,
                                    final FilesystemBasedDirImpl dir,
                                    final String path, final boolean append)
      throws CommandAbstractException {
    this.session = session;
    auth = (FilesystemBasedAuthImpl) session.getAuth();
    this.dir = dir;
    currentFile = path;
    isAppend = append;
    currentRealFile = getFileFromPath(path);
    if (append) {
      try {
        setPosition(currentRealFile.length());
      } catch (final IOException e) {
        // not ready
        return;
      }
    } else {
      try {
        setPosition(0);
      } catch (final IOException ignored) {
        // nothing
      }
    }
    isReady = true;
  }

  /**
   * Special constructor for possibly external file
   *
   * @param session
   * @param dir It is not necessary the directory that owns this file.
   * @param path
   */
  protected FilesystemBasedFileImpl(final SessionInterface session,
                                    final FilesystemBasedDirImpl dir,
                                    final String path) {
    this.session = session;
    auth = (FilesystemBasedAuthImpl) session.getAuth();
    this.dir = dir;
    currentFile = path;
    currentRealFile = null;
    isReady = true;
    isAppend = false;
    position = 0;
  }

  @Override
  public final void clear() throws CommandAbstractException {
    super.clear();
    currentFile = null;
    currentRealFile = null;
    isAppend = false;
  }

  @Override
  public SessionInterface getSession() {
    return session;
  }

  @Override
  public final DirInterface getDir() {
    return dir;
  }

  /**
   * Get the File from this path, checking first its validity
   *
   * @param path
   *
   * @return the FileInterface
   *
   * @throws CommandAbstractException
   */
  protected final File getFileFromPath(final String path)
      throws CommandAbstractException {
    final String newdir = getDir().validatePath(path);
    if (dir.isAbsolute(newdir)) {
      return new File(newdir);
    }
    final String truedir = auth.getAbsolutePath(newdir);
    final File file = new File(truedir);
    logger.debug("Final File: {} CanRead: {}", truedir, file.canRead());
    return file;
  }

  /**
   * Get the relative path (without mount point)
   *
   * @param file
   *
   * @return the relative path
   */
  protected final String getRelativePath(final File file) {
    return auth.getRelativePath(
        AbstractDir.normalizePath(file.getAbsolutePath()));
  }

  /**
   * Adapt File.isDirectory() to leverage synchronization error with filesystem
   *
   * @param file
   *
   * @return as with File.isDirectory()
   */
  public static boolean isDirectory(final File file) {
    for (int i = 0; i < 3; i++) {
      if (file.isDirectory()) {
        return true;
      }
      try {
        Thread.sleep(10);
      } catch (final InterruptedException ignored) { //NOSONAR
        SysErrLogger.FAKE_LOGGER.ignoreLog(ignored);
      }
    }
    return false;
  }

  @Override
  public boolean isDirectory() throws CommandAbstractException {
    checkIdentify();
    if (currentRealFile == null) {
      currentRealFile = getFileFromPath(currentFile);
    }
    return isDirectory(currentRealFile);
  }

  /**
   * Adapt File.isFile() to leverage synchronization error with filesystem
   *
   * @param file
   *
   * @return as with File.isFile()
   */
  public static boolean isFile(final File file) {
    for (int i = 0; i < 3; i++) {
      if (file.isFile()) {
        return true;
      }
      try {
        Thread.sleep(10);
      } catch (final InterruptedException ignored) { //NOSONAR
        SysErrLogger.FAKE_LOGGER.ignoreLog(ignored);
      }
    }
    return false;
  }

  @Override
  public boolean isFile() throws CommandAbstractException {
    checkIdentify();
    if (currentRealFile == null) {
      currentRealFile = getFileFromPath(currentFile);
    }
    return isFile(currentRealFile);
  }

  @Override
  public final String getFile() throws CommandAbstractException {
    checkIdentify();
    return currentFile;
  }

  @Override
  public synchronized boolean closeFile() throws CommandAbstractException {
    if (fileInputStream != null) {
      FileUtils.close(fileInputStream);
      fileInputStream = null;
    }
    if (reusableBytes != null) {
      reusableBytes = null;
    }
    if (fileOutputStream != null) {
      FileUtils.close(fileOutputStream);
      fileOutputStream = null;
    }
    position = 0;
    isReady = false;
    // Do not clear the filename itself
    return true;
  }

  @Override
  public final synchronized boolean abortFile()
      throws CommandAbstractException {
    if (isInWriting() &&
        ((FilesystemBasedFileParameterImpl) getSession().getFileParameter()).deleteOnAbort) {
      delete();
    }
    closeFile();
    return true;
  }

  @Override
  public long length() throws CommandAbstractException {
    checkIdentify();
    if (!isReady) {
      return -1;
    }
    if (!exists()) {
      return -1;
    }
    if (currentRealFile == null) {
      currentRealFile = getFileFromPath(currentFile);
    }
    return currentRealFile.length();
  }

  @Override
  public final synchronized boolean isInReading() {
    if (!isReady) {
      return false;
    }
    return fileInputStream != null;
  }

  @Override
  public final synchronized boolean isInWriting() {
    if (!isReady) {
      return false;
    }
    return fileOutputStream != null;
  }

  /**
   * Adapt File.canRead() to leverage synchronization error with filesystem
   *
   * @param file
   *
   * @return as with File.canRead()
   */
  public static boolean canRead(final File file) {
    for (int i = 0; i < 3; i++) {
      if (file.canRead()) {
        return true;
      }
      try {
        Thread.sleep(10);
      } catch (final InterruptedException ignored) { //NOSONAR
        SysErrLogger.FAKE_LOGGER.ignoreLog(ignored);
      }
    }
    return false;
  }

  @Override
  public boolean canRead() throws CommandAbstractException {
    checkIdentify();
    if (!isReady) {
      return false;
    }
    if (currentRealFile == null) {
      currentRealFile = getFileFromPath(currentFile);
    }
    return canRead(currentRealFile);
  }

  @Override
  public boolean canWrite() throws CommandAbstractException {
    checkIdentify();
    if (!isReady) {
      return false;
    }
    if (currentRealFile == null) {
      currentRealFile = getFileFromPath(currentFile);
    }
    if (currentRealFile.exists()) {
      return currentRealFile.canWrite();
    }
    return currentRealFile.getParentFile().canWrite();
  }

  /**
   * Adapt File.exists() to leverage synchronization error with filesystem
   *
   * @param file
   *
   * @return as with File.exists()
   */
  public static boolean exists(final File file) {
    for (int i = 0; i < 3; i++) {
      if (file.exists()) {
        return true;
      }
      try {
        Thread.sleep(10);
      } catch (final InterruptedException ignored) { //NOSONAR
        SysErrLogger.FAKE_LOGGER.ignoreLog(ignored);
      }
    }
    return false;
  }

  @Override
  public boolean exists() throws CommandAbstractException {
    checkIdentify();
    if (!isReady) {
      return false;
    }
    if (currentRealFile == null) {
      currentRealFile = getFileFromPath(currentFile);
    }
    return exists(currentRealFile);
  }

  @Override
  public boolean delete() throws CommandAbstractException {
    checkIdentify();
    if (!isReady) {
      return false;
    }
    if (!exists()) {
      return true;
    }
    closeFile();
    if (currentRealFile == null) {
      currentRealFile = getFileFromPath(currentFile);
    }
    return currentRealFile.delete();
  }

  @Override
  public boolean renameTo(final String path) throws CommandAbstractException {
    checkIdentify();
    if (!isReady) {
      logger.warn("File not ready: {}", this);
      return false;
    }
    if (currentRealFile == null) {
      currentRealFile = getFileFromPath(currentFile);
    }
    if (canRead(currentRealFile)) {
      final File newFile = getFileFromPath(path);
      if (newFile.exists()) {
        logger.warn("Target file already exists: " + newFile.getAbsolutePath());
        return false;
      }
      if (newFile.getAbsolutePath().equals(currentRealFile.getAbsolutePath())) {
        // already in the right position
        isReady = true;
        return true;
      }
      if (newFile.getParentFile().canWrite()) {
        if (!currentRealFile.renameTo(newFile)) {
          FileUtils.copy(currentRealFile, newFile, true, false);
        }
        currentFile = getRelativePath(newFile);
        currentRealFile = newFile;
        isReady = true;
        logger.debug("File renamed to: {} and real position: {}", this,
                     newFile);
        return true;
      } else {
        logger.warn("Cannot write file: {} from {}", newFile, currentFile);
        return false;
      }
    }
    logger.warn("Cannot read file: {}", currentFile);
    return false;
  }

  @Override
  public final synchronized DataBlock readDataBlock()
      throws FileTransferException, FileEndOfTransferException {
    if (isReady) {
      return getByteBlock(getSession().getBlockSize());
    }
    throw new FileTransferException(NO_FILE_IS_READY);
  }

  @Override
  public final synchronized DataBlock readDataBlock(final byte[] bufferGiven)
      throws FileTransferException, FileEndOfTransferException {
    if (isReady) {
      return getByteBlock(bufferGiven);
    }
    throw new FileTransferException(NO_FILE_IS_READY);
  }

  @Override
  public final synchronized void writeDataBlock(final DataBlock dataBlock)
      throws FileTransferException {
    if (isReady) {
      if (dataBlock.isEOF()) {
        writeBlockEnd(dataBlock.getByteBlock(), dataBlock.getOffset(),
                      dataBlock.getByteCount());
        return;
      }
      writeBlock(dataBlock.getByteBlock(), dataBlock.getOffset(),
                 dataBlock.getByteCount());
      return;
    }
    throw new FileTransferException(
        "No file is ready while trying to write: " + dataBlock);
  }

  /**
   * Return the current position in the FileInterface. In write mode, it is
   * the
   * current file length.
   *
   * @return the position
   */
  public final synchronized long getPosition() {
    return position;
  }

  /**
   * Change the position in the file.
   *
   * @param position the position to set
   *
   * @throws IOException
   */
  @Override
  public final synchronized void setPosition(final long position)
      throws IOException {
    if (this.position != position) {
      this.position = position;
      if (fileInputStream != null) {
        FileUtils.close(fileInputStream);
        fileInputStream = getFileInputStream();
      }
      if (fileOutputStream != null) {
        FileUtils.close(fileOutputStream);
        fileOutputStream = getFileOutputStream(true);
        if (fileOutputStream == null) {
          throw new IOException("File cannot changed of Position");
        }
      }
    }
  }

  /**
   * Write the current FileInterface with the given byte array. The file is not
   * limited to 2^32 bytes since this
   * write operation is in add mode.
   * <p>
   * In case of error, the current already written blocks are maintained and
   * the
   * position is not changed.
   *
   * @param buffer added to the file
   *
   * @throws FileTransferException
   */
  private synchronized void writeBlock(final byte[] buffer, final int offset,
                                       final int length)
      throws FileTransferException {
    if (length > 0 && !isReady) {
      throw new FileTransferException(NO_FILE_IS_READY);
    }
    // An empty buffer is allowed
    if (buffer == null || length == 0) {
      return;// could do FileEndOfTransfer ?
    }
    if (fileOutputStream == null) {
      fileOutputStream = getFileOutputStream(position > 0);
    }
    if (fileOutputStream == null) {
      throw new FileTransferException(INTERNAL_ERROR_FILE_IS_NOT_READY);
    }
    try {
      fileOutputStream.write(buffer, offset, length);
    } catch (final IOException e2) {
      logger.error("Error during write: {}", e2.getMessage());
      try {
        closeFile();
      } catch (final CommandAbstractException ignored) {
        // nothing
      }
      // NO this.realFile.delete(); NO DELETE SINCE BY BLOCK IT CAN BE
      // REDO
      throw new FileTransferException(INTERNAL_ERROR_FILE_IS_NOT_READY);
    }
    position += length;
  }

  /**
   * End the Write of the current FileInterface with the given byte array. The
   * file
   * is not limited to 2^32 bytes
   * since this write operation is in add mode.
   *
   * @param buffer added to the file
   *
   * @throws FileTransferException
   */
  private synchronized void writeBlockEnd(final byte[] buffer, final int offset,
                                          final int length)
      throws FileTransferException {
    writeBlock(buffer, offset, length);
    try {
      closeFile();
    } catch (final CommandAbstractException e) {
      throw new FileTransferException("Close in error", e);
    }
  }

  private void checkByteBufSize(final int size) {
    if (reusableBytes == null || reusableBytes.length != size) {
      reusableBytes = new byte[size];
    }
  }

  /**
   * Get the current block of bytes of the current FileInterface. There is
   * therefore no limitation of the file
   * size to 2^32 bytes.
   * <p>
   * The returned block is limited to sizeblock. If the returned block is less
   * than sizeblock length, through lastReadSize, it is the
   * last block to read.
   *
   * @param sizeblock is the limit size for the block array
   *
   * @return the resulting DataBlock (even empty or partial)
   *
   * @throws FileTransferException
   * @throws FileEndOfTransferException
   */
  private synchronized DataBlock getByteBlock(final int sizeblock)
      throws FileTransferException, FileEndOfTransferException {
    if (!isReady) {
      throw new FileTransferException(NO_FILE_IS_READY);
    }
    if (fileInputStream == null) {
      checkByteBufSize(sizeblock);
    }
    return getByteBlock(reusableBytes);
  }

  /**
   * Get the current block of bytes of the current FileInterface. There is
   * therefore no limitation of the file
   * size to 2^32 bytes.
   * <p>
   * The returned block is limited to sizeblock. If the returned block is less
   * than sizeblock length, through lastReadSize, it is the
   * last block to read.
   *
   * @param bufferGiven buffer to use with the limit size for the block array
   *
   * @return the resulting DataBlock (even empty or partial)
   *
   * @throws FileTransferException
   * @throws FileEndOfTransferException
   */
  private synchronized DataBlock getByteBlock(final byte[] bufferGiven)
      throws FileTransferException, FileEndOfTransferException {
    if (!isReady) {
      throw new FileTransferException(NO_FILE_IS_READY);
    }
    final int sizeblock = bufferGiven.length;
    if (fileInputStream == null) {
      fileInputStream = getFileInputStream();
      if (fileInputStream == null) {
        throw new FileTransferException(INTERNAL_ERROR_FILE_IS_NOT_READY);
      }
      reusableBytes = bufferGiven;
    }
    int sizeout = 0;
    while (sizeout < sizeblock) {
      try {
        final int sizeread =
            fileInputStream.read(reusableBytes, sizeout, sizeblock - sizeout);
        if (sizeread <= 0) {
          break;
        }
        sizeout += sizeread;
      } catch (final IOException e) {
        logger.error(ERROR_DURING_GET + " {}", e.getMessage());
        try {
          closeFile();
        } catch (final CommandAbstractException ignored) {
          // nothing
        }
        throw new FileTransferException(INTERNAL_ERROR_FILE_IS_NOT_READY);
      }
    }
    if (sizeout <= 0) {
      try {
        closeFile();
      } catch (final CommandAbstractException ignored) {
        // nothing
      }
      isReady = false;
      throw new FileEndOfTransferException("End of file");
    }
    position += sizeout;
    final DataBlock dataBlock = new DataBlock();
    dataBlock.setBlock(reusableBytes, sizeout);
    if (sizeout < sizeblock) {// last block
      dataBlock.setEOF(true);
      try {
        closeFile();
      } catch (final CommandAbstractException ignored) {
        // nothing
      }
      isReady = false;
    }
    return dataBlock;
  }

  protected FileInputStream getFileInputStream() {
    if (!isReady) {
      return null;
    }
    try {
      if (currentRealFile == null) {
        currentRealFile = getFileFromPath(currentFile);
      }
    } catch (final CommandAbstractException e1) {
      return null;
    }
    @SuppressWarnings("resource")
    FileInputStream fileInputStreamTemp = null;
    try {
      fileInputStreamTemp = new FileInputStream(currentRealFile);//NOSONAR
      if (position != 0) {
        final long read = fileInputStreamTemp.skip(position);
        if (read != position) {
          logger.warn("Cannot ensure position: {} while is {}", position, read);
        }
      }
    } catch (final FileNotFoundException e) {
      FileUtils.close(fileInputStreamTemp);
      logger.error("File not found in getFileInputStream: {}", e.getMessage());
      return null;
    } catch (final IOException e) {
      FileUtils.close(fileInputStreamTemp);
      logger.error("Change position in getFileInputStream: {}", e.getMessage());
      return null;
    }
    return fileInputStreamTemp;
  }

  /**
   * Returns the RandomAccessFile in Out mode associated with the current
   * file.
   *
   * @return the RandomAccessFile (OUT="rw")
   */
  protected RandomAccessFile getRandomFile() {
    if (!isReady) {
      return null;
    }
    try {
      if (currentRealFile == null) {
        currentRealFile = getFileFromPath(currentFile);
      }
    } catch (final CommandAbstractException e1) {
      return null;
    }
    final RandomAccessFile raf;
    try {
      raf = new RandomAccessFile(currentRealFile, "rw");//NOSONAR
      raf.seek(position);
    } catch (final FileNotFoundException e) {
      logger.error("File not found in getRandomFile: {}", e.getMessage());
      return null;
    } catch (final IOException e) {
      logger.error("Change position in getRandomFile: {}", e.getMessage());
      return null;
    }
    return raf;
  }

  /**
   * Returns the FileOutputStream in Out mode associated with the current
   * file.
   *
   * @param append True if the FileOutputStream should be in append
   *     mode
   *
   * @return the FileOutputStream (OUT)
   */
  protected FileOutputStream getFileOutputStream(final boolean append) {
    if (!isReady) {
      return null;
    }
    try {
      if (currentRealFile == null) {
        currentRealFile = getFileFromPath(currentFile);
      }
    } catch (final CommandAbstractException e1) {
      return null;
    }
    if (position > 0) {
      if (currentRealFile.length() < position) {
        logger.error(
            "Cannot Change position in getFileOutputStream: file is smaller than required position");
        return null;
      }
      final RandomAccessFile raf = getRandomFile();
      try {
        raf.setLength(position);
        FileUtils.close(raf);
      } catch (final IOException e) {
        logger.error("Change position in getFileOutputStream: {}",
                     e.getMessage());
        return null;
      }
      if (logger.isDebugEnabled()) {
        logger.debug("New size: {}:{}", currentRealFile.length(), position);
      }
    }
    final FileOutputStream fos;
    try {
      fos = new FileOutputStream(currentRealFile, append);
    } catch (final FileNotFoundException e) {
      logger.error("File not found in getRandomFile: {}", e.getMessage());
      return null;
    }
    return fos;
  }
}