/** * This file is part of Waarp Project. * * Copyright 2009, Frederic Bregier, 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 java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import org.waarp.common.command.exception.CommandAbstractException; import org.waarp.common.command.exception.Reply550Exception; import org.waarp.common.exception.FileEndOfTransferException; import org.waarp.common.exception.FileTransferException; import org.waarp.common.file.AbstractFile; import org.waarp.common.file.DataBlock; import org.waarp.common.file.DirInterface; import org.waarp.common.file.SessionInterface; import org.waarp.common.logging.WaarpLogger; import org.waarp.common.logging.WaarpLoggerFactory; /** * File implementation for Filesystem Based * * @author Frederic Bregier * */ public abstract class FilesystemBasedFileImpl extends AbstractFile { /** * 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 = null; /** * Is this file in append mode */ protected boolean isAppend = false; /** * @param session * @param dir * It is not necessary the directory that owns this file. * @param path * @param append * @throws CommandAbstractException */ public FilesystemBasedFileImpl(SessionInterface session, FilesystemBasedDirImpl dir, String path, boolean append) throws CommandAbstractException { this.session = session; auth = (FilesystemBasedAuthImpl) session.getAuth(); this.dir = dir; currentFile = path; isAppend = append; File file = getFileFromPath(path); if (append) { try { setPosition(file.length()); } catch (IOException e) { // not ready return; } } else { try { setPosition(0); } catch (IOException e) { } } isReady = true; } /** * Special constructor for possibly external file * * @param session * @param dir * It is not necessary the directory that owns this file. * @param path */ public FilesystemBasedFileImpl(SessionInterface session, FilesystemBasedDirImpl dir, String path) { this.session = session; auth = (FilesystemBasedAuthImpl) session.getAuth(); this.dir = dir; currentFile = path; isReady = true; isAppend = false; position = 0; } public void clear() throws CommandAbstractException { super.clear(); currentFile = null; isAppend = false; } public SessionInterface getSession() { return session; } public DirInterface getDir() { return dir; } /** * Get the File from this path, checking first its validity * * @param path * @return the FileInterface * @throws CommandAbstractException */ protected File getFileFromPath(String path) throws CommandAbstractException { String newdir = getDir().validatePath(path); if (dir.isAbsolute(newdir)) { return new File(newdir); } String truedir = auth.getAbsolutePath(newdir); File file = new File(truedir); logger.debug("Final File: " + truedir + " CanRead: " + file.canRead()); return file; } /** * Get the relative path (without mount point) * * @param file * @return the relative path */ protected String getRelativePath(File file) { return auth.getRelativePath(FilesystemBasedDirImpl.normalizePath(file .getAbsolutePath())); } public boolean isDirectory() throws CommandAbstractException { checkIdentify(); File dir1 = getFileFromPath(currentFile); return dir1.isDirectory(); } public boolean isFile() throws CommandAbstractException { checkIdentify(); return getFileFromPath(currentFile).isFile(); } public String getFile() throws CommandAbstractException { checkIdentify(); return currentFile; } public boolean closeFile() throws CommandAbstractException { if (bfileChannelIn != null) { try { bfileChannelIn.close(); } catch (IOException e) { } bfileChannelIn = null; bbyteBuffer = null; } if (fileOutputStream != null) { /* * try { rafOut.getFD().sync(); } catch (SyncFailedException e1) { } catch (IOException * e1) { } */ try { fileOutputStream.flush(); fileOutputStream.close(); } catch (ClosedChannelException e) { // ignore } catch (IOException e) { throw new Reply550Exception("Close in error"); } fileOutputStream = null; } position = 0; isReady = false; // Do not clear the filename itself return true; } public boolean abortFile() throws CommandAbstractException { if (isInWriting() && ((FilesystemBasedFileParameterImpl) getSession() .getFileParameter()).deleteOnAbort) { delete(); } closeFile(); return true; } public long length() throws CommandAbstractException { checkIdentify(); if (!isReady) { return -1; } if (!exists()) { return -1; } return getFileFromPath(currentFile).length(); } public boolean isInReading() throws CommandAbstractException { if (!isReady) { return false; } return bfileChannelIn != null; } public boolean isInWriting() throws CommandAbstractException { if (!isReady) { return false; } return fileOutputStream != null; } public boolean canRead() throws CommandAbstractException { checkIdentify(); if (!isReady) { return false; } return getFileFromPath(currentFile).canRead(); } public boolean canWrite() throws CommandAbstractException { checkIdentify(); if (!isReady) { return false; } File file = getFileFromPath(currentFile); if (file.exists()) { return file.canWrite(); } return file.getParentFile().canWrite(); } public boolean exists() throws CommandAbstractException { checkIdentify(); if (!isReady) { return false; } return getFileFromPath(currentFile).exists(); } public boolean delete() throws CommandAbstractException { checkIdentify(); if (!isReady) { return false; } if (!exists()) { return true; } closeFile(); return getFileFromPath(currentFile).delete(); } public boolean renameTo(String path) throws CommandAbstractException { checkIdentify(); if (!isReady) { logger.warn("File not ready: {}", this); return false; } File file = getFileFromPath(currentFile); if (file.canRead()) { File newFile = getFileFromPath(path); if (newFile.exists()) { logger.warn("Target file already exists: "+newFile.getAbsolutePath()); return false; } if (newFile.getAbsolutePath().equals(file.getAbsolutePath())) { // already in the right position isReady = true; return true; } if (newFile.getParentFile().canWrite()) { if (!file.renameTo(newFile)) { FileOutputStream fileOutputStream = null; try { try { fileOutputStream = new FileOutputStream(newFile); } catch (FileNotFoundException e) { logger .warn("Cannot find file: " + newFile.getName(), e); return false; } FileChannel fileChannelOut = fileOutputStream.getChannel(); if (get(fileChannelOut)) { delete(); } else { try { fileChannelOut.close(); } catch (IOException e) { } logger.warn("Cannot write file: {}", newFile); return false; } } finally { try { if (fileOutputStream != null) { fileOutputStream.close(); } } catch (IOException e) { } } } currentFile = getRelativePath(newFile); isReady = true; logger.debug("File renamed to: {} and real position: {}", this, newFile); return true; } else { logger.warn("Cannot write file: {} from {}", newFile, file); return false; } } logger.warn("Cannot read file: {}", file); return false; } public DataBlock readDataBlock() throws FileTransferException, FileEndOfTransferException { if (isReady) { DataBlock dataBlock = new DataBlock(); ByteBuf buffer = null; buffer = getBlock(getSession().getBlockSize()); if (buffer != null) { dataBlock.setBlock(buffer); if (dataBlock.getByteCount() < getSession().getBlockSize()) { dataBlock.setEOF(true); } return dataBlock; } } throw new FileTransferException("No file is ready"); } public void writeDataBlock(DataBlock dataBlock) throws FileTransferException { if (isReady) { if (dataBlock.isEOF()) { writeBlockEnd(dataBlock.getBlock()); return; } writeBlock(dataBlock.getBlock()); return; } throw new FileTransferException("No file is ready while trying to write: " + dataBlock.toString()); } /** * Valid Position of this file */ private long position = 0; /** * FileOutputStream Out */ private FileOutputStream fileOutputStream = null; /** * FileChannel In */ private FileChannel bfileChannelIn = null; /** * Associated ByteBuffer */ private ByteBuffer bbyteBuffer = null; /** * Return the current position in the FileInterface. In write mode, it is the current file * length. * * @return the position */ public long getPosition() { return position; } /** * Change the position in the file. * * @param position * the position to set * @throws IOException */ public void setPosition(long position) throws IOException { this.position = position; if (bfileChannelIn != null) { bfileChannelIn = bfileChannelIn.position(position); } /* * if (rafOut != null) { rafOut.seek(position); } */ if (fileOutputStream != null) { fileOutputStream.flush(); fileOutputStream.close(); fileOutputStream = getFileOutputStream(true); if (fileOutputStream == null) { throw new IOException("File cannot changed of Position"); } } } private byte[] reusableBytes = null; /** * Write the current FileInterface with the given ByteBuf. The file is not limited to 2^32 * bytes since this write operation is in add mode. * * 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 void writeBlock(ByteBuf buffer) throws FileTransferException { if (!isReady) { throw new FileTransferException("No file is ready"); } // An empty buffer is allowed if (buffer == null) { return;// could do FileEndOfTransfer ? } if (fileOutputStream == null) { // rafOut = getRandomFile(); fileOutputStream = getFileOutputStream(position > 0); } if (fileOutputStream == null) { throw new FileTransferException("Internal error, file is not ready"); } int bufferSize = buffer.readableBytes(); int start = 0; byte[] newbuf; if (buffer.hasArray()) { start = buffer.arrayOffset(); newbuf = buffer.array(); buffer.readerIndex(buffer.readerIndex() + bufferSize); } else { if (reusableBytes == null || reusableBytes.length != bufferSize) { reusableBytes = new byte[bufferSize]; } newbuf = reusableBytes; buffer.readBytes(newbuf); } try { fileOutputStream.write(newbuf, start, bufferSize); } catch (IOException e2) { logger.error("Error during write:", e2); try { closeFile(); } catch (CommandAbstractException e1) { } // NO this.realFile.delete(); NO DELETE SINCE BY BLOCK IT CAN BE // REDO throw new FileTransferException("Internal error, file is not ready"); } position += bufferSize; } /** * End the Write of the current FileInterface with the given ByteBuf. 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 void writeBlockEnd(ByteBuf buffer) throws FileTransferException { writeBlock(buffer); try { closeFile(); } catch (CommandAbstractException e) { throw new FileTransferException("Close in error", e); } } /** * Get the current block ByteBuf of the current FileInterface. There is therefore no * limitation of the file size to 2^32 bytes. * * The returned block is limited to sizeblock. If the returned block is less than sizeblock * length, it is the last block to read. * * @param sizeblock * is the limit size for the block array * @return the resulting block ByteBuf (even empty) * @throws FileTransferException * @throws FileEndOfTransferException */ private ByteBuf getBlock(int sizeblock) throws FileTransferException, FileEndOfTransferException { if (!isReady) { throw new FileTransferException("No file is ready"); } if (bfileChannelIn == null) { bfileChannelIn = getFileChannel(); if (bfileChannelIn != null) { if (bbyteBuffer != null) { if (bbyteBuffer.capacity() != sizeblock) { bbyteBuffer = null; bbyteBuffer = ByteBuffer.allocateDirect(sizeblock); } } else { bbyteBuffer = ByteBuffer.allocateDirect(sizeblock); } } } if (bfileChannelIn == null) { throw new FileTransferException("Internal error, file is not ready"); } int sizeout = 0; while (sizeout < sizeblock) { try { int sizeread = bfileChannelIn.read(bbyteBuffer); if (sizeread <= 0) { break; } sizeout += sizeread; } catch (IOException e) { logger.error("Error during get:", e); try { closeFile(); } catch (CommandAbstractException e1) { } throw new FileTransferException("Internal error, file is not ready"); } } if (sizeout <= 0) { try { closeFile(); } catch (CommandAbstractException e1) { } isReady = false; throw new FileEndOfTransferException("End of file"); } bbyteBuffer.flip(); position += sizeout; ByteBuf buffer = Unpooled.wrappedBuffer(bbyteBuffer); bbyteBuffer.clear(); if (sizeout < sizeblock) {// last block try { closeFile(); } catch (CommandAbstractException e1) { } isReady = false; } return buffer; } /** * Write the FileInterface to the fileChannelOut, thus bypassing the limitation of the file size * to 2^32 bytes. * * This call closes the fileChannelOut with fileChannelOut.close() if the operation is in * success. * * @param fileChannelOut * @return True if OK, False in error. */ protected boolean get(FileChannel fileChannelOut) { if (!isReady) { return false; } FileChannel fileChannelIn = getFileChannel(); if (fileChannelIn == null) { return false; } long size = 0; long transfert = 0; try { size = fileChannelIn.size(); if (size < 0) { try { size = length(); } catch (CommandAbstractException e) { logger.error("Error during get:", e); return false; } if (size < 0) { logger.error("Error during get, wrong size: " + size); return false; } } long chunkSize = size; while (transfert < size) { chunkSize = size - transfert; transfert += fileChannelOut.transferFrom(fileChannelIn, transfert, chunkSize); } fileChannelOut.force(true); } catch (IOException e) { logger.error("Error during get:", e); return false; } finally { try { fileChannelOut.close(); } catch (IOException e) { } try { fileChannelIn.close(); } catch (IOException e) { } fileChannelIn = null; } if (transfert == size) { position += size; } return transfert == size; } /** * Returns the FileChannel in In mode associated with the current file. * * @return the FileChannel (IN mode) */ protected FileChannel getFileChannel() { if (!isReady) { return null; } File trueFile; try { trueFile = getFileFromPath(currentFile); } catch (CommandAbstractException e1) { return null; } FileChannel fileChannel = null; try { @SuppressWarnings("resource") FileInputStream fileInputStream = new FileInputStream(trueFile); fileChannel = fileInputStream.getChannel(); if (position != 0) { fileChannel = fileChannel.position(position); } } catch (FileNotFoundException e) { logger.error("File not found in getFileChannel:", e); return null; } catch (IOException e) { if (fileChannel != null) { try { fileChannel.close(); } catch (IOException e1) { } } logger.error("Change position in getFileChannel:", e); return null; } return fileChannel; } /** * Returns the RandomAccessFile in Out mode associated with the current file. * * @return the RandomAccessFile (OUT="rw") */ protected RandomAccessFile getRandomFile() { if (!isReady) { return null; } File trueFile; try { trueFile = getFileFromPath(currentFile); } catch (CommandAbstractException e1) { return null; } RandomAccessFile raf = null; try { raf = new RandomAccessFile(trueFile, "rw"); raf.seek(position); } catch (FileNotFoundException e) { logger.error("File not found in getRandomFile:", e); return null; } catch (IOException e) { logger.error("Change position in getRandomFile:", e); 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(boolean append) { if (!isReady) { return null; } File trueFile; try { trueFile = getFileFromPath(currentFile); } catch (CommandAbstractException e1) { return null; } if (position > 0) { if (trueFile.length() < position) { logger.error("Cannot Change position in getFileOutputStream: file is smaller than required position"); return null; } RandomAccessFile raf = getRandomFile(); try { raf.setLength(position); raf.close(); } catch (IOException e) { logger.error("Change position in getFileOutputStream:", e); return null; } logger.debug("New size: " + trueFile.length() + " : " + position); } FileOutputStream fos = null; try { fos = new FileOutputStream(trueFile, append); } catch (FileNotFoundException e) { logger.error("File not found in getRandomFile:", e); return null; } return fos; } }